diff --git a/admin_terraform_version.go b/admin_terraform_version.go new file mode 100644 index 000000000..72af9e48a --- /dev/null +++ b/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) +} diff --git a/admin_terraform_version_test.go b/admin_terraform_version_test.go new file mode 100644 index 000000000..e815aeff9 --- /dev/null +++ b/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) + }) +} diff --git a/errors.go b/errors.go index f287dc471..dc73ec51c 100644 --- a/errors.go +++ b/errors.go @@ -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'") ) diff --git a/helper_test.go b/helper_test.go index 91a4a0b39..30db798b4 100644 --- a/helper_test.go +++ b/helper_test.go @@ -2,8 +2,11 @@ package tfe import ( "context" + "crypto/hmac" "crypto/md5" + "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "io/ioutil" "os" @@ -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 { diff --git a/tfe.go b/tfe.go index 43c476c84..d6f1e5c4f 100644 --- a/tfe.go +++ b/tfe.go @@ -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. @@ -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.