diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c06aaa1..5f9f70181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,14 @@ ## Enhancements * Adds Bitbucket Data Center as a new `ServiceProviderType` and ensures similar validation as Bitbucket Server by @zainq11 [#879](https://github.com/hashicorp/go-tfe/pull/879) +* Add `GlobalRunTasks` field to `Entitlements`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865) +* Add `Global` field to `RunTask`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865) +* Add `Stages` field to `WorkspaceRunTask`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865) -# v1.49.0 +## Deprecations +* The `Stage` field has been deprecated on `WorkspaceRunTask`. Instead, use `Stages`. by @glennsarti [#865](https://github.com/hashicorp/go-tfe/pull/865) +# v1.49.0 ## Enhancements * Adds `post_apply` to list of possible `stages` for Run Tasks by @glennsarti [#878](https://github.com/hashicorp/go-tfe/pull/878) diff --git a/helper_test.go b/helper_test.go index 1f34b0280..b2d794e34 100644 --- a/helper_test.go +++ b/helper_test.go @@ -18,7 +18,6 @@ import ( "io" "math/rand" "net/http" - "net/url" "os" "os/exec" "path/filepath" @@ -38,31 +37,6 @@ const agentVersion = "1.3.0" var _testAccountDetails *TestAccountDetails -type featureSet struct { - ID string `jsonapi:"primary,feature-sets"` -} - -type featureSetList struct { - Items []*featureSet - *Pagination -} - -type featureSetListOptions struct { - Q string `url:"q,omitempty"` -} - -type retryableFn func() (interface{}, error) - -type updateFeatureSetOptions struct { - Type string `jsonapi:"primary,subscription"` - RunsCeiling int `jsonapi:"attr,runs-ceiling"` - ContractStartAt time.Time `jsonapi:"attr,contract-start-at,iso8601"` - ContractUserLimit int `jsonapi:"attr,contract-user-limit"` - ContractApplyLimit int `jsonapi:"attr,contract-apply-limit"` - - FeatureSet *featureSet `jsonapi:"relation,feature-set"` -} - func testClient(t *testing.T) *Client { client, err := NewClient(&Config{ RetryServerErrors: true, @@ -959,7 +933,7 @@ func createOrganizationWithOptions(t *testing.T, client *Client, options Organiz ctx := context.Background() org, err := client.Organizations.Create(ctx, options) if err != nil { - t.Fatal(err) + t.Fatalf("Failed to create organization: %s", err) } return org, func() { @@ -2526,49 +2500,9 @@ func createVariableSetVariable(t *testing.T, client *Client, vs *VariableSet, op } // Attempts to upgrade an organization to the business plan. Requires a user token with admin access. -func upgradeOrganizationSubscription(t *testing.T, client *Client, organization *Organization) { - if enterpriseEnabled() { - t.Skip("Cannot upgrade an organization's subscription when enterprise is enabled. Set ENABLE_TFE=0 to run.") - } - - adminClient := testAdminClient(t, provisionLicensesAdmin) - req, err := adminClient.NewRequest("GET", "admin/feature-sets", featureSetListOptions{ - Q: "Business", - }) - if err != nil { - t.Fatal(err) - return - } - - fsl := &featureSetList{} - err = req.Do(context.Background(), fsl) - if err != nil { - t.Fatalf("failed to enumerate feature sets: %v", err) - return - } else if len(fsl.Items) == 0 { - t.Fatalf("feature set response was empty") - return - } - - opts := updateFeatureSetOptions{ - RunsCeiling: 10, - ContractStartAt: time.Now(), - ContractUserLimit: 1000, - ContractApplyLimit: 5000, - FeatureSet: fsl.Items[0], - } - - u := fmt.Sprintf("admin/organizations/%s/subscription", url.QueryEscape(organization.Name)) - req, err = adminClient.NewRequest("POST", u, &opts) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - return - } - - err = req.Do(context.Background(), nil) - if err != nil { - t.Fatalf("Failed to upgrade subscription: %v", err) - } +// DEPRECATED : Please use the newSubscriptionUpdater instead. +func upgradeOrganizationSubscription(t *testing.T, _ *Client, organization *Organization) { + newSubscriptionUpdater(organization).WithBusinessPlan().Update(t) } func createProject(t *testing.T, client *Client, org *Organization) (*Project, func()) { diff --git a/internal_run_task.go b/internal_run_task.go new file mode 100644 index 000000000..ee62fcf82 --- /dev/null +++ b/internal_run_task.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +// A private struct we need for unmarshalling +type internalRunTask struct { + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Description string `jsonapi:"attr,description"` + Category string `jsonapi:"attr,category"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` + Enabled bool `jsonapi:"attr,enabled"` + RawGlobal map[string]interface{} `jsonapi:"attr,global-configuration,omitempty"` + + Organization *Organization `jsonapi:"relation,organization"` + WorkspaceRunTasks []*internalWorkspaceRunTask `jsonapi:"relation,workspace-tasks"` +} + +// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using map[string]interface{} +// and then perform our own conversion from the map into a GlobalRunTask struct +func (irt internalRunTask) ToRunTask() *RunTask { + obj := RunTask{ + ID: irt.ID, + Name: irt.Name, + URL: irt.URL, + Description: irt.Description, + Category: irt.Category, + HMACKey: irt.HMACKey, + Enabled: irt.Enabled, + + Organization: irt.Organization, + } + + // Convert the WorkspaceRunTasks + workspaceTasks := make([]*WorkspaceRunTask, len(irt.WorkspaceRunTasks)) + for idx, rawTask := range irt.WorkspaceRunTasks { + if rawTask != nil { + workspaceTasks[idx] = rawTask.ToWorkspaceRunTask() + } + } + obj.WorkspaceRunTasks = workspaceTasks + + // Check if the global configuration exists + if val, ok := irt.RawGlobal["enabled"]; !ok { + // The enabled property is required so we can assume now that the + // global configuration was not supplied + return &obj + } else if boolVal, ok := val.(bool); !ok { + // The enabled property exists but it is invalid (Couldn't cast to boolean) + // so assume the global configuration was not supplied + return &obj + } else { + obj.Global = &GlobalRunTask{ + Enabled: boolVal, + } + } + + // Global Enforcement Level + if val, ok := irt.RawGlobal["enforcement-level"]; ok { + if stringVal, ok := val.(string); ok { + obj.Global.EnforcementLevel = TaskEnforcementLevel(stringVal) + } + } + + // Global Stages + if val, ok := irt.RawGlobal["stages"]; ok { + if stringsVal, ok := val.([]interface{}); ok { + obj.Global.Stages = make([]Stage, len(stringsVal)) + for idx, stageName := range stringsVal { + if stringVal, ok := stageName.(string); ok { + obj.Global.Stages[idx] = Stage(stringVal) + } + } + } + } + + return &obj +} + +// A private struct we need for unmarshalling +type internalRunTaskList struct { + *Pagination + Items []*internalRunTask +} + +// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using +// the internal RunTask struct and convert that a RunTask +func (irt internalRunTaskList) ToRunTaskList() *RunTaskList { + obj := RunTaskList{ + Pagination: irt.Pagination, + Items: make([]*RunTask, len(irt.Items)), + } + + for idx, src := range irt.Items { + if src != nil { + obj.Items[idx] = src.ToRunTask() + } + } + + return &obj +} diff --git a/internal_workspace_run_task.go b/internal_workspace_run_task.go new file mode 100644 index 000000000..c6dce495f --- /dev/null +++ b/internal_workspace_run_task.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +// A private struct we need for unmarshalling +type internalWorkspaceRunTask struct { + ID string `jsonapi:"primary,workspace-tasks"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` + Stage Stage `jsonapi:"attr,stage"` + Stages []string `jsonapi:"attr,stages"` + + RunTask *RunTask `jsonapi:"relation,task"` + Workspace *Workspace `jsonapi:"relation,workspace"` +} + +// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using map[string]interface{} +// and then perform our own conversion for the Stages +func (irt internalWorkspaceRunTask) ToWorkspaceRunTask() *WorkspaceRunTask { + obj := WorkspaceRunTask{ + ID: irt.ID, + EnforcementLevel: irt.EnforcementLevel, + Stage: irt.Stage, + Stages: make([]Stage, len(irt.Stages)), + RunTask: irt.RunTask, + Workspace: irt.Workspace, + } + + for idx, val := range irt.Stages { + obj.Stages[idx] = Stage(val) + } + + return &obj +} + +// A private struct we need for unmarshalling +type internalWorkspaceRunTaskList struct { + *Pagination + Items []*internalWorkspaceRunTask +} + +// Due to https://github.com/google/jsonapi/issues/74 we must first unmarshall using +// the internal WorkspaceRunTask struct and convert that a WorkspaceRunTask +func (irt internalWorkspaceRunTaskList) ToWorkspaceRunTaskList() *WorkspaceRunTaskList { + obj := WorkspaceRunTaskList{ + Pagination: irt.Pagination, + Items: make([]*WorkspaceRunTask, len(irt.Items)), + } + + for idx, src := range irt.Items { + if src != nil { + obj.Items[idx] = src.ToWorkspaceRunTask() + } + } + + return &obj +} diff --git a/organization.go b/organization.go index e8d4274b6..1ec47a47f 100644 --- a/organization.go +++ b/organization.go @@ -160,6 +160,7 @@ type Entitlements struct { Agents bool `jsonapi:"attr,agents"` AuditLogging bool `jsonapi:"attr,audit-logging"` CostEstimation bool `jsonapi:"attr,cost-estimation"` + GlobalRunTasks bool `jsonapi:"attr,global-run-tasks"` Operations bool `jsonapi:"attr,operations"` PrivateModuleRegistry bool `jsonapi:"attr,private-module-registry"` RunTasks bool `jsonapi:"attr,run-tasks"` diff --git a/run_task.go b/run_task.go index b0d2358e4..26c9334aa 100644 --- a/run_task.go +++ b/run_task.go @@ -38,25 +38,33 @@ type RunTasks interface { AttachToWorkspace(ctx context.Context, workspaceID string, runTaskID string, enforcementLevel TaskEnforcementLevel) (*WorkspaceRunTask, error) } -// runTasks implements RunTasks +// runTasks implements RunTasks type runTasks struct { client *Client } // RunTask represents a TFC/E run task type RunTask struct { - ID string `jsonapi:"primary,tasks"` - Name string `jsonapi:"attr,name"` - URL string `jsonapi:"attr,url"` - Description string `jsonapi:"attr,description"` - Category string `jsonapi:"attr,category"` - HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` - Enabled bool `jsonapi:"attr,enabled"` + ID string `jsonapi:"primary,tasks"` + Name string `jsonapi:"attr,name"` + URL string `jsonapi:"attr,url"` + Description string `jsonapi:"attr,description"` + Category string `jsonapi:"attr,category"` + HMACKey *string `jsonapi:"attr,hmac-key,omitempty"` + Enabled bool `jsonapi:"attr,enabled"` + Global *GlobalRunTask `jsonapi:"attr,global-configuration,omitempty"` Organization *Organization `jsonapi:"relation,organization"` WorkspaceRunTasks []*WorkspaceRunTask `jsonapi:"relation,workspace-tasks"` } +// GlobalRunTask represents the global configuration of a TFC/E run task +type GlobalRunTask struct { + Enabled bool `jsonapi:"attr,enabled"` + Stages []Stage `jsonapi:"attr,stages"` + EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` +} + // RunTaskList represents a list of run tasks type RunTaskList struct { *Pagination @@ -87,6 +95,13 @@ type RunTaskReadOptions struct { Include []RunTaskIncludeOpt `url:"include,omitempty"` } +// GlobalRunTask represents the optional global configuration of a TFC/E run task +type GlobalRunTaskOptions struct { + Enabled *bool `jsonapi:"attr,enabled,omitempty"` + Stages *[]Stage `jsonapi:"attr,stages,omitempty"` + EnforcementLevel *TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"` +} + // RunTaskCreateOptions represents the set of options for creating a run task type RunTaskCreateOptions struct { // Type is a public field utilized by JSON:API to @@ -112,6 +127,9 @@ type RunTaskCreateOptions struct { // Optional: Whether the task should be enabled Enabled *bool `jsonapi:"attr,enabled,omitempty"` + + // Optional: Whether the task contains global configuration + Global *GlobalRunTaskOptions `jsonapi:"attr,global-configuration,omitempty"` } // RunTaskUpdateOptions represents the set of options for updating an organization's run task @@ -139,6 +157,9 @@ type RunTaskUpdateOptions struct { // Optional: Whether the task should be enabled Enabled *bool `jsonapi:"attr,enabled,omitempty"` + + // Optional: Whether the task contains global configuration + Global *GlobalRunTaskOptions `jsonapi:"attr,global-configuration,omitempty"` } // Create is used to create a new run task for an organization @@ -157,13 +178,13 @@ func (s *runTasks) Create(ctx context.Context, organization string, options RunT return nil, err } - r := &RunTask{} + r := &internalRunTask{} err = req.Do(ctx, r) if err != nil { return nil, err } - return r, nil + return r.ToRunTask(), nil } // List all the run tasks for an organization @@ -181,13 +202,13 @@ func (s *runTasks) List(ctx context.Context, organization string, options *RunTa return nil, err } - rl := &RunTaskList{} + rl := &internalRunTaskList{} err = req.Do(ctx, rl) if err != nil { return nil, err } - return rl, nil + return rl.ToRunTaskList(), nil } // Read is used to read an organization's run task by ID @@ -210,13 +231,13 @@ func (s *runTasks) ReadWithOptions(ctx context.Context, runTaskID string, option return nil, err } - r := &RunTask{} + r := &internalRunTask{} err = req.Do(ctx, r) if err != nil { return nil, err } - return r, nil + return r.ToRunTask(), nil } // Update an existing run task for an organization by ID @@ -235,13 +256,13 @@ func (s *runTasks) Update(ctx context.Context, runTaskID string, options RunTask return nil, err } - r := &RunTask{} + r := &internalRunTask{} err = req.Do(ctx, r) if err != nil { return nil, err } - return r, nil + return r.ToRunTask(), nil } // Delete an existing run task for an organization by ID diff --git a/run_task_integration_test.go b/run_task_integration_test.go index f08b08c1f..ba1811cfb 100644 --- a/run_task_integration_test.go +++ b/run_task_integration_test.go @@ -5,6 +5,7 @@ package tfe import ( "context" + "errors" "os" "testing" @@ -12,6 +13,17 @@ import ( "github.com/stretchr/testify/require" ) +func hasGlobalRunTasks(client *Client, organizationName string) (bool, error) { + ctx := context.Background() + if orgEntitlements, err := client.Organizations.ReadEntitlements(ctx, organizationName); err != nil { + return false, err + } else if orgEntitlements == nil { + return false, errors.New("The organization entitlements are empty.") + } else { + return orgEntitlements.GlobalRunTasks, nil + } +} + func TestRunTasksCreate(t *testing.T) { client := testClient(t) ctx := context.Background() @@ -19,7 +31,14 @@ func TestRunTasksCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() - upgradeOrganizationSubscription(t, client, orgTest) + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + + if v, err := hasGlobalRunTasks(client, orgTest.Name); err != nil { + t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err) + } else if !v { + t.Fatal("The test organization requires the global-run-tasks entitlement but is not entitled.") + return + } runTaskServerURL := os.Getenv("TFC_RUN_TASK_URL") if runTaskServerURL == "" { @@ -28,6 +47,12 @@ func TestRunTasksCreate(t *testing.T) { runTaskName := "tst-runtask-" + randomString(t) runTaskDescription := "A Run Task Description" + globalEnabled := true + globalStages := []Stage{ + PostPlan, + PrePlan, + } + globalEnforce := Mandatory t.Run("add run task to organization", func(t *testing.T) { r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{ @@ -36,6 +61,11 @@ func TestRunTasksCreate(t *testing.T) { Description: &runTaskDescription, Category: "task", Enabled: Bool(true), + Global: &GlobalRunTaskOptions{ + Enabled: &globalEnabled, + Stages: &globalStages, + EnforcementLevel: &globalEnforce, + }, }) require.NoError(t, err) @@ -44,6 +74,10 @@ func TestRunTasksCreate(t *testing.T) { assert.Equal(t, r.URL, runTaskServerURL) assert.Equal(t, r.Category, "task") assert.Equal(t, r.Description, runTaskDescription) + assert.NotNil(t, r.Global) + assert.Equal(t, globalEnabled, r.Global.Enabled) + assert.Equal(t, globalEnforce, r.Global.EnforcementLevel) + assert.Equal(t, globalStages, r.Global.Stages) t.Run("ensure org is deserialized properly", func(t *testing.T) { assert.Equal(t, r.Organization.Name, orgTest.Name) @@ -51,6 +85,56 @@ func TestRunTasksCreate(t *testing.T) { }) } +func TestRunTasksCreateWithoutGlobalEntitlement(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + newSubscriptionUpdater(orgTest).WithTrialPlan().Update(t) + + if v, err := hasGlobalRunTasks(client, orgTest.Name); err != nil { + t.Fatalf("Could not retrieve the entitlements for the test organization.: %s", err) + } else if v { + t.Fatal("The test organization should not have the global-run-tasks entitlement but it does.") + return + } + + runTaskServerURL := os.Getenv("TFC_RUN_TASK_URL") + if runTaskServerURL == "" { + t.Error("Cannot create a run task with an empty URL. You must set TFC_RUN_TASK_URL for run task related tests.") + } + + runTaskName := "tst-runtask-" + randomString(t) + runTaskDescription := "A Run Task Description" + globalStages := []Stage{ + PostPlan, + PrePlan, + } + globalEnforce := Mandatory + + t.Run("add run task to organization", func(t *testing.T) { + r, err := client.RunTasks.Create(ctx, orgTest.Name, RunTaskCreateOptions{ + Name: runTaskName, + URL: runTaskServerURL, + Description: &runTaskDescription, + Category: "task", + // Even though we pass in these global parameters, + // they should be ignored and not throw an API error + Global: &GlobalRunTaskOptions{ + Enabled: Bool(true), + Stages: &globalStages, + EnforcementLevel: &globalEnforce, + }, + }) + require.NoError(t, err) + + assert.NotEmpty(t, r.ID) + assert.Nil(t, r.Global) + }) +} + func TestRunTasksList(t *testing.T) { client := testClient(t) ctx := context.Background() diff --git a/subscription_updater_test.go b/subscription_updater_test.go new file mode 100644 index 000000000..6a60c48f1 --- /dev/null +++ b/subscription_updater_test.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfe + +import ( + "context" + "fmt" + "net/url" + "testing" + "time" +) + +type featureSet struct { + ID string `jsonapi:"primary,feature-sets"` +} + +type featureSetList struct { + Items []*featureSet + *Pagination +} + +type featureSetListOptions struct { + Q string `url:"q,omitempty"` +} + +type retryableFn func() (interface{}, error) + +type updateFeatureSetOptions struct { + Type string `jsonapi:"primary,subscription"` + RunsCeiling *int `jsonapi:"attr,runs-ceiling,omitempty"` + ContractStartAt *time.Time `jsonapi:"attr,contract-start-at,iso8601,omitempty"` + ContractUserLimit *int `jsonapi:"attr,contract-user-limit,omitempty"` + ContractApplyLimit *int `jsonapi:"attr,contract-apply-limit,omitempty"` + + FeatureSet *featureSet `jsonapi:"relation,feature-set"` +} + +type organizationSubscriptionUpdater struct { + organization *Organization + planName string + updateOpts updateFeatureSetOptions +} + +func newSubscriptionUpdater(organization *Organization) *organizationSubscriptionUpdater { + return &organizationSubscriptionUpdater{ + organization: organization, + updateOpts: updateFeatureSetOptions{}, + } +} + +func (b *organizationSubscriptionUpdater) WithBusinessPlan() *organizationSubscriptionUpdater { + b.planName = "Business" + + ceiling := 10 + start := time.Now() + userLimit := 1000 + applyLimit := 5000 + + b.updateOpts.RunsCeiling = &ceiling + b.updateOpts.ContractStartAt = &start + b.updateOpts.ContractUserLimit = &userLimit + b.updateOpts.ContractApplyLimit = &applyLimit + return b +} + +func (b *organizationSubscriptionUpdater) WithTrialPlan() *organizationSubscriptionUpdater { + b.planName = "Trial" + ceiling := 1 + b.updateOpts.RunsCeiling = &ceiling + return b +} + +// Attempts to change an organization's subscription to a different plan. Requires a user token with admin access. +func (b *organizationSubscriptionUpdater) Update(t *testing.T) { + if enterpriseEnabled() { + t.Skip("Cannot upgrade an organization's subscription when enterprise is enabled. Set ENABLE_TFE=0 to run.") + } + + if b.planName == "" { + t.Fatal("organizationSubscriptionUpdater requires a plan") + return + } + + adminClient := testAdminClient(t, provisionLicensesAdmin) + req, err := adminClient.NewRequest("GET", "admin/feature-sets", featureSetListOptions{ + Q: b.planName, + }) + if err != nil { + t.Fatal(err) + return + } + + fsl := &featureSetList{} + err = req.Do(context.Background(), fsl) + if err != nil { + t.Fatalf("failed to enumerate feature sets: %v", err) + return + } else if len(fsl.Items) == 0 { + t.Fatalf("feature set response was empty") + return + } + + b.updateOpts.FeatureSet = fsl.Items[0] + + u := fmt.Sprintf("admin/organizations/%s/subscription", url.QueryEscape(b.organization.Name)) + req, err = adminClient.NewRequest("POST", u, &b.updateOpts) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + return + } + + err = req.Do(context.Background(), nil) + if err != nil { + t.Fatalf("Failed to upgrade subscription: %v", err) + } +} diff --git a/workspace_run_task.go b/workspace_run_task.go index 450305ea0..f8718eac8 100644 --- a/workspace_run_task.go +++ b/workspace_run_task.go @@ -39,7 +39,9 @@ type workspaceRunTasks struct { type WorkspaceRunTask struct { ID string `jsonapi:"primary,workspace-tasks"` EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` - Stage Stage `jsonapi:"attr,stage"` + // Deprecated: Use Stages property instead. + Stage Stage `jsonapi:"attr,stage"` + Stages []Stage `jsonapi:"attr,stages"` RunTask *RunTask `jsonapi:"relation,task"` Workspace *Workspace `jsonapi:"relation,workspace"` @@ -63,15 +65,20 @@ type WorkspaceRunTaskCreateOptions struct { EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level"` // Required: The run task to attach to the workspace RunTask *RunTask `jsonapi:"relation,task"` - // Optional: The stage to run the task in + // Deprecated: Use Stages property instead. Stage *Stage `jsonapi:"attr,stage,omitempty"` + // Optional: The stage to run the task in + Stages *[]Stage `jsonapi:"attr,stages,omitempty"` } // WorkspaceRunTaskUpdateOptions represent the set of options for updating a workspace run task. type WorkspaceRunTaskUpdateOptions struct { Type string `jsonapi:"primary,workspace-tasks"` EnforcementLevel TaskEnforcementLevel `jsonapi:"attr,enforcement-level,omitempty"` - Stage *Stage `jsonapi:"attr,stage,omitempty"` // The stage to run the task in + // Deprecated: Use Stages property instead. + Stage *Stage `jsonapi:"attr,stage,omitempty"` + // Optional: The stage to run the task in + Stages *[]Stage `jsonapi:"attr,stages,omitempty"` } // List all run tasks attached to a workspace @@ -86,13 +93,13 @@ func (s *workspaceRunTasks) List(ctx context.Context, workspaceID string, option return nil, err } - rl := &WorkspaceRunTaskList{} + rl := &internalWorkspaceRunTaskList{} err = req.Do(ctx, rl) if err != nil { return nil, err } - return rl, nil + return rl.ToWorkspaceRunTaskList(), nil } // Read a workspace run task by ID @@ -115,13 +122,13 @@ func (s *workspaceRunTasks) Read(ctx context.Context, workspaceID, workspaceTask return nil, err } - wr := &WorkspaceRunTask{} + wr := &internalWorkspaceRunTask{} err = req.Do(ctx, wr) if err != nil { return nil, err } - return wr, nil + return wr.ToWorkspaceRunTask(), nil } // Create is used to attach a run task to a workspace, or in other words: create a workspace run task. The run task must exist in the workspace's organization. @@ -140,13 +147,13 @@ func (s *workspaceRunTasks) Create(ctx context.Context, workspaceID string, opti return nil, err } - wr := &WorkspaceRunTask{} + wr := &internalWorkspaceRunTask{} err = req.Do(ctx, wr) if err != nil { return nil, err } - return wr, nil + return wr.ToWorkspaceRunTask(), nil } // Update an existing workspace run task by ID @@ -169,13 +176,13 @@ func (s *workspaceRunTasks) Update(ctx context.Context, workspaceID, workspaceTa return nil, err } - wr := &WorkspaceRunTask{} + wr := &internalWorkspaceRunTask{} err = req.Do(ctx, wr) if err != nil { return nil, err } - return wr, nil + return wr.ToWorkspaceRunTask(), nil } // Delete a workspace run task by ID diff --git a/workspace_run_task_integration_test.go b/workspace_run_task_integration_test.go index 971474f1c..87143abd5 100644 --- a/workspace_run_task_integration_test.go +++ b/workspace_run_task_integration_test.go @@ -27,8 +27,10 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { defer wkspaceTestCleanup() t.Run("attach run task to workspace", func(t *testing.T) { + s := []Stage{PrePlan, PostPlan} wr, err := client.WorkspaceRunTasks.Create(ctx, wkspaceTest.ID, WorkspaceRunTaskCreateOptions{ EnforcementLevel: Mandatory, + Stages: &s, RunTask: runTaskTest, }) @@ -39,17 +41,19 @@ func TestWorkspaceRunTasksCreate(t *testing.T) { }() assert.NotEmpty(t, wr.ID) - assert.Equal(t, wr.EnforcementLevel, Mandatory) + assert.Equal(t, Mandatory, wr.EnforcementLevel) + assert.Equal(t, s[0], wr.Stage) + assert.Equal(t, s, wr.Stages) t.Run("ensure run task is deserialized properly", func(t *testing.T) { + assert.NotNil(t, wr.RunTask) assert.NotEmpty(t, wr.RunTask.ID) }) }) } -func TestWorkspaceRunTasksCreateBeta(t *testing.T) { - // Once Pre-Plan Tasks are generally available, this can replace the above TestWorkspaceRunTasksCreate - skipUnlessBeta(t) +func TestWorkspaceRunTasksCreateDeprecated(t *testing.T) { + // This test uses the deprecate `stage` attribute client := testClient(t) ctx := context.Background() @@ -176,22 +180,24 @@ func TestWorkspaceRunTasksUpdate(t *testing.T) { wrTaskTest, wrTaskTestCleanup := createWorkspaceRunTask(t, client, wkspaceTest, runTaskTest) defer wrTaskTestCleanup() - t.Run("rename task", func(t *testing.T) { + t.Run("update task", func(t *testing.T) { + stages := []Stage{PrePlan, PostPlan} wr, err := client.WorkspaceRunTasks.Update(ctx, wkspaceTest.ID, wrTaskTest.ID, WorkspaceRunTaskUpdateOptions{ EnforcementLevel: Mandatory, + Stages: &stages, }) require.NoError(t, err) wr, err = client.WorkspaceRunTasks.Read(ctx, wkspaceTest.ID, wr.ID) require.NoError(t, err) - assert.Equal(t, wr.EnforcementLevel, Mandatory) + assert.Equal(t, Mandatory, wr.EnforcementLevel) + assert.Equal(t, stages, wr.Stages) + assert.Equal(t, PrePlan, wr.Stage) }) } -func TestWorkspaceRunTasksUpdateBeta(t *testing.T) { - // Once Pre-Plan Tasks are generally available, this can replace the above TestWorkspaceRunTasksUpdate - skipUnlessBeta(t) +func TestWorkspaceRunTasksUpdateDeprecated(t *testing.T) { client := testClient(t) ctx := context.Background()