Skip to content

Commit

Permalink
Add project level auto destroy setting (#1011)
Browse files Browse the repository at this point in the history
* Add project level settings for auto destroy setting

* Add missing example change

* fix fmt

* Update changelog

* Fix typo

* Run go fmt

* Add project moving workspace scoped tests

* Run go fmt again

* Fix potential subscription error

* Add workspace level indicator of project inheritance

* Add new subscription updater

* Remove some moving tests that have dependent logic

* Update API for go-tfe

* Add fmt changes

* update the main example

* Update all to use business plan

* Add skip unless beta tags to certain tests

* Run fmt

* Update CHANGELOG.md

Co-authored-by: Sebastian Rivera <[email protected]>

---------

Co-authored-by: Sebastian Rivera <[email protected]>
  • Loading branch information
simonxmh and sebasslash authored Dec 19, 2024
1 parent f9d7888 commit 98975f4
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 24 deletions.
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)

# 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)
})
}

0 comments on commit 98975f4

Please sign in to comment.