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 #1550

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c47bc97
Add existing changes for just modifying the attributes
simonxmh Dec 12, 2024
6bfea88
Remove testing residues
simonxmh Dec 12, 2024
bf80b24
Add conditional switch logic for nonbreaking changing
simonxmh Dec 12, 2024
cef9d18
Remove automated boolean setting
simonxmh Dec 16, 2024
01a4069
Add all test changes
simonxmh Dec 19, 2024
468036a
Add changelog changes and datasource
simonxmh Dec 19, 2024
37bacc3
Merge branch 'main' into simonxmh/add_project_level_auto_destroy
simonxmh Dec 19, 2024
822df2e
Add project data source changes
simonxmh Dec 19, 2024
2152c80
Add workspace data source tests
simonxmh Dec 19, 2024
1b115d0
Merge branch 'main' into simonxmh/add_project_level_auto_destroy
simonxmh Dec 20, 2024
63b9299
Add go mod replace for now
simonxmh Dec 20, 2024
cfd4c95
Revert test for auto destroy
simonxmh Dec 20, 2024
bbbc2cd
Change go mod, make automated toggle
simonxmh Jan 6, 2025
c05b61a
Add missing interface method
simonxmh Jan 6, 2025
2d2c113
Reduce nesting and add automated logic
simonxmh Jan 7, 2025
c57f046
Use the config level setting
simonxmh Jan 7, 2025
c2beedc
Change behavior of inherited field
simonxmh Jan 8, 2025
54858bc
Fix the data source test
simonxmh Jan 9, 2025
f6701d0
Merge branch 'main' into simonxmh/add_project_level_auto_destroy
simonxmh Jan 9, 2025
3c5ba7c
Remove duplicate tag binding stub
simonxmh Jan 9, 2025
541a519
Disable beta tests
simonxmh Jan 9, 2025
7e8107f
Merge branch 'main' into simonxmh/add_project_level_auto_destroy
simonxmh Jan 10, 2025
bc421e4
Fix creation flow
simonxmh Jan 10, 2025
0a185f2
Add workspace updates
simonxmh Jan 17, 2025
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
## Unreleased

