Skip to content

Commit

Permalink
Use the stages attribute for the Workspace Run Task resource (#1459)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
glennsarti authored Sep 5, 2024
1 parent 275c68c commit 9ff07e2
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

* `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)

BUG FIXES:
* `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)

## v0.58.0

ENHANCEMENTS:
Expand Down
28 changes: 28 additions & 0 deletions internal/provider/client_capabilites.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package provider

import (
tfe "github.com/hashicorp/go-tfe"
)

type capabilitiesResolver interface {
IsCloud() bool
RemoteTFEVersion() string
}

func newDefaultCapabilityResolver(client *tfe.Client) capabilitiesResolver {
return &defaultCapabilityResolver{
client: client,
}
}

type defaultCapabilityResolver struct {
client *tfe.Client
}

func (r *defaultCapabilityResolver) IsCloud() bool {
return r.client.IsCloud()
}

func (r *defaultCapabilityResolver) RemoteTFEVersion() string {
return r.client.RemoteTFEVersion()
}
25 changes: 25 additions & 0 deletions internal/provider/client_capabilites_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package provider

var _ capabilitiesResolver = &staticCapabilityResolver{}

// A mock capability resolver used for testing to set specific capabilities
type staticCapabilityResolver struct {
isCloud bool
tfeVer string
}

func (r *staticCapabilityResolver) IsCloud() bool {
return r.isCloud
}

func (r *staticCapabilityResolver) RemoteTFEVersion() string {
return r.tfeVer
}

func (r *staticCapabilityResolver) SetIsCloud(val bool) {
r.isCloud = val
}

func (r *staticCapabilityResolver) SetRemoteTFEVersion(val string) {
r.tfeVer = val
}
119 changes: 114 additions & 5 deletions internal/provider/resource_tfe_workspace_run_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"

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

type resourceWorkspaceRunTask struct {
config ConfiguredClient
config ConfiguredClient
capabilities capabilitiesResolver
supportsStages *bool
}

var _ resource.Resource = &resourceWorkspaceRunTask{}
Expand Down Expand Up @@ -97,6 +100,7 @@ func (r *resourceWorkspaceRunTask) Configure(ctx context.Context, req resource.C
)
}
r.config = client
r.capabilities = newDefaultCapabilityResolver(client.Client)
}

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

stage := tfe.Stage(plan.Stage.ValueString())
level := tfe.TaskEnforcementLevel(plan.EnforcementLevel.ValueString())

options := tfe.WorkspaceRunTaskCreateOptions{
RunTask: task,
EnforcementLevel: level,
Stage: &stage,
}

stage, stages := r.extractStageAndStages(plan, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
if stage != nil {
// Needed for older TFE instances
options.Stage = stage //nolint:staticcheck
}
if stages != nil {
options.Stages = &stages
}

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

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

options := tfe.WorkspaceRunTaskUpdateOptions{
EnforcementLevel: level,
Stage: stage,
}

stage, stages := r.extractStageAndStages(plan, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
if stage != nil {
// Needed for older TFE instances
options.Stage = stage //nolint:staticcheck
}
if stages != nil {
options.Stages = &stages
}

wstaskID := plan.ID.ValueString()
Expand Down Expand Up @@ -297,3 +321,88 @@ func (r *resourceWorkspaceRunTask) UpgradeState(ctx context.Context) map[int64]r
},
}
}

func (r *resourceWorkspaceRunTask) supportsStagesProperty() bool {
// The Stages property is available in HCP Terraform and Terraform Enterprise v202404-1 onwards.
//
// The version comparison here can use plain string comparisons due to the nature of the naming scheme. If
// TFE every changes its scheme, the comparison will be problematic.
if r.supportsStages == nil {
value := r.capabilities.IsCloud() || r.capabilities.RemoteTFEVersion() > "v202404"
r.supportsStages = &value
}
return *r.supportsStages
}

func (r *resourceWorkspaceRunTask) addStageSupportDiag(d *diag.Diagnostics, isError bool) {
summary := "Terraform Enterprise version"
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())
if isError {
d.AddError(detail, summary)
} else {
d.AddWarning(detail, summary)
}
}

func (r *resourceWorkspaceRunTask) extractStageAndStages(plan modelTFEWorkspaceRunTaskV1, d *diag.Diagnostics) (*tfe.Stage, []tfe.Stage) {
// There are some complex interactions here between deprecated values in the TF model, and whether the backend server even supports the newer
// API call style. This function attempts to extract the Stage and Stages properties and emit useful diagnostics

// If neither stage or stages is set, then it's all fine, we use the server defaults
if plan.Stage.IsUnknown() && plan.Stages.IsUnknown() {
return nil, nil
}

if r.supportsStagesProperty() {
if plan.Stages.IsUnknown() {
// The user has supplied Stage but not Stages. They would already have received the deprecation warning so just munge
// the stage into a slice and we're fine
stages := []tfe.Stage{tfe.Stage(plan.Stage.ValueString())}
return nil, stages
}

// Convert the plan values into the slice we need
var stageStrings []types.String
if err := plan.Stages.ElementsAs(ctx, &stageStrings, false); err != nil && err.HasError() {
d.Append(err...)
return nil, nil
}
stages := make([]tfe.Stage, len(stageStrings))
for idx, s := range stageStrings {
stages[idx] = tfe.Stage(s.ValueString())
}
return nil, stages
}

// The backend server doesn't support Stages
if !plan.Stages.IsUnknown() {
// The user has supplied a stages array. We need to figure out if we can munge this into a stage attribute
stagesCount := len(plan.Stages.Elements())

if stagesCount > 1 {
// The user has supplied more than one stage so we can't munge this
r.addStageSupportDiag(d, true)
return nil, nil
}

// Send the warning
r.addStageSupportDiag(d, false)

if stagesCount == 0 {
// Somehow we've got no stages listed. Use default server values
return nil, nil
}

// ... Otherwise there's a single Stages value which we can munge into Stage.
var stageStrings []types.String
if err := plan.Stages.ElementsAs(ctx, &stageStrings, false); err != nil && err.HasError() {
d.Append(err...)
return nil, nil
}
stage := tfe.Stage(stageStrings[0].ValueString())
return &stage, nil
}

// The user supplied a Stage value to a server that doesn't support stages
return r.stringPointerToStagePointer(plan.Stage.ValueStringPointer()), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ var resourceWorkspaceRunTaskSchemaV1 = schema.Schema{
)),
Optional: true,
Computed: true,
Default: stringdefault.StaticString(string(tfe.PostPlan)),
Validators: []validator.String{
stringvalidator.OneOf(workspaceRunTaskStages()...),
},
Expand Down
Loading

0 comments on commit 9ff07e2

Please sign in to comment.