Skip to content
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

Use the stages attribute for the Workspace Run Task resource #1459

Merged
merged 6 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading