-
Notifications
You must be signed in to change notification settings - Fork 156
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
feat(workspace): add auto destroy activity duration #1377
Changes from 6 commits
6074f95
3b73392
34c4695
6cdf1e6
f529b02
0ac6e4a
6b66010
dfc82e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -190,6 +190,25 @@ func TestAccTFEWorkspaceDataSource_readAutoDestroyAt(t *testing.T) { | |
}) | ||
} | ||
|
||
func TestAccTFEWorkspaceDataSource_readAutoDestroyDuration(t *testing.T) { | ||
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() | ||
|
||
resource.Test(t, resource.TestCase{ | ||
PreCheck: func() { testAccPreCheck(t) }, | ||
Providers: testAccProviders, | ||
Steps: []resource.TestStep{ | ||
{ | ||
Config: testAccTFEWorkspaceDataSourceConfig_basic(rInt), | ||
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", ""), | ||
}, | ||
{ | ||
Config: testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroyDuration(rInt), | ||
Check: resource.TestCheckResourceAttr("data.tfe_workspace.foobar", "auto_destroy_activity_duration", "1d"), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func TestAccTFEWorkspaceDataSource_readProjectIDDefault(t *testing.T) { | ||
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() | ||
|
||
|
@@ -300,6 +319,27 @@ data "tfe_workspace" "foobar" { | |
organization = tfe_workspace.foobar.organization | ||
}`, rInt, rInt) | ||
} | ||
|
||
func testAccTFEWorkspaceDataSourceConfig_basicWithAutoDestroyDuration(rInt int) string { | ||
return fmt.Sprintf(` | ||
resource "tfe_organization" "foobar" { | ||
name = "tst-terraform-%d" | ||
email = "[email protected]" | ||
} | ||
|
||
resource "tfe_workspace" "foobar" { | ||
name = "workspace-test-%d" | ||
organization = tfe_organization.foobar.id | ||
description = "provider-testing" | ||
auto_destroy_activity_duration = "1d" | ||
} | ||
|
||
data "tfe_workspace" "foobar" { | ||
name = tfe_workspace.foobar.name | ||
organization = tfe_workspace.foobar.organization | ||
}`, rInt, rInt) | ||
} | ||
|
||
func testAccTFEWorkspaceDataSourceConfigWithTriggerPatterns(workspaceName string, organizationName string) string { | ||
return fmt.Sprintf(` | ||
data "tfe_workspace" "foobar" { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -63,6 +63,10 @@ func resourceTFEWorkspace() *schema.Resource { | |||||
return err | ||||||
} | ||||||
|
||||||
if err := customizeDiffAutoDestroyAt(c, d); err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
return nil | ||||||
}, | ||||||
|
||||||
|
@@ -112,9 +116,17 @@ func resourceTFEWorkspace() *schema.Resource { | |||||
|
||||||
"auto_destroy_at": { | ||||||
Type: schema.TypeString, | ||||||
Computed: true, | ||||||
Optional: true, | ||||||
}, | ||||||
|
||||||
"auto_destroy_activity_duration": { | ||||||
Type: schema.TypeString, | ||||||
Optional: true, | ||||||
ConflictsWith: []string{"auto_destroy_at"}, | ||||||
ValidateFunc: validation.StringMatch(regexp.MustCompile(`^\d{1,5}[dh]$`), "must be 1-5 digits followed by d or h"), | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. edge case but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
validation is mostly an additional nice-to-have to give the user early feedback. I'm fine with the logic here not needing to 100% match the business logic in the API, since it's an edge case |
||||||
}, | ||||||
|
||||||
"execution_mode": { | ||||||
Type: schema.TypeString, | ||||||
Optional: true, | ||||||
|
@@ -354,6 +366,10 @@ func resourceTFEWorkspaceCreate(d *schema.ResourceData, meta interface{}) error | |||||
options.AutoDestroyAt = autoDestroyAt | ||||||
} | ||||||
|
||||||
if v, ok := d.GetOk("auto_destroy_activity_duration"); ok { | ||||||
options.AutoDestroyActivityDuration = jsonapi.NewNullableAttrWithValue(v.(string)) | ||||||
} | ||||||
|
||||||
if v, ok := d.GetOk("execution_mode"); ok { | ||||||
executionMode := tfe.String(v.(string)) | ||||||
options.SettingOverwrites = &tfe.WorkspaceSettingOverwritesOptions{ | ||||||
|
@@ -553,6 +569,15 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error { | |||||
} | ||||||
d.Set("auto_destroy_at", autoDestroyAt) | ||||||
|
||||||
if workspace.AutoDestroyActivityDuration.IsSpecified() { | ||||||
v, err := workspace.AutoDestroyActivityDuration.Get() | ||||||
if err != nil { | ||||||
return fmt.Errorf("Error reading auto destroy activity duration: %w", err) | ||||||
} | ||||||
|
||||||
d.Set("auto_destroy_activity_duration", v) | ||||||
} | ||||||
|
||||||
var tagNames []interface{} | ||||||
managedTags := d.Get("tag_names").(*schema.Set) | ||||||
for _, tagName := range workspace.TagNames { | ||||||
|
@@ -605,7 +630,8 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error | |||||
d.HasChange("operations") || d.HasChange("execution_mode") || | ||||||
d.HasChange("description") || d.HasChange("agent_pool_id") || | ||||||
d.HasChange("global_remote_state") || d.HasChange("structured_run_output_enabled") || | ||||||
d.HasChange("assessments_enabled") || d.HasChange("project_id") || d.HasChange("auto_destroy_at") { | ||||||
d.HasChange("assessments_enabled") || d.HasChange("project_id") || | ||||||
hasAutoDestroyAtChange(d) || d.HasChange("auto_destroy_activity_duration") { | ||||||
// Create a new options struct. | ||||||
options := tfe.WorkspaceUpdateOptions{ | ||||||
Name: tfe.String(d.Get("name").(string)), | ||||||
|
@@ -658,14 +684,23 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error | |||||
} | ||||||
} | ||||||
|
||||||
if d.HasChange("auto_destroy_at") { | ||||||
if hasAutoDestroyAtChange(d) { | ||||||
autoDestroyAt, err := expandAutoDestroyAt(d) | ||||||
if err != nil { | ||||||
return fmt.Errorf("Error expanding auto destroy during update: %w", err) | ||||||
} | ||||||
options.AutoDestroyAt = autoDestroyAt | ||||||
} | ||||||
|
||||||
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)) | ||||||
} | ||||||
} | ||||||
|
||||||
if d.HasChange("execution_mode") { | ||||||
if v, ok := d.GetOk("execution_mode"); ok { | ||||||
options.ExecutionMode = tfe.String(v.(string)) | ||||||
|
@@ -961,35 +996,6 @@ func validateAgentExecution(_ context.Context, d *schema.ResourceDiff) error { | |||||
return nil | ||||||
} | ||||||
|
||||||
func expandAutoDestroyAt(d *schema.ResourceData) (jsonapi.NullableAttr[time.Time], error) { | ||||||
v, ok := d.GetOk("auto_destroy_at") | ||||||
|
||||||
if !ok { | ||||||
return jsonapi.NewNullNullableAttr[time.Time](), nil | ||||||
} | ||||||
|
||||||
autoDestroyAt, err := time.Parse(time.RFC3339, v.(string)) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
return jsonapi.NewNullableAttrWithValue(autoDestroyAt), nil | ||||||
} | ||||||
|
||||||
func flattenAutoDestroyAt(a jsonapi.NullableAttr[time.Time]) (*string, error) { | ||||||
if !a.IsSpecified() { | ||||||
return nil, nil | ||||||
} | ||||||
|
||||||
autoDestroyTime, err := a.Get() | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
autoDestroyAt := autoDestroyTime.Format(time.RFC3339) | ||||||
return &autoDestroyAt, nil | ||||||
} | ||||||
|
||||||
func validTagName(tag string) bool { | ||||||
// Tags are re-validated here because the API will accept uppercase letters and automatically | ||||||
// 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 | |||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
func customizeDiffAutoDestroyAt(_ context.Context, d *schema.ResourceDiff) error { | ||||||
config := d.GetRawConfig() | ||||||
|
||||||
// check if auto_destroy_activity_duration is set in config | ||||||
if !config.GetAttr("auto_destroy_activity_duration").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() { | ||||||
return d.SetNew("auto_destroy_at", nil) | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
func expandAutoDestroyAt(d *schema.ResourceData) (jsonapi.NullableAttr[time.Time], error) { | ||||||
v := d.GetRawConfig().GetAttr("auto_destroy_at") | ||||||
|
||||||
if v.IsNull() { | ||||||
return jsonapi.NewNullNullableAttr[time.Time](), nil | ||||||
} | ||||||
|
||||||
autoDestroyAt, err := time.Parse(time.RFC3339, v.AsString()) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
return jsonapi.NewNullableAttrWithValue(autoDestroyAt), nil | ||||||
} | ||||||
|
||||||
func flattenAutoDestroyAt(a jsonapi.NullableAttr[time.Time]) (*string, error) { | ||||||
if !a.IsSpecified() { | ||||||
return nil, nil | ||||||
} | ||||||
|
||||||
autoDestroyTime, err := a.Get() | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
autoDestroyAt := autoDestroyTime.Format(time.RFC3339) | ||||||
return &autoDestroyAt, nil | ||||||
} | ||||||
|
||||||
func hasAutoDestroyAtChange(d *schema.ResourceData) bool { | ||||||
state := d.GetRawState() | ||||||
if state.IsNull() { | ||||||
return d.HasChange("auto_destroy_at") | ||||||
} | ||||||
|
||||||
config := d.GetRawConfig() | ||||||
autoDestroyAt := config.GetAttr("auto_destroy_at") | ||||||
if !autoDestroyAt.IsNull() { | ||||||
return d.HasChange("auto_destroy_at") | ||||||
} | ||||||
|
||||||
return config.GetAttr("auto_destroy_at") != state.GetAttr("auto_destroy_at") | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately most of the raw config/state checking in this PR is due to this value being changed to
computed
. The main problem is insuring that unsettingauto_destroy_at
(whenauto_destroy_activity_duration
is also unset) sends a null value to the TFC API.