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 project level auto destroy setting #1011

Merged
merged 20 commits into from
Dec 19, 2024
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

## Enhancements
* Add support for project level auto destroy settings @simonxmh [#1011](https://github.com/hashicorp/go-tfe/pull/1011)
simonxmh marked this conversation as resolved.
Show resolved Hide resolved

# v1.71.0

## Enhancements
Expand Down
57 changes: 57 additions & 0 deletions examples/projects/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
14 changes: 8 additions & 6 deletions examples/workspaces/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ 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)
}

// 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)
Expand Down
14 changes: 14 additions & 0 deletions project.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"context"
"fmt"
"net/url"

"github.com/hashicorp/jsonapi"
)

// Compile-time proof of interface implementation.
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions projects_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

"github.com/hashicorp/jsonapi"
)

func TestProjectsList(t *testing.T) {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
})
}
7 changes: 7 additions & 0 deletions workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`

Expand Down Expand Up @@ -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
Expand Down
44 changes: 26 additions & 18 deletions workspace_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
})
}
Loading