diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d112c9d6..0c5d6f95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +## Enhancements +* Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011) + # v1.71.0 ## Enhancements diff --git a/examples/projects/main.go b/examples/projects/main.go new file mode 100644 index 000000000..b952f1bb5 --- /dev/null +++ b/examples/projects/main.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package main + +import ( + "context" + "log" + + tfe "github.com/hashicorp/go-tfe" + + "github.com/hashicorp/jsonapi" +) + +func main() { + config := &tfe.Config{ + Token: "insert-your-token-here", + RetryServerErrors: true, + } + + client, err := tfe.NewClient(config) + if err != nil { + log.Fatal(err) + } + + // Create a context + ctx := context.Background() + + // Create a new project + p, err := client.Projects.Create(ctx, "org-test", tfe.ProjectCreateOptions{ + Name: "my-app-tst", + }) + if err != nil { + log.Fatal(err) + } + + // Update the project auto destroy activity duration + p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"), + }) + if err != nil { + log.Fatal(err) + } + + // Disable auto destroy + p, err = client.Projects.Update(ctx, p.ID, tfe.ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullNullableAttr[string](), + }) + if err != nil { + log.Fatal(err) + } + + err = client.Projects.Delete(ctx, p.ID) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/workspaces/main.go b/examples/workspaces/main.go index 5d460acac..b324e1dba 100644 --- a/examples/workspaces/main.go +++ b/examples/workspaces/main.go @@ -27,8 +27,9 @@ func main() { // Create a new workspace w, err := client.Workspaces.Create(ctx, "org-name", tfe.WorkspaceCreateOptions{ - Name: tfe.String("my-app-tst"), - AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + Name: tfe.String("my-app-tst"), + AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + InheritsProjectAutoDestroy: tfe.Bool(false), }) if err != nil { log.Fatal(err) @@ -36,10 +37,11 @@ func main() { // Update the workspace w, err = client.Workspaces.Update(ctx, "org-name", w.Name, tfe.WorkspaceUpdateOptions{ - AutoApply: tfe.Bool(false), - TerraformVersion: tfe.String("0.11.1"), - WorkingDirectory: tfe.String("my-app/infra"), - AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + AutoApply: tfe.Bool(false), + TerraformVersion: tfe.String("0.11.1"), + WorkingDirectory: tfe.String("my-app/infra"), + AutoDestroyAt: tfe.NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + InheritsProjectAutoDestroy: tfe.Bool(false), }) if err != nil { log.Fatal(err) diff --git a/project.go b/project.go index 89437a328..faf1da8c5 100644 --- a/project.go +++ b/project.go @@ -7,6 +7,8 @@ import ( "context" "fmt" "net/url" + + "github.com/hashicorp/jsonapi" ) // Compile-time proof of interface implementation. @@ -63,6 +65,8 @@ type Project struct { Description string `jsonapi:"attr,description"` + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Relations Organization *Organization `jsonapi:"relation,organization"` } @@ -100,6 +104,11 @@ type ProjectCreateOptions struct { // Associated TagBindings of the project. TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"` + + // Optional: For all workspaces in the project, the period of time to wait + // after workspace activity to trigger a destroy run. The format should roughly + // match a Go duration string limited to days and hours, e.g. "24h" or "1d". + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` } // ProjectUpdateOptions represents the options for updating a project @@ -119,6 +128,11 @@ type ProjectUpdateOptions struct { // Associated TagBindings of the project. Note that this will replace // all existing tag bindings. TagBindings []*TagBinding `jsonapi:"relation,tag-bindings,omitempty"` + + // Optional: For all workspaces in the project, the period of time to wait + // after workspace activity to trigger a destroy run. The format should roughly + // match a Go duration string limited to days and hours, e.g. "24h" or "1d". + AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` } // ProjectAddTagBindingsOptions represents the options for adding tag bindings diff --git a/projects_integration_test.go b/projects_integration_test.go index 962719282..b643aad9e 100644 --- a/projects_integration_test.go +++ b/projects_integration_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/hashicorp/jsonapi" ) func TestProjectsList(t *testing.T) { @@ -150,6 +152,8 @@ func TestProjectsCreate(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) defer orgTestCleanup() + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + t.Run("with valid options", func(t *testing.T) { options := ProjectCreateOptions{ Name: "foo", @@ -193,6 +197,17 @@ func TestProjectsCreate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidOrg.Error()) }) + + t.Run("when options has an invalid auto destroy activity duration", func(t *testing.T) { + skipUnlessBeta(t) + + w, err := client.Projects.Create(ctx, orgTest.Name, ProjectCreateOptions{ + Name: "foo", + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("20m"), + }) + assert.Nil(t, w) + assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')") + }) } func TestProjectsUpdate(t *testing.T) { @@ -284,6 +299,21 @@ func TestProjectsUpdate(t *testing.T) { assert.Nil(t, w) assert.EqualError(t, err, ErrInvalidProjectID.Error()) }) + + t.Run("without a valid projects auto destroy activity duration", func(t *testing.T) { + skipUnlessBeta(t) + + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + + kBefore, kTestCleanup := createProject(t, client, orgTest) + defer kTestCleanup() + + w, err := client.Projects.Update(ctx, kBefore.ID, ProjectUpdateOptions{ + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("bar"), + }) + assert.Nil(t, w) + assert.Contains(t, err.Error(), "invalid attribute\n\nAuto destroy activity duration has an incorrect format, we expect up to 4 numeric digits and 1 unit ('d' or 'h')") + }) } func TestProjectsAddTagBindings(t *testing.T) { @@ -378,3 +408,32 @@ func TestProjectsDelete(t *testing.T) { assert.EqualError(t, err, ErrInvalidProjectID.Error()) }) } + +func TestProjectsAutoDestroy(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) + + t.Run("when creating workspace in project with autodestroy", func(t *testing.T) { + options := ProjectCreateOptions{ + Name: "foo", + Description: String("qux"), + AutoDestroyActivityDuration: jsonapi.NewNullableAttrWithValue("3d"), + } + + p, err := client.Projects.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + + w, _ := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + Project: p, + }) + + assert.Equal(t, p.AutoDestroyActivityDuration, w.AutoDestroyActivityDuration) + }) +} diff --git a/workspace.go b/workspace.go index 1a3990dbb..40ee48d71 100644 --- a/workspace.go +++ b/workspace.go @@ -186,6 +186,7 @@ type Workspace struct { ExecutionMode string `jsonapi:"attr,execution-mode"` FileTriggersEnabled bool `jsonapi:"attr,file-triggers-enabled"` GlobalRemoteState bool `jsonapi:"attr,global-remote-state"` + InheritsProjectAutoDestroy bool `jsonapi:"attr,inherits-project-auto-destroy"` Locked bool `jsonapi:"attr,locked"` MigrationEnvironment string `jsonapi:"attr,migration-environment"` Name string `jsonapi:"attr,name"` @@ -393,6 +394,9 @@ type WorkspaceCreateOptions struct { // should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d". AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Optional: Whether the workspace inherits auto destroy settings from the project + InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"` + // Optional: A description for the workspace. Description *string `jsonapi:"attr,description,omitempty"` @@ -550,6 +554,9 @@ type WorkspaceUpdateOptions struct { // should roughly match a Go duration string limited to days and hours, e.g. "24h" or "1d". AutoDestroyActivityDuration jsonapi.NullableAttr[string] `jsonapi:"attr,auto-destroy-activity-duration,omitempty"` + // Optional: Whether the workspace inherits auto destroy settings from the project + InheritsProjectAutoDestroy *bool `jsonapi:"attr,inherits-project-auto-destroy,omitempty"` + // Optional: A new name for the workspace, which can only include letters, numbers, -, // and _. This will be used as an identifier and must be unique in the // organization. Warning: Changing a workspace's name changes its URL in the diff --git a/workspace_integration_test.go b/workspace_integration_test.go index 1505716a6..28e34c61c 100644 --- a/workspace_integration_test.go +++ b/workspace_integration_test.go @@ -2961,7 +2961,7 @@ func TestWorkspacesAutoDestroy(t *testing.T) { orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - upgradeOrganizationSubscription(t, client, orgTest) + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) autoDestroyAt := NullableTime(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)) wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ @@ -2999,31 +2999,39 @@ func TestWorkspacesAutoDestroy(t *testing.T) { } func TestWorkspacesAutoDestroyDuration(t *testing.T) { + skipUnlessBeta(t) + client := testClient(t) ctx := context.Background() orgTest, orgTestCleanup := createOrganization(t, client) t.Cleanup(orgTestCleanup) - upgradeOrganizationSubscription(t, client, orgTest) + newSubscriptionUpdater(orgTest).WithBusinessPlan().Update(t) - duration := jsonapi.NewNullableAttrWithValue("14d") - nilDuration := jsonapi.NewNullNullableAttr[string]() - nilAutoDestroy := jsonapi.NewNullNullableAttr[time.Time]() - wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ - Name: String(randomString(t)), - AutoDestroyActivityDuration: duration, - }) - t.Cleanup(wCleanup) + t.Run("when creating a new workspace with standalone auto destroy settings", func(t *testing.T) { + duration := jsonapi.NewNullableAttrWithValue("14d") + nilDuration := jsonapi.NewNullNullableAttr[string]() + nilAutoDestroy := jsonapi.NewNullNullableAttr[time.Time]() + wTest, wCleanup := createWorkspaceWithOptions(t, client, orgTest, WorkspaceCreateOptions{ + Name: String(randomString(t)), + AutoDestroyActivityDuration: duration, + InheritsProjectAutoDestroy: Bool(false), + }) + t.Cleanup(wCleanup) - require.Equal(t, duration, wTest.AutoDestroyActivityDuration) - require.NotEqual(t, nilAutoDestroy, wTest.AutoDestroyAt) + require.Equal(t, duration, wTest.AutoDestroyActivityDuration) + require.NotEqual(t, nilAutoDestroy, wTest.AutoDestroyAt) + require.Equal(t, wTest.InheritsProjectAutoDestroy, false) - w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{ - AutoDestroyActivityDuration: nilDuration, - }) + w, err := client.Workspaces.Update(ctx, orgTest.Name, wTest.Name, WorkspaceUpdateOptions{ + AutoDestroyActivityDuration: nilDuration, + InheritsProjectAutoDestroy: Bool(false), + }) - require.NoError(t, err) - require.False(t, w.AutoDestroyActivityDuration.IsSpecified()) - require.False(t, w.AutoDestroyAt.IsSpecified()) + require.NoError(t, err) + require.False(t, w.AutoDestroyActivityDuration.IsSpecified()) + require.False(t, w.AutoDestroyAt.IsSpecified()) + require.Equal(t, wTest.InheritsProjectAutoDestroy, false) + }) }