BREAKING_CHANGES:
* `r/tfe_workspace`: Add attribute `inherits_project_auto_destroy` to tfe_workspace, existing workspaces with auto-destroy settings should add `inherits_project_auto_destroy: false` to tfe_workspace resource [1550](https://github.com/hashicorp/terraform-provider-tfe/pull/1550)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should do fill this out for the user the same way we do on the frontend. Basically if they have one defined in config then we should automatically set this to false


FEATURES:
* `r/tfe_project`: Add `auto_destroy_activity_duration` field to the project resource, which automatically propagates auto-destroy settings to workspaces [1550](https://github.com/hashicorp/terraform-provider-tfe/pull/1550)
* `d/tfe_project`: Add `auto_destroy_activity_duration` field to the project datasource [1550](https://github.com/hashicorp/terraform-provider-tfe/pull/1550)
* `d/tfe_workspace`: Add attribute `inherits_project_auto_destroy` to tfe_workspace datasource [1550](https://github.com/hashicorp/terraform-provider-tfe/pull/1550)

## v0.62.0

FEATURES:
Expand Down
74 changes: 45 additions & 29 deletions internal/provider/data_source_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ func dataSourceTFEProject() *schema.Resource {
Optional: true,
},

"auto_destroy_activity_duration": {
Type: schema.TypeString,
Computed: true,
},

"workspace_ids": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -77,39 +82,50 @@ func dataSourceTFEProjectRead(ctx context.Context, d *schema.ResourceData, meta

for _, proj := range l.Items {
// Case-insensitive uniqueness is enforced in TFC
if strings.EqualFold(proj.Name, projName) {
// Only now include workspaces to cut down on request load.
readOptions := &tfe.WorkspaceListOptions{
ProjectID: proj.ID,
if !strings.EqualFold(proj.Name, projName) {
continue
}
// Only now include workspaces to cut down on request load.
readOptions := &tfe.WorkspaceListOptions{
ProjectID: proj.ID,
}
var workspaces []interface{}
var workspaceNames []interface{}
for {
wl, err := config.Client.Workspaces.List(ctx, orgName, readOptions)
if err != nil {
return diag.Errorf("Error retrieving workspaces: %v", err)
}

for _, workspace := range wl.Items {
workspaces = append(workspaces, workspace.ID)
workspaceNames = append(workspaceNames, workspace.Name)
}
var workspaces []interface{}
var workspaceNames []interface{}
for {
wl, err := config.Client.Workspaces.List(ctx, orgName, readOptions)
if err != nil {
return diag.Errorf("Error retrieving workspaces: %v", err)
}

for _, workspace := range wl.Items {
workspaces = append(workspaces, workspace.ID)
workspaceNames = append(workspaceNames, workspace.Name)
}

// Exit the loop when we've seen all pages.
if wl.CurrentPage >= wl.TotalPages {
break
}

// Update the page number to get the next page.
readOptions.PageNumber = wl.NextPage

// Exit the loop when we've seen all pages.
if wl.CurrentPage >= wl.TotalPages {
break
}

d.Set("workspace_ids", workspaces)
d.Set("workspace_names", workspaceNames)
d.Set("description", proj.Description)
d.SetId(proj.ID)
return nil
// Update the page number to get the next page.
readOptions.PageNumber = wl.NextPage
}

d.Set("workspace_ids", workspaces)
d.Set("workspace_names", workspaceNames)
d.Set("description", proj.Description)

var autoDestroyDuration string
if proj.AutoDestroyActivityDuration.IsSpecified() {
autoDestroyDuration, err = proj.AutoDestroyActivityDuration.Get()
if err != nil {
return diag.Errorf("Error reading auto destroy activity duration: %v", err)
}
}
d.Set("auto_destroy_activity_duration", autoDestroyDuration)
d.SetId(proj.ID)

return nil
}
return diag.Errorf("could not find project %s/%s", orgName, projName)
}
43 changes: 43 additions & 0 deletions internal/provider/data_source_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ func TestAccTFEProjectDataSource_caseInsensitive(t *testing.T) {
})
}

func TestAccTFEProjectDataSource_basicWithAutoDestroy(t *testing.T) {
skipUnlessBeta(t)
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

tfeClient, err := getClientUsingEnv()
if err != nil {
t.Fatal(err)
}
org, orgCleanup := createBusinessOrganization(t, tfeClient)
t.Cleanup(orgCleanup)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEProjectDataSourceConfigWithAutoDestroy(rInt, org.Name, "3d"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(
"data.tfe_project.foobar", "name", fmt.Sprintf("project-test-%d", rInt)),
resource.TestCheckResourceAttr(
"data.tfe_project.foobar", "auto_destroy_activity_duration", "3d"),
),
},
},
})
}

func testAccTFEProjectDataSourceConfig(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
Expand Down Expand Up @@ -112,3 +140,18 @@ data "tfe_project" "foobar" {
]
}`, rInt, rInt, rInt)
}

func testAccTFEProjectDataSourceConfigWithAutoDestroy(rInt int, orgName string, duration string) string {
return fmt.Sprintf(`
resource "tfe_project" "foobar" {
name = "project-test-%d"
description = "project description"
organization = "%s"
auto_destroy_activity_duration = "%s"
}

data "tfe_project" "foobar" {
name = tfe_project.foobar.name
organization = tfe_project.foobar.organization
}`, rInt, orgName, duration)
}
6 changes: 6 additions & 0 deletions internal/provider/data_source_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ func dataSourceTFEWorkspace() *schema.Resource {
Computed: true,
},

"inherits_project_auto_destroy": {
Type: schema.TypeBool,
Computed: true,
},

"file_triggers_enabled": {
Type: schema.TypeBool,
Computed: true,
Expand Down Expand Up @@ -249,6 +254,7 @@ func dataSourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error
d.Set("file_triggers_enabled", workspace.FileTriggersEnabled)
d.Set("operations", workspace.Operations)
d.Set("policy_check_failures", workspace.PolicyCheckFailures)
d.Set("inherits_project_auto_destroy", workspace.InheritsProjectAutoDestroy)

autoDestroyAt, err := flattenAutoDestroyAt(workspace.AutoDestroyAt)
if err != nil {
Expand Down
12 changes: 10 additions & 2 deletions internal/provider/data_source_workspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ func TestAccTFEWorkspaceDataSource_readAutoDestroyAt(t *testing.T) {
},
{
Config: testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroy(rInt),
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"),
resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "inherits_project_auto_destroy", "false"),
),
},
},
})
Expand All @@ -203,7 +206,10 @@ func TestAccTFEWorkspaceDataSource_readAutoDestroyDuration(t *testing.T) {
},
{
Config: testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroyDuration(rInt),
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "inherits_project_auto_destroy", "false"),
),
},
},
})
Expand Down Expand Up @@ -312,6 +318,7 @@ resource "tfe_workspace" "foobar" {
organization = tfe_organization.foobar.id
description = "provider-testing"
auto_destroy_at = "2100-01-01T00:00:00Z"
inherits_project_auto_destroy = false
}
data "tfe_workspace" "foobar" {
Expand All @@ -332,6 +339,7 @@ resource "tfe_workspace" "foobar" {
organization = tfe_organization.foobar.id
description = "provider-testing"
auto_destroy_activity_duration = "1d"
inherits_project_auto_destroy = false
}
data "tfe_workspace" "foobar" {
Expand Down
29 changes: 29 additions & 0 deletions internal/provider/resource_tfe_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"regexp"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/jsonapi"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand Down Expand Up @@ -55,6 +56,12 @@ func resourceTFEProject() *schema.Resource {
Computed: true,
ForceNew: true,
},

"auto_destroy_activity_duration": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^\d{1,4}[dh]$`), "must be 1-4 digits followed by d or h"),
},
},
}
}
Expand All @@ -73,6 +80,10 @@ func resourceTFEProjectCreate(ctx context.Context, d *schema.ResourceData, meta
Description: tfe.String(d.Get("description").(string)),
}

