Skip to content

Commit

Permalink
Merge pull request #1550 from hashicorp/simonxmh/add_project_level_au…
Browse files Browse the repository at this point in the history
…to_destroy

Add project level auto destroy
  • Loading branch information
simonxmh authored Feb 3, 2025
2 parents cccffac + 6dd39b3 commit 404d250
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 40 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## Unreleased

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)

BUG FIXES:
* `r/tfe_stack`: Fix serialization issue when using github app installation within vcs_repo block, by @mjyocca [#1572](https://github.com/hashicorp/terraform-provider-tfe/pull/1572)

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) {
skipIfEnterprise(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
10 changes: 8 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
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
Loading

0 comments on commit 404d250

Please sign in to comment.