Skip to content

Commit

Permalink
Add Admin Terraform Versions API (#186)
Browse files Browse the repository at this point in the history
* Add Admin Terraform Versions
  • Loading branch information
omarismail committed Mar 9, 2021
1 parent 3ecf508 commit 862478c
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 6 deletions.
176 changes: 176 additions & 0 deletions admin_terraform_version.go
@@ -0,0 +1,176 @@
package tfe

import (
"context"
"fmt"
"net/url"
"time"
)

// Compile-time proof of interface implementation.
var _ AdminTerraformVersions = (*adminTerraformVersions)(nil)

// AdminTerraformVersions describes all the admin terraform versions related methods that
// the Terraform Enterprise API supports.
// Note that admin terraform versions are only available in Terraform Enterprise.
//
// TFE API docs: https://www.terraform.io/docs/cloud/api/admin/terraform-versions.html
type AdminTerraformVersions interface {
// List all the terraform versions.
List(ctx context.Context, options AdminTerraformVersionsListOptions) (*AdminTerraformVersionsList, error)

// Read a terraform version by its ID.
Read(ctx context.Context, id string) (*AdminTerraformVersion, error)

// Create a terraform version.
Create(ctx context.Context, options AdminTerraformVersionCreateOptions) (*AdminTerraformVersion, error)

// Update a terraform version.
Update(ctx context.Context, id string, options AdminTerraformVersionUpdateOptions) (*AdminTerraformVersion, error)

// Delete a terraform version
Delete(ctx context.Context, id string) error
}

// adminTerraformVersions implements AdminTerraformVersions.
type adminTerraformVersions struct {
client *Client
}

// AdminTerraformVersion represents a Terraform Version
type AdminTerraformVersion struct {
ID string `jsonapi:"primary,terraform-versions"`
Version string `jsonapi:"attr,version"`
URL string `jsonapi:"attr,url"`
Sha string `jsonapi:"attr,sha"`
Official bool `jsonapi:"attr,official"`
Enabled bool `jsonapi:"attr,enabled"`
Beta bool `jsonapi:"attr,beta"`
Usage int `jsonapi:"attr,usage"`
CreatedAt time.Time `jsonapi:"attr,created-at,iso8601"`
}

// AdminTerraformVersionsListOptions represents the options for listing
// terraform versions.
type AdminTerraformVersionsListOptions struct {
ListOptions
}

// AdminTerraformVersionsList represents a list of terraform versions.
type AdminTerraformVersionsList struct {
*Pagination
Items []*AdminTerraformVersion
}

// List all the terraform versions.
func (a *adminTerraformVersions) List(ctx context.Context, options AdminTerraformVersionsListOptions) (*AdminTerraformVersionsList, error) {
req, err := a.client.newRequest("GET", "admin/terraform-versions", &options)
if err != nil {
return nil, err
}

tvl := &AdminTerraformVersionsList{}
err = a.client.do(ctx, req, tvl)
if err != nil {
return nil, err
}

return tvl, nil
}

// Read a terraform version by its ID.
func (a *adminTerraformVersions) Read(ctx context.Context, id string) (*AdminTerraformVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidTerraformVersionID
}

u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id))
req, err := a.client.newRequest("GET", u, nil)
if err != nil {
return nil, err
}

tfv := &AdminTerraformVersion{}
err = a.client.do(ctx, req, tfv)
if err != nil {
return nil, err
}

return tfv, nil
}

// AdminTerraformVersionCreateOptions for creating a terraform version.
// https://www.terraform.io/docs/cloud/api/admin/terraform-versions.html#request-body
type AdminTerraformVersionCreateOptions struct {
Type string `jsonapi:"primary,terraform-versions"`
Version *string `jsonapi:"attr,version"`
URL *string `jsonapi:"attr,url"`
Sha *string `jsonapi:"attr,sha"`
Official *bool `jsonapi:"attr,official"`
Enabled *bool `jsonapi:"attr,enabled"`
Beta *bool `jsonapi:"attr,beta"`
}

