Skip to content

Commit 9ff07e2

Browse files
authored
Use the stages attribute for the Workspace Run Task resource (#1459)
* Add a client capabilites resolver This commit adds a resolver interface which will be used within the codebase to determine what the capabilities of the remote service are (e.g. Is is Enterprise, if so what version). Note this is an interface, not a struct to make testing easier. Later commits will use this resolver. * Use the new stages attribute for run tasks Previously the schema for workspace run tasks was updated for the new stages property however it wasn't actually used. This commit updates the Workspace Run Task resource to aware of the stages attributes. In particular; * Attempts to detect if the remote service supports the stages property. Stages is available in HCP Terraform and TFE v202404-1 onwards. * Munges the Stages and Stage attribtue depending on the remote capability. * Emits a warning about the remove server capability. * Adds some automated tests. Unfortunately we can't test older TFE versions, to ensure the munging is correct, however manual testing was performed in a local development enivronment to confirm the behaviour. * Removes the default value for the Stage property in the Schema. This will not cause issues with existing state and allows the provider to determine if the attribute was passed in via configuration as opposed to defaulting. * Update changelog * Memoize the result for supportsStagesProperty * Remove pointer for slice Removing the pointer reference as it's not required. * Do not reuse test subject for workspace run task Previously the test subject for the stagesSupport unit tests would reuse the subject and resolver however this led to timing issues when parallel tests were run. This commit changes the test to create new objects per test.
1 parent 275c68c commit 9ff07e2

6 files changed

+318
-16
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
* `r/tfe_team`: Default "secret" visibility has been removed from tfe_team because it now requires explicit or owner access. The default, "organization", is now computed by the platform. by @brandonc [#1439](https://github.com/hashicorp/terraform-provider-tfe/pull/1439)
66

7+
BUG FIXES:
8+
* `r/tfe_workspace_run_task`: The Workspace Run Task resource will use the stages attribute by @glennsarti [#1459](https://github.com/hashicorp/terraform-provider-tfe/pull/1459)
9+
710
## v0.58.0
811

912
ENHANCEMENTS:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package provider
2+
3+
import (
4+
tfe "github.com/hashicorp/go-tfe"
5+
)
6+
7+
type capabilitiesResolver interface {
8+
IsCloud() bool
9+
RemoteTFEVersion() string
10+
}
11+
12+
func newDefaultCapabilityResolver(client *tfe.Client) capabilitiesResolver {
13+
return &defaultCapabilityResolver{
14+
client: client,
15+
}
16+
}
17+
18+
type defaultCapabilityResolver struct {
19+
client *tfe.Client
20+
}
21+
22+
func (r *defaultCapabilityResolver) IsCloud() bool {
23+
return r.client.IsCloud()
24+
}
25+
26+
func (r *defaultCapabilityResolver) RemoteTFEVersion() string {
27+
return r.client.RemoteTFEVersion()
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package provider
2+
3+
var _ capabilitiesResolver = &staticCapabilityResolver{}
4+
5+
// A mock capability resolver used for testing to set specific capabilities
6+
type staticCapabilityResolver struct {
7+
isCloud bool
8+
tfeVer string
9+
}
10+
11+
func (r *staticCapabilityResolver) IsCloud() bool {
12+
return r.isCloud
13+
}
14+
15+
func (r *staticCapabilityResolver) RemoteTFEVersion() string {
16+
return r.tfeVer
17+
}
18+
19+
func (r *staticCapabilityResolver) SetIsCloud(val bool) {
20+
r.isCloud = val
21+
}
22+
23+
func (r *staticCapabilityResolver) SetRemoteTFEVersion(val string) {
24+
r.tfeVer = val
25+
}

internal/provider/resource_tfe_workspace_run_task.go

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111

1212
tfe "github.com/hashicorp/go-tfe"
13+
"github.com/hashicorp/terraform-plugin-framework/diag"
1314
"github.com/hashicorp/terraform-plugin-framework/resource"
1415

1516
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -50,7 +51,9 @@ func sentenceList(items []string, prefix string, suffix string, conjunction stri
5051
}
5152

5253
type resourceWorkspaceRunTask struct {
53-
config ConfiguredClient
54+
config ConfiguredClient
55+
capabilities capabilitiesResolver
56+
supportsStages *bool
5457
}
5558

5659
var _ resource.Resource = &resourceWorkspaceRunTask{}
@@ -97,6 +100,7 @@ func (r *resourceWorkspaceRunTask) Configure(ctx context.Context, req resource.C
97100
)
98101
}
99102
r.config = client
103+
r.capabilities = newDefaultCapabilityResolver(client.Client)
100104
}
101105

102106
func (r *resourceWorkspaceRunTask) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
@@ -150,13 +154,23 @@ func (r *resourceWorkspaceRunTask) Create(ctx context.Context, req resource.Crea
150154
return
151155
}
152156

153-
stage := tfe.Stage(plan.Stage.ValueString())
154157
level := tfe.TaskEnforcementLevel(plan.EnforcementLevel.ValueString())
155158

156159
options := tfe.WorkspaceRunTaskCreateOptions{
157160
RunTask: task,
158161
EnforcementLevel: level,
159-
Stage: &stage,
162+
}
163+
164+
stage, stages := r.extractStageAndStages(plan, &resp.Diagnostics)
165+
if resp.Diagnostics.HasError() {
166+
return
167+
}
168+
if stage != nil {
169+
// Needed for older TFE instances
170+
options.Stage = stage //nolint:staticcheck
171+
}
172+
if stages != nil {
173+
options.Stages = &stages
160174
}
161175

162176
tflog.Debug(ctx, fmt.Sprintf("Create task %s in workspace: %s", taskID, workspaceID))
@@ -190,11 +204,21 @@ func (r *resourceWorkspaceRunTask) Update(ctx context.Context, req resource.Upda
190204
}
191205

192206
level := tfe.TaskEnforcementLevel(plan.EnforcementLevel.ValueString())
193-
stage := r.stringPointerToStagePointer(plan.Stage.ValueStringPointer())
194207

195208
options := tfe.WorkspaceRunTaskUpdateOptions{
196209
EnforcementLevel: level,
197-
Stage: stage,
210+
}
211+
212+
stage, stages := r.extractStageAndStages(plan, &resp.Diagnostics)
213+
if resp.Diagnostics.HasError() {
214+
return
215+
}
216+
if stage != nil {
217+
// Needed for older TFE instances
218+
options.Stage = stage //nolint:staticcheck
219+
}
220+
if stages != nil {
221+
options.Stages = &stages
198222
}
199223

200224
wstaskID := plan.ID.ValueString()
@@ -297,3 +321,88 @@ func (r *resourceWorkspaceRunTask) UpgradeState(ctx context.Context) map[int64]r
297321
},
298322
}
299323
}
324+
325+
func (r *resourceWorkspaceRunTask) supportsStagesProperty() bool {
326+
// The Stages property is available in HCP Terraform and Terraform Enterprise v202404-1 onwards.
327+
//
328+
// The version comparison here can use plain string comparisons due to the nature of the naming scheme. If
329+
// TFE every changes its scheme, the comparison will be problematic.
330+
if r.supportsStages == nil {
331+
value := r.capabilities.IsCloud() || r.capabilities.RemoteTFEVersion() > "v202404"
332+
r.supportsStages = &value
333+
}
334+
return *r.supportsStages
335+
}
336+
337+
func (r *resourceWorkspaceRunTask) addStageSupportDiag(d *diag.Diagnostics, isError bool) {
338+
summary := "Terraform Enterprise version"
339+
detail := fmt.Sprintf("The version of Terraform Enterprise does not support the stages attribute on Workspace Run Tasks. Got %s but requires v202404-1+", r.config.Client.RemoteTFEVersion())
340+
if isError {
341+
d.AddError(detail, summary)
342+
} else {
343+
d.AddWarning(detail, summary)
344+
}
345+
}
346+
347+
func (r *resourceWorkspaceRunTask) extractStageAndStages(plan modelTFEWorkspaceRunTaskV1, d *diag.Diagnostics) (*tfe.Stage, []tfe.Stage) {
348+
// There are some complex interactions here between deprecated values in the TF model, and whether the backend server even supports the newer
349+
// API call style. This function attempts to extract the Stage and Stages properties and emit useful diagnostics
350+
351+
// If neither stage or stages is set, then it's all fine, we use the server defaults
352+
if plan.Stage.IsUnknown() && plan.Stages.IsUnknown() {
353+
return nil, nil
354+
}
355+
356+
if r.supportsStagesProperty() {
357+
if plan.Stages.IsUnknown() {
358+
// The user has supplied Stage but not Stages. They would already have received the deprecation warning so just munge
359+
// the stage into a slice and we're fine
360+
stages := []tfe.Stage{tfe.Stage(plan.Stage.ValueString())}
361+
return nil, stages
362+
}
363+
364+
// Convert the plan values into the slice we need
365+
var stageStrings []types.String
366+
if err := plan.Stages.ElementsAs(ctx, &stageStrings, false); err != nil && err.HasError() {
367+
d.Append(err...)
368+
return nil, nil
369+
}
370+
stages := make([]tfe.Stage, len(stageStrings))
371+
for idx, s := range stageStrings {
372+
stages[idx] = tfe.Stage(s.ValueString())
373+
}
374+
return nil, stages
375+
}
376+
377+
// The backend server doesn't support Stages
378+
if !plan.Stages.IsUnknown() {
379+
// The user has supplied a stages array. We need to figure out if we can munge this into a stage attribute
380+
stagesCount := len(plan.Stages.Elements())
381+
382+
if stagesCount > 1 {
383+
// The user has supplied more than one stage so we can't munge this
384+
r.addStageSupportDiag(d, true)
385+
return nil, nil
386+
}
387+
388+
// Send the warning
389+
r.addStageSupportDiag(d, false)
390+
391+
if stagesCount == 0 {
392+
// Somehow we've got no stages listed. Use default server values
393+
return nil, nil
394+
}
395+
396+
// ... Otherwise there's a single Stages value which we can munge into Stage.
397+
var stageStrings []types.String
398+
if err := plan.Stages.ElementsAs(ctx, &stageStrings, false); err != nil && err.HasError() {
399+
d.Append(err...)
400+
return nil, nil
401+
}
402+
stage := tfe.Stage(stageStrings[0].ValueString())
403+
return &stage, nil
404+
}
405+
406+
// The user supplied a Stage value to a server that doesn't support stages
407+
return r.stringPointerToStagePointer(plan.Stage.ValueStringPointer()), nil
408+
}

internal/provider/resource_tfe_workspace_run_task_schemas.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ var resourceWorkspaceRunTaskSchemaV1 = schema.Schema{
134134
)),
135135
Optional: true,
136136
Computed: true,
137-
Default: stringdefault.StaticString(string(tfe.PostPlan)),
138137
Validators: []validator.String{
139138
stringvalidator.OneOf(workspaceRunTaskStages()...),
140139
},

0 commit comments

Comments
 (0)