if v, ok := d.GetOk("auto_destroy_activity_duration"); ok {
options.AutoDestroyActivityDuration = jsonapi.NewNullableAttrWithValue(v.(string))
}

log.Printf("[DEBUG] Create new project: %s", name)
project, err := config.Client.Projects.Create(ctx, organization, options)
if err != nil {
Expand Down Expand Up @@ -102,6 +113,15 @@ func resourceTFEProjectRead(ctx context.Context, d *schema.ResourceData, meta in
d.Set("description", project.Description)
d.Set("organization", project.Organization.Name)

if project.AutoDestroyActivityDuration.IsSpecified() {
v, err := project.AutoDestroyActivityDuration.Get()
if err != nil {
return diag.Errorf("Error reading auto destroy activity duration: %v", err)
}

d.Set("auto_destroy_activity_duration", v)
}

return nil
}

Expand All @@ -113,6 +133,15 @@ func resourceTFEProjectUpdate(ctx context.Context, d *schema.ResourceData, meta
Description: tfe.String(d.Get("description").(string)),
}

if d.HasChange("auto_destroy_activity_duration") {
duration, ok := d.GetOk("auto_destroy_activity_duration")
if !ok {
options.AutoDestroyActivityDuration = jsonapi.NewNullNullableAttr[string]()
} else {
options.AutoDestroyActivityDuration = jsonapi.NewNullableAttrWithValue(duration.(string))
}
}

log.Printf("[DEBUG] Update configuration of project: %s", d.Id())
project, err := config.Client.Projects.Update(ctx, d.Id(), options)
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions internal/provider/resource_tfe_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ func TestAccTFEProject_import(t *testing.T) {
})
}

func TestAccTFEProject_withAutoDestroy(t *testing.T) {
project := &tfe.Project{}
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckTFEProjectDestroy,
Steps: []resource.TestStep{
{
Config: testAccTFEProject_basicWithAutoDestroy(rInt, "3d"),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEProjectExists(
"tfe_project.foobar", project),
testAccCheckTFEProjectAttributes(project),
resource.TestCheckResourceAttr(
"tfe_project.foobar", "auto_destroy_activity_duration", "3d"),
),
},
{
Config: testAccTFEProject_basicWithAutoDestroy(rInt, "10m"),
ExpectError: regexp.MustCompile(`must be 1-4 digits followed by d or h`),
},
{
Config: testAccTFEProject_basic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEProjectExists(
"tfe_project.foobar", project),
testAccCheckTFEProjectAttributes(project),
resource.TestCheckResourceAttr(
"tfe_project.foobar", "auto_destroy_activity_duration", ""),
),
},
},
})
}

func testAccTFEProject_update(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
Expand Down Expand Up @@ -182,6 +219,20 @@ resource "tfe_project" "foobar" {
}`, rInt)
}

func testAccTFEProject_basicWithAutoDestroy(rInt int, duration string) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "tst-terraform-%d"
email = "[email protected]"
}

resource "tfe_project" "foobar" {
organization = tfe_organization.foobar.name
name = "projecttest"
auto_destroy_activity_duration = "%s"
}`, rInt, duration)
}

func testAccCheckTFEProjectDestroy(s *terraform.State) error {
config := testAccProvider.Meta().(ConfiguredClient)

Expand Down
17 changes: 15 additions & 2 deletions internal/provider/resource_tfe_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ func resourceTFEWorkspace() *schema.Resource {
Deprecated: "Use resource `tfe_workspace_settings` to modify the workspace `global_remote_state`. `global_remote_state` on `tfe_workspace` is no longer validated properly and will be removed in a future release of the provider.",
},

"inherits_project_auto_destroy": {
Type: schema.TypeBool,
Optional: true,
Computed: true,
},

"remote_state_consumer_ids": {
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -538,6 +544,7 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("working_directory", workspace.WorkingDirectory)
d.Set("organization", workspace.Organization.Name)
d.Set("resource_count", workspace.ResourceCount)
d.Set("inherits_project_auto_destroy", workspace.InheritsProjectAutoDestroy)

if workspace.Links["self-html"] != nil {
baseAPI := config.Client.BaseURL()
Expand Down Expand Up @@ -578,8 +585,9 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
if err != nil {
return fmt.Errorf("Error reading auto destroy activity duration: %w", err)
}

d.Set("auto_destroy_activity_duration", v)
if !workspace.InheritsProjectAutoDestroy {
d.Set("auto_destroy_activity_duration", v)
}
}

var tagNames []interface{}
Expand Down Expand Up @@ -1078,6 +1086,11 @@ func customizeDiffAutoDestroyAt(_ context.Context, d *schema.ResourceDiff) error
return nil
}

// if the workspace inherits project auto destroy, we do not refresh the auto_destroy_at
if d.GetRawState().GetAttr("inherits_project_auto_destroy").True() && config.GetAttr("auto_destroy_at").IsNull() {
return nil
}

// if config auto_destroy_at is unset but it exists in state, clear it out
// required because auto_destroy_at is computed and we want to set it to null
if _, ok := d.GetOk("auto_destroy_at"); ok && config.GetAttr("auto_destroy_at").IsNull() {
Expand Down
Loading
Loading