Skip to content

Commit 863e9f2

Browse files
authored
feat(workspace): add auto destroy activity duration (#1377)
1 parent 6acfb85 commit 863e9f2

File tree

7 files changed

+252
-35
lines changed

7 files changed

+252
-35
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
FEATURES:
44
* `r/tfe_team`: Add attribute `manage_agent_pools` to `organization_access` on `tfe_team` by @emlanctot [#1358](https://github.com/hashicorp/terraform-provider-tfe/pull/1358)
5+
* `r/tfe_workspace`: Add an `auto_destroy_activity_duration` attribute for automatic scheduling of auto-destroy runs based off of workspace activity, by @notchairmk [#1377](https://github.com/hashicorp/terraform-provider-tfe/pull/1377)
6+
* `d/tfe_workspace`: Add an `auto_destroy_activity_duration`, by @notchairmk [#1377](https://github.com/hashicorp/terraform-provider-tfe/pull/1377)
57

68
## v0.56.0
79

internal/provider/data_source_workspace.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ func dataSourceTFEWorkspace() *schema.Resource {
5757
Computed: true,
5858
},
5959

60+
"auto_destroy_activity_duration": {
61+
Type: schema.TypeString,
62+
Computed: true,
63+
},
64+
6065
"file_triggers_enabled": {
6166
Type: schema.TypeBool,
6267
Computed: true,
@@ -251,6 +256,15 @@ func dataSourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error
251256
}
252257
d.Set("auto_destroy_at", autoDestroyAt)
253258

259+
var autoDestroyDuration string
260+
if workspace.AutoDestroyActivityDuration.IsSpecified() {
261+
autoDestroyDuration, err = workspace.AutoDestroyActivityDuration.Get()
262+
if err != nil {
263+
return fmt.Errorf("Error reading auto destroy activity duration: %w", err)
264+
}
265+
}
266+
d.Set("auto_destroy_activity_duration", autoDestroyDuration)
267+
254268
// If target tfe instance predates projects, then workspace.Project will be nil
255269
if workspace.Project != nil {
256270
d.Set("project_id", workspace.Project.ID)

internal/provider/data_source_workspace_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@ func TestAccTFEWorkspaceDataSource_readAutoDestroyAt(t *testing.T) {
190190
})
191191
}
192192

193+
func TestAccTFEWorkspaceDataSource_readAutoDestroyDuration(t *testing.T) {
194+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
195+
196+
resource.Test(t, resource.TestCase{
197+
PreCheck: func() { testAccPreCheck(t) },
198+
Providers: testAccProviders,
199+
Steps: []resource.TestStep{
200+
{
201+
Config: testAccTFEWorkspaceDataSourceConfig_basic(rInt),
202+
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", ""),
203+
},
204+
{
205+
Config: testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroyDuration(rInt),
206+
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
207+
},
208+
},
209+
})
210+
}
211+
193212
func TestAccTFEWorkspaceDataSource_readProjectIDDefault(t *testing.T) {
194213
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
195214

@@ -300,6 +319,27 @@ data "tfe_workspace" "foobar" {
300319
organization = tfe_workspace.foobar.organization
301320
}`, rInt, rInt)
302321
}
322+
323+
func testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroyDuration(rInt int) string {
324+
return fmt.Sprintf(`
325+
resource "tfe_organization" "foobar" {
326+
name = "tst-terraform-%d"
327+
328+
}
329+
330+
resource "tfe_workspace" "foobar" {
331+
name = "workspace-test-%d"
332+
organization = tfe_organization.foobar.id
333+
description = "provider-testing"
334+
auto_destroy_activity_duration = "1d"
335+
}
336+
337+
data "tfe_workspace" "foobar" {
338+
name = tfe_workspace.foobar.name
339+
organization = tfe_workspace.foobar.organization
340+
}`, rInt, rInt)
341+
}
342+
303343
func testAccTFEWorkspaceDataSourceConfigWithTriggerPatterns(workspaceName string, organizationName string) string {
304344
return fmt.Sprintf(`
305345
data "tfe_workspace" "foobar" {

internal/provider/resource_tfe_workspace.go

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ func resourceTFEWorkspace() *schema.Resource {
6363
return err
6464
}
6565

66+
if err := customizeDiffAutoDestroyAt(c, d); err != nil {
67+
return err
68+
}
69+
6670
return nil
6771
},
6872

@@ -112,9 +116,17 @@ func resourceTFEWorkspace() *schema.Resource {
112116

113117
"auto_destroy_at": {
114118
Type: schema.TypeString,
119+
Computed: true,
115120
Optional: true,
116121
},
117122

123+
"auto_destroy_activity_duration": {
124+
Type: schema.TypeString,
125+
Optional: true,
126+
ConflictsWith: []string{"auto_destroy_at"},
127+
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^\d{1,4}[dh]$`), "must be 1-4 digits followed by d or h"),
128+
},
129+
118130
"execution_mode": {
119131
Type: schema.TypeString,
120132
Optional: true,
@@ -354,6 +366,10 @@ func resourceTFEWorkspaceCreate(d *schema.ResourceData, meta interface{}) error
354366
options.AutoDestroyAt = autoDestroyAt
355367
}
356368

369+
if v, ok := d.GetOk("auto_destroy_activity_duration"); ok {
370+
options.AutoDestroyActivityDuration = jsonapi.NewNullableAttrWithValue(v.(string))
371+
}
372+
357373
if v, ok := d.GetOk("execution_mode"); ok {
358374
executionMode := tfe.String(v.(string))
359375
options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{
@@ -553,6 +569,15 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
553569
}
554570
d.Set("auto_destroy_at", autoDestroyAt)
555571

572+
if workspace.AutoDestroyActivityDuration.IsSpecified() {
573+
v, err := workspace.AutoDestroyActivityDuration.Get()
574+
if err != nil {
575+
return fmt.Errorf("Error reading auto destroy activity duration: %w", err)
576+
}
577+
578+
d.Set("auto_destroy_activity_duration", v)
579+
}
580+
556581
var tagNames []interface{}
557582
managedTags := d.Get("tag_names").(*schema.Set)
558583
for _, tagName := range workspace.TagNames {
@@ -605,7 +630,8 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error
605630
d.HasChange("operations") || d.HasChange("execution_mode") ||
606631
d.HasChange("description") || d.HasChange("agent_pool_id") ||
607632
d.HasChange("global_remote_state") || d.HasChange("structured_run_output_enabled") ||
608-
d.HasChange("assessments_enabled") || d.HasChange("project_id") || d.HasChange("auto_destroy_at") {
633+
d.HasChange("assessments_enabled") || d.HasChange("project_id") ||
634+
hasAutoDestroyAtChange(d) || d.HasChange("auto_destroy_activity_duration") {
609635
// Create a new options struct.
610636
options := tfe.WorkspaceUpdateOptions{
611637
Name: tfe.String(d.Get("name").(string)),
@@ -658,14 +684,23 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error
658684
}
659685
}
660686

661-
if d.HasChange("auto_destroy_at") {
687+
if hasAutoDestroyAtChange(d) {
662688
autoDestroyAt, err := expandAutoDestroyAt(d)
663689
if err != nil {
664690
return fmt.Errorf("Error expanding auto destroy during update: %w", err)
665691
}
666692
options.AutoDestroyAt = autoDestroyAt
667693
}
668694

695+
if d.HasChange("auto_destroy_activity_duration") {
696+
duration, ok := d.GetOk("auto_destroy_activity_duration")
697+
if !ok {
698+
options.AutoDestroyActivityDuration = jsonapi.NewNullNullableAttr[string]()
699+
} else {
700+
options.AutoDestroyActivityDuration = jsonapi.NewNullableAttrWithValue(duration.(string))
701+
}
702+
}
703+
669704
if d.HasChange("execution_mode") {
670705
if v, ok := d.GetOk("execution_mode"); ok {
671706
options.ExecutionMode = tfe.String(v.(string))
@@ -961,35 +996,6 @@ func validateAgentExecution(_ context.Context, d *schema.ResourceDiff) error {
961996
return nil
962997
}
963998

964-
func expandAutoDestroyAt(d *schema.ResourceData) (jsonapi.NullableAttr[time.Time], error) {
965-
v, ok := d.GetOk("auto_destroy_at")
966-
967-
if !ok {
968-
return jsonapi.NewNullNullableAttr[time.Time](), nil
969-
}
970-
971-
autoDestroyAt, err := time.Parse(time.RFC3339, v.(string))
972-
if err != nil {
973-
return nil, err
974-
}
975-
976-
return jsonapi.NewNullableAttrWithValue(autoDestroyAt), nil
977-
}
978-
979-
func flattenAutoDestroyAt(a jsonapi.NullableAttr[time.Time]) (*string, error) {
980-
if !a.IsSpecified() {
981-
return nil, nil
982-
}
983-
984-
autoDestroyTime, err := a.Get()
985-
if err != nil {
986-
return nil, err
987-
}
988-
989-
autoDestroyAt := autoDestroyTime.Format(time.RFC3339)
990-
return &autoDestroyAt, nil
991-
}
992-
993999
func validTagName(tag string) bool {
9941000
// Tags are re-validated here because the API will accept uppercase letters and automatically
9951001
// downcase them, causing resource drift. It's better to catch this issue during the plan phase
@@ -1076,3 +1082,64 @@ func errWorkspaceResourceCountCheck(workspaceID string, resourceCount int) error
10761082
}
10771083
return nil
10781084
}
1085+
1086+
func customizeDiffAutoDestroyAt(_ context.Context, d *schema.ResourceDiff) error {
1087+
config := d.GetRawConfig()
1088+
1089+
// check if auto_destroy_activity_duration is set in config
1090+
if !config.GetAttr("auto_destroy_activity_duration").IsNull() {
1091+
return nil
1092+
}
1093+
1094+
// if config auto_destroy_at is unset but it exists in state, clear it out
1095+
// required because auto_destroy_at is computed and we want to set it to null
1096+
if _, ok := d.GetOk("auto_destroy_at"); ok && config.GetAttr("auto_destroy_at").IsNull() {
1097+
return d.SetNew("auto_destroy_at", nil)
1098+
}
1099+
1100+
return nil
1101+
}
1102+
1103+
func expandAutoDestroyAt(d *schema.ResourceData) (jsonapi.NullableAttr[time.Time], error) {
1104+
v := d.GetRawConfig().GetAttr("auto_destroy_at")
1105+
1106+
if v.IsNull() {
1107+
return jsonapi.NewNullNullableAttr[time.Time](), nil
1108+
}
1109+
1110+
autoDestroyAt, err := time.Parse(time.RFC3339, v.AsString())
1111+
if err != nil {
1112+
return nil, err
1113+
}
1114+
1115+
return jsonapi.NewNullableAttrWithValue(autoDestroyAt), nil
1116+
}
1117+
1118+
func flattenAutoDestroyAt(a jsonapi.NullableAttr[time.Time]) (*string, error) {
1119+
if !a.IsSpecified() {
1120+
return nil, nil
1121+
}
1122+
1123+
autoDestroyTime, err := a.Get()
1124+
if err != nil {
1125+
return nil, err
1126+
}
1127+
1128+
autoDestroyAt := autoDestroyTime.Format(time.RFC3339)
1129+
return &autoDestroyAt, nil
1130+
}
1131+
1132+
func hasAutoDestroyAtChange(d *schema.ResourceData) bool {
1133+
state := d.GetRawState()
1134+
if state.IsNull() {
1135+
return d.HasChange("auto_destroy_at")
1136+
}
1137+
1138+
config := d.GetRawConfig()
1139+
autoDestroyAt := config.GetAttr("auto_destroy_at")
1140+
if !autoDestroyAt.IsNull() {
1141+
return d.HasChange("auto_destroy_at")
1142+
}
1143+
1144+
return config.GetAttr("auto_destroy_at") != state.GetAttr("auto_destroy_at")
1145+
}

internal/provider/resource_tfe_workspace_test.go

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2694,6 +2694,82 @@ func TestAccTFEWorkspace_updateWithAutoDestroyAt(t *testing.T) {
26942694
})
26952695
}
26962696

2697+
func TestAccTFEWorkspace_createWithAutoDestroyDuration(t *testing.T) {
2698+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
2699+
2700+
resource.Test(t, resource.TestCase{
2701+
PreCheck: func() { testAccPreCheck(t) },
2702+
Providers: testAccProviders,
2703+
CheckDestroy: testAccCheckTFEWorkspaceDestroy,
2704+
Steps: []resource.TestStep{
2705+
{
2706+
Config: testAccTFEWorkspace_basicWithAutoDestroyDuration(rInt, "1d"),
2707+
Check: resource.ComposeTestCheckFunc(
2708+
testAccCheckTFEWorkspaceExists("tfe_workspace.foobar", &tfe.Workspace{}, testAccProvider),
2709+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
2710+
),
2711+
},
2712+
},
2713+
})
2714+
}
2715+
2716+
func TestAccTFEWorkspace_updateWithAutoDestroyDuration(t *testing.T) {
2717+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
2718+
2719+
resource.Test(t, resource.TestCase{
2720+
PreCheck: func() { testAccPreCheck(t) },
2721+
Providers: testAccProviders,
2722+
CheckDestroy: testAccCheckTFEWorkspaceDestroy,
2723+
Steps: []resource.TestStep{
2724+
{
2725+
Config: testAccTFEWorkspace_basicWithAutoDestroyDuration(rInt, "1d"),
2726+
Check: resource.ComposeTestCheckFunc(
2727+
testAccCheckTFEWorkspaceExists("tfe_workspace.foobar", &tfe.Workspace{}, testAccProvider),
2728+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
2729+
),
2730+
},
2731+
{
2732+
Config: testAccTFEWorkspace_basicWithAutoDestroyAt(rInt),
2733+
Check: resource.ComposeTestCheckFunc(
2734+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_activity_duration", ""),
2735+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", "2100-01-01T00:00:00Z"),
2736+
),
2737+
},
2738+
{
2739+
Config: testAccTFEWorkspace_basicWithAutoDestroyDuration(rInt, "1d"),
2740+
Check: resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"),
2741+
},
2742+
{
2743+
Config: testAccTFEWorkspace_basic(rInt),
2744+
Check: resource.ComposeTestCheckFunc(
2745+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_at", ""),
2746+
resource.TestCheckResourceAttr("tfe_workspace.foobar", "auto_destroy_activity_duration", ""),
2747+
),
2748+
},
2749+
},
2750+
})
2751+
}
2752+
2753+
func TestAccTFEWorkspace_validationAutoDestroyDuration(t *testing.T) {
2754+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
2755+
2756+
values := []string{"d", "1w", "1d1", "123456h"}
2757+
steps := []resource.TestStep{}
2758+
for _, value := range values {
2759+
steps = append(steps, resource.TestStep{
2760+
Config: testAccTFEWorkspace_basicWithAutoDestroyDuration(rInt, value),
2761+
ExpectError: regexp.MustCompile("must be 1-4 digits followed by d or h"),
2762+
})
2763+
}
2764+
2765+
resource.Test(t, resource.TestCase{
2766+
PreCheck: func() { testAccPreCheck(t) },
2767+
Providers: testAccProviders,
2768+
CheckDestroy: testAccCheckTFEWorkspaceDestroy,
2769+
Steps: steps,
2770+
})
2771+
}
2772+
26972773
func TestAccTFEWorkspace_createWithSourceURL(t *testing.T) {
26982774
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
26992775

@@ -2978,14 +3054,30 @@ resource "tfe_organization" "foobar" {
29783054
}
29793055
29803056
resource "tfe_workspace" "foobar" {
2981-
name = "workspace-test"
2982-
organization = tfe_organization.foobar.id
2983-
auto_apply = true
3057+
name = "workspace-test"
3058+
organization = tfe_organization.foobar.id
3059+
auto_apply = true
29843060
file_triggers_enabled = false
29853061
auto_destroy_at = "2100-01-01T00:00:00Z"
29863062
}`, rInt)
29873063
}
29883064

3065+
func testAccTFEWorkspace_basicWithAutoDestroyDuration(rInt int, value string) string {
3066+
return fmt.Sprintf(`
3067+
resource "tfe_organization" "foobar" {
3068+
name = "tst-terraform-%d"
3069+
3070+
}
3071+
3072+
resource "tfe_workspace" "foobar" {
3073+
name = "workspace-test"
3074+
organization = tfe_organization.foobar.id
3075+
auto_apply = true
3076+
file_triggers_enabled = false
3077+
auto_destroy_activity_duration = "%s"
3078+
}`, rInt, value)
3079+
}
3080+
29893081
func testAccTFEWorkspace_operationsTrue(organization string) string {
29903082
return fmt.Sprintf(`
29913083
resource "tfe_workspace" "foobar" {

0 commit comments

Comments
 (0)