// Create a new terraform version.
func (a *adminTerraformVersions) Create(ctx context.Context, options AdminTerraformVersionCreateOptions) (*AdminTerraformVersion, error) {
req, err := a.client.newRequest("POST", "admin/terraform-versions", &options)
if err != nil {
return nil, err
}

tfv := &AdminTerraformVersion{}
err = a.client.do(ctx, req, tfv)
if err != nil {
return nil, err
}

return tfv, nil
}

// AdminTerraformVersionUpdateOptions for updating terraform version.
// https://www.terraform.io/docs/cloud/api/admin/terraform-versions.html#request-body
type AdminTerraformVersionUpdateOptions struct {
Type string `jsonapi:"primary,terraform-versions"`
Version *string `jsonapi:"attr,version,omitempty"`
URL *string `jsonapi:"attr,url,omitempty"`
Sha *string `jsonapi:"attr,sha,omitempty"`
Official *bool `jsonapi:"attr,official,omitempty"`
Enabled *bool `jsonapi:"attr,enabled,omitempty"`
Beta *bool `jsonapi:"attr,beta,omitempty"`
}

// Update an existing terraform version.
func (a *adminTerraformVersions) Update(ctx context.Context, id string, options AdminTerraformVersionUpdateOptions) (*AdminTerraformVersion, error) {
if !validStringID(&id) {
return nil, ErrInvalidTerraformVersionID
}

u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id))
req, err := a.client.newRequest("PATCH", u, &options)
if err != nil {
return nil, err
}

tfv := &AdminTerraformVersion{}
err = a.client.do(ctx, req, tfv)
if err != nil {
return nil, err
}

return tfv, nil
}

// Delete a terraform version.
func (a *adminTerraformVersions) Delete(ctx context.Context, id string) error {
if !validStringID(&id) {
return ErrInvalidTerraformVersionID
}

u := fmt.Sprintf("admin/terraform-versions/%s", url.QueryEscape(id))
req, err := a.client.newRequest("DELETE", u, nil)
if err != nil {
return err
}

return a.client.do(ctx, req, nil)
}
154 changes: 154 additions & 0 deletions admin_terraform_version_test.go
@@ -0,0 +1,154 @@
package tfe

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAdminTerraformVersions_List(t *testing.T) {
skipIfCloud(t)

client := testClient(t)
ctx := context.Background()

t.Run("without list options", func(t *testing.T) {
tfList, err := client.Admin.TerraformVersions.List(ctx, AdminTerraformVersionsListOptions{})
require.NoError(t, err)

assert.NotEmpty(t, tfList.Items)
})

t.Run("with list options", func(t *testing.T) {
tfList, err := client.Admin.TerraformVersions.List(ctx, AdminTerraformVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 999,
PageSize: 100,
},
})
require.NoError(t, err)
// Out of range page number, so the items should be empty
assert.Empty(t, tfList.Items)
assert.Equal(t, 999, tfList.CurrentPage)

tfList, err = client.Admin.TerraformVersions.List(ctx, AdminTerraformVersionsListOptions{
ListOptions: ListOptions{
PageNumber: 1,
PageSize: 100,
},
})
require.NoError(t, err)
assert.Equal(t, 1, tfList.CurrentPage)
for _, item := range tfList.Items {
assert.NotNil(t, item.ID)
assert.NotNil(t, item.Version)
assert.NotNil(t, item.URL)
assert.NotNil(t, item.Sha)
assert.NotNil(t, item.Official)
assert.NotNil(t, item.Enabled)
assert.NotNil(t, item.Beta)
assert.NotNil(t, item.Usage)
assert.NotNil(t, item.CreatedAt)
}
})
}

func TestAdminTerraformVersions_CreateDelete(t *testing.T) {
skipIfCloud(t)

client := testClient(t)
ctx := context.Background()

t.Run("with valid options", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{
Version: String("1.1.1"),
URL: String("https://www.hashicorp.com"),
Sha: String(genSha("secret", "data")),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)

defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, tfv.ID)
require.NoError(t, deleteErr)
}()

assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.URL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
})

t.Run("with empty options", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{}

_, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.Error(t, err)
})
}

func TestAdminTerraformVersions_ReadUpdate(t *testing.T) {
skipIfCloud(t)

client := testClient(t)
ctx := context.Background()

t.Run("reads and updates", func(t *testing.T) {
opts := AdminTerraformVersionCreateOptions{
Version: String("1.1.1"),
URL: String("https://www.hashicorp.com"),
Sha: String(genSha("secret", "data")),
Official: Bool(false),
Enabled: Bool(false),
Beta: Bool(false),
}
tfv, err := client.Admin.TerraformVersions.Create(ctx, opts)
require.NoError(t, err)
id := tfv.ID

defer func() {
deleteErr := client.Admin.TerraformVersions.Delete(ctx, id)
require.NoError(t, deleteErr)
}()

tfv, err = client.Admin.TerraformVersions.Read(ctx, id)
require.NoError(t, err)

assert.Equal(t, *opts.Version, tfv.Version)
assert.Equal(t, *opts.URL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)

updateVersion := "1.1.2"
updateURL := "https://app.terraform.io/"
updateOpts := AdminTerraformVersionUpdateOptions{
Version: String(updateVersion),
URL: String(updateURL),
}

tfv, err = client.Admin.TerraformVersions.Update(ctx, id, updateOpts)
require.NoError(t, err)

assert.Equal(t, updateVersion, tfv.Version)
assert.Equal(t, updateURL, tfv.URL)
assert.Equal(t, *opts.Sha, tfv.Sha)
assert.Equal(t, *opts.Official, tfv.Official)
assert.Equal(t, *opts.Enabled, tfv.Enabled)
assert.Equal(t, *opts.Beta, tfv.Beta)
})

t.Run("with non existant terraform version", func(t *testing.T) {
randomID := "random-id"
_, err := client.Admin.TerraformVersions.Read(ctx, randomID)
require.Error(t, err)
})
}
9 changes: 9 additions & 0 deletions errors.go
Expand Up @@ -73,4 +73,13 @@ var (

// ErrInvalidCostEstimateID is returned when the cost estimate ID is invalid.
ErrInvalidCostEstimateID = errors.New("invalid value for cost estimate ID")

// Terraform Versions

// ErrInvalidTerraformVersionID is returned when the ID for a terraform
// version is invalid.
ErrInvalidTerraformVersionID = errors.New("invalid value for terraform version ID")

// ErrInvalidTerraformVersionType is returned when the type is not valid.
ErrInvalidTerraformVersionType = errors.New("invalid type for terraform version. Please use 'terraform-version'")
)
10 changes: 10 additions & 0 deletions helper_test.go
Expand Up @@ -2,8 +2,11 @@ package tfe

import (
"context"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -965,6 +968,13 @@ func createWorkspaceWithVCS(t *testing.T, client *Client, org *Organization) (*W
}
}

func genSha(secret, data string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(data))
sha := hex.EncodeToString(h.Sum(nil))
return sha
}

func randomString(t *testing.T) string {
v, err := uuid.GenerateUUID()
if err != nil {
Expand Down
14 changes: 8 additions & 6 deletions tfe.go
Expand Up @@ -134,9 +134,10 @@ type Client struct {
// wide admin settings. These are only available for Terraform Enterprise and
// do not function against Terraform Cloud.
type Admin struct {
Organizations AdminOrganizations
Workspaces AdminWorkspaces
Runs AdminRuns
Organizations AdminOrganizations
Workspaces AdminWorkspaces
Runs AdminRuns
TerraformVersions AdminTerraformVersions
}

// Meta contains any Terraform Cloud APIs which provide data about the API itself.
Expand Down Expand Up @@ -218,9 +219,10 @@ func NewClient(cfg *Config) (*Client, error) {

// Create Admin
client.Admin = Admin{
Organizations: &adminOrganizations{client: client},
Workspaces: &adminWorkspaces{client: client},
Runs: &adminRuns{client: client},
Organizations: &adminOrganizations{client: client},
Workspaces: &adminWorkspaces{client: client},
Runs: &adminRuns{client: client},
TerraformVersions: &adminTerraformVersions{client: client},
}

// Create the services.
Expand Down

0 comments on commit 862478c

Please sign in to comment.