Skip to content

Commit

Permalink
Fix all TODOs, add examples and update README
Browse files Browse the repository at this point in the history
  • Loading branch information
Sander van Harmelen committed Jun 28, 2018
1 parent 3a424de commit 310afc2
Show file tree
Hide file tree
Showing 28 changed files with 424 additions and 663 deletions.
115 changes: 113 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,117 @@
Terraform Enterprise Go Client
==============================

This is an API client for [Terraform Enterprise][tfe].
This is an API client for [Terraform Enterprise](https://www.hashicorp.com/products/terraform).

[tfe]: https://www.hashicorp.com/products/terraform
[![Build Status](https://travis-ci.org/hashicorp/go-tfe.svg?branch=master)](https://travis-ci.org/hashicorp/go-tfe)
[![GitHub license](https://img.shields.io/github/license/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/hashicorp/go-tfe?status.svg)](https://godoc.org/github.com/hashicorp/go-tfe)
[![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-tfe)](https://goreportcard.com/report/github.com/hashicorp/go-tfe)
[![GitHub issues](https://img.shields.io/github/issues/hashicorp/go-tfe.svg)](https://github.com/hashicorp/go-tfe/issues)

## NOTE

The Terraform Enterprise API endpoints are in beta and are subject to change!
So that means this API client is also in beta and is also subject to change. We
will indicate any breaking changes by releasing new versions. Until the release
of v1.0, any minor version changes will indicate possible breaking changes. Patch
version changes will be used for both bugfixes and non-breaking changes.

## Coverage

Currently the following endpoints are supported:

- [x] [Accounts](https://www.terraform.io/docs/enterprise/api/account.html)
- [x] [Configuration Versions](https://www.terraform.io/docs/enterprise/api/configuration-versions.html)
- [x] [OAuth Clients](https://www.terraform.io/docs/enterprise/api/oauth-clients.html)
- [x] [OAuth Tokens](https://www.terraform.io/docs/enterprise/api/oauth-tokens.html)
- [x] [Organizations](https://www.terraform.io/docs/enterprise/api/organizations.html)
- [x] [Organization Tokens](https://www.terraform.io/docs/enterprise/api/organization-tokens.html)
- [x] [Policies](https://www.terraform.io/docs/enterprise/api/policies.html)
- [x] [Policy Checks](https://www.terraform.io/docs/enterprise/api/policy-checks.html)
- [ ] [Registry Modules](https://www.terraform.io/docs/enterprise/api/modules.html)
- [x] [Runs](https://www.terraform.io/docs/enterprise/api/run.html)
- [x] [SSH Keys](https://www.terraform.io/docs/enterprise/api/ssh-keys.html)
- [x] [Team Access](https://www.terraform.io/docs/enterprise/api/team-access.html)
- [x] [Team Memberships](https://www.terraform.io/docs/enterprise/api/team-members.html)
- [x] [Team Tokens](https://www.terraform.io/docs/enterprise/api/team-tokens.html)
- [x] [Teams](https://www.terraform.io/docs/enterprise/api/teams.html)
- [x] [Variables](https://www.terraform.io/docs/enterprise/api/variables.html)
- [x] [Workspaces](https://www.terraform.io/docs/enterprise/api/workspaces.html)
- [ ] [Admin](https://www.terraform.io/docs/enterprise/api/admin/index.html)

## Installation

```
go get -u github.com/hashicorp/go-tfe
```

## Usage

```go
import tfe "github.com/hashicorp/go-tfe"
```

Construct a new TFE client, then use the various endpoints on the client to
access different parts of the Terraform Enterprise API. For example, to list
all organizations:

```go
config := &tfe.Config{
Token: "UXsybZKSz07IEw.tfev2.FajRykbzcnG9ESrhBjBMLNUSsPp69qLyzclIskE",
}

client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}

orgs, err := client.Organizations.List(OrganizationListOptions{})
if err != nil {
log.Fatal(err)
}
```

### Examples

The [examples](https://github.com/hashicorp/go-tfe/tree/master/examples) directory
contains a couple of examples. One of which is listed here as well:

```go
package main

import (
"log"

tfe "github.com/hashicorp/go-tfe"
)

func main() {
config := &tfe.Config{
Token: "UXsybZKSz07IEw.tfev2.FajRykbzcnG9ESrhBjBMLNUSsPp69qLyzclIskE",
}

client, err := tfe.NewClient(config)
if err != nil {
log.Fatal(err)
}

// Create a new organization
options := tfe.OrganizationCreateOptions{
Name: tfe.String("example"),
Email: tfe.String("[email protected]"),
}
org, err := client.Organizations.Create(options)
if err != nil {
log.Fatal(err)
}

// Delete an organization
err = client.Organizations.Delete(org.Name)
if err != nil {
log.Fatal(err)
}
}
```

For complete usage of the API client, see the full [package docs](https://godoc.org/github.com/hashicorp/go-tfe).
123 changes: 112 additions & 11 deletions configuration_version.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package tfe

import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io/ioutil"
"io"
"os"
"path/filepath"
"time"

"github.com/hashicorp/go-tfe/slug"
)

// ConfigurationVersions handles communication with the configuration version
Expand Down Expand Up @@ -152,22 +154,121 @@ func (s *ConfigurationVersions) Retrieve(cvID string) (*ConfigurationVersion, er
// upload URL from a configuration version and the path to the configuration
// files on disk.
func (s *ConfigurationVersions) Upload(url, path string) error {
fh, err := ioutil.TempFile("", "go-tfe")
body, err := s.pack(path)
if err != nil {
return err
}
fh.Close()
defer os.Remove(fh.Name())

if _, err := slug.Pack(path, fh.Name()); err != nil {
req, err := s.client.newRequest("PUT", url, body)
if err != nil {
return err
}

fh, err = os.Open(fh.Name())
_, err = s.client.do(req, nil)

return err
}

// pack creates a compressed tar file containing the configuration files found
// in the provided src directory and returns the archive as raw bytes.
func (s *ConfigurationVersions) pack(src string) ([]byte, error) {
buf := new(bytes.Buffer)

// Gzip compress all the output data
gzipW := gzip.NewWriter(buf)

// Tar the file contents
tarW := tar.NewWriter(gzipW)

// Walk the tree of files
err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Check the file type and if we need to write the body
keepFile, writeBody := checkFileMode(info.Mode())
if !keepFile {
return nil
}

// Get the relative path from the unpack directory
subpath, err := filepath.Rel(src, path)
if err != nil {
return fmt.Errorf("Failed to get relative path for file %q: %v", path, err)
}
if subpath == "." {
return nil
}

// Read the symlink target. We don't track the error because
// it doesn't matter if there is an error.
target, _ := os.Readlink(path)

// Build the file header for the tar entry
header, err := tar.FileInfoHeader(info, target)
if err != nil {
return fmt.Errorf("Failed creating archive header for file %q: %v", path, err)
}

// Modify the header to properly be the full subpath
header.Name = subpath
if info.IsDir() {
header.Name += "/"
}

// Write the header first to the archive.
if err := tarW.WriteHeader(header); err != nil {
return fmt.Errorf("Failed writing archive header for file %q: %v", path, err)
}

// Skip writing file data for certain file types (above).
if !writeBody {
return nil
}

f, err := os.Open(path)
if err != nil {
return fmt.Errorf("Failed opening file %q for archiving: %v", path, err)
}
defer f.Close()

if _, err = io.Copy(tarW, f); err != nil {
return fmt.Errorf("Failed copying file %q to archive: %v", path, err)
}

return nil
})
if err != nil {
return err
return nil, err
}

// Flush the tar writer
if err := tarW.Close(); err != nil {
return nil, fmt.Errorf("Failed to close the tar archive: %v", err)
}

// Flush the gzip writer
if err := gzipW.Close(); err != nil {
return nil, fmt.Errorf("Failed to close the gzip writer: %v", err)
}

return buf.Bytes(), nil
}

// checkFileMode is used to examine an os.FileMode and determine if it should
// be included in the archive, and if it has a data body which needs writing.
func checkFileMode(m os.FileMode) (keep, body bool) {
switch {
case m.IsRegular():
return true, true

case m.IsDir():
return true, false

case m&os.ModeSymlink != 0:
return true, false
}
// Already have a defer os.Remove() on this.

return s.client.upload(url, fh)
return false, false
}
67 changes: 62 additions & 5 deletions configuration_version_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package tfe

import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"time"

Expand Down Expand Up @@ -78,8 +82,6 @@ func TestConfigurationVersionsCreate(t *testing.T) {
cv,
refreshed,
} {
// TODO: Fix this. API does not return workspace associations.
// assert.Equal(t, wTest.ID, item.Workspace.ID)
assert.NotEmpty(t, item.ID)
assert.Empty(t, item.Error)
assert.Equal(t, item.Source, ConfigurationSourceAPI)
Expand Down Expand Up @@ -119,7 +121,7 @@ func TestConfigurationVersionsRetrieve(t *testing.T) {
t.Run("when the configuration version does not exist", func(t *testing.T) {
cv, err := client.ConfigurationVersions.Retrieve("nonexisting")
assert.Nil(t, cv)
assert.EqualError(t, err, "Resource not found")
assert.EqualError(t, err, "Error: not found")
})

t.Run("with invalid configuration version id", func(t *testing.T) {
Expand All @@ -138,7 +140,7 @@ func TestConfigurationVersionsUpload(t *testing.T) {
t.Run("with valid options", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
cv.UploadURL,
"test-fixtures/configuration-version.tar.gz",
"test-fixtures/config-version",
)
require.NoError(t, err)

Expand All @@ -163,7 +165,7 @@ func TestConfigurationVersionsUpload(t *testing.T) {
t.Run("without a valid upload URL", func(t *testing.T) {
err := client.ConfigurationVersions.Upload(
cv.UploadURL[:len(cv.UploadURL)-10]+"nonexisting",
"test-fixtures/configuration-version.tar.gz",
"test-fixtures/config-version",
)
assert.Error(t, err)
})
Expand All @@ -176,3 +178,58 @@ func TestConfigurationVersionsUpload(t *testing.T) {
assert.Error(t, err)
})
}

func TestConfigurationVersionsPack(t *testing.T) {
client := testClient(t)

t.Run("with a valid path", func(t *testing.T) {
raw, err := client.ConfigurationVersions.pack("test-fixtures/archive-dir")
require.NoError(t, err)

gzipR, err := gzip.NewReader(bytes.NewReader(raw))
require.NoError(t, err)

tarR := tar.NewReader(gzipR)
var (
symFound bool
fileList []string
slugSize int64
)
for {
hdr, err := tarR.Next()
if err == io.EOF {
break
}
require.NoError(t, err)

fileList = append(fileList, hdr.Name)
if hdr.Typeflag == tar.TypeReg || hdr.Typeflag == tar.TypeRegA {
slugSize += hdr.Size
}

if hdr.Name == "sub/foo.txt" {
require.EqualValues(t, tar.TypeSymlink, hdr.Typeflag, "expect symlink for 'sub/foo.txt'")
assert.Equal(t, "../foo.txt", hdr.Linkname, "expect target of '../foo.txt'")
symFound = true
}
}

t.Run("confirm we saw and handled a symlink", func(t *testing.T) {
assert.True(t, symFound)
})

t.Run("check that the archive was created correctly", func(t *testing.T) {
expectedFiles := []string{"bar.txt", "exe", "foo.txt", "sub/", "sub/foo.txt", "sub/zip.txt"}
expectedSize := int64(12)

assert.Equal(t, expectedFiles, fileList)
assert.Equal(t, expectedSize, slugSize)
})
})

t.Run("without a valid path", func(t *testing.T) {
raw, err := client.ConfigurationVersions.pack("nonexisting")
assert.Nil(t, raw)
assert.Error(t, err)
})
}
Loading

0 comments on commit 310afc2

Please sign in to comment.