Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Admin Terraform Versions API #186

Merged
merged 5 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions admin_terraform_version.go
Original file line number Diff line number Diff line change
@@ -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 {
omarismail marked this conversation as resolved.
Show resolved Hide resolved
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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like what you landed on here, with Type used in *Options but leaving the necessary tag as ID in the struct (since jsonapi does say it should be used on the primary key) Nice work!

One thing I'd consider adding in your followup is a docstring indicating what this is for in each *Options (something far more helpful than "Internal use only!"); maybe even just See /link/to/readme 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄 .

adding in your followup is a docstring indicating what this is for

👍 will do

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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