8000 chore: pass previous values into terraform apply by Emyrk · Pull Request #17696 · coder/coder · GitHub
[go: up one dir, main page]

Skip to content

chore: pass previous values into terraform apply #17696

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

Merged
merged 23 commits into from
May 12, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
chore: pass previous values into the terraform apply
remove custom monotonic check
  • Loading branch information
Emyrk committed May 6, 2025
commit d565cb57797bb8ed46a4b1fb668cf3b8da3becfb
35 changes: 29 additions & 6 deletions coderd/provisionerdserver/provisionerdserver.go
1E79
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,28 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo
return nil, failJob(fmt.Sprintf("convert workspace transition: %s", err))
}

// A previous workspace build exists
var lastWorkspaceBuildParameters []database.WorkspaceBuildParameter
if workspaceBuild.BuildNumber > 1 {
// TODO: Should we fetch the last build that succeeded? This fetches the
// previous build regardless of the status of the build.
Comment on lines +555 to +556
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we check for the last successful build, we could end up with no builds. What do we do then? Do we just settle for the last build? IMO just checking the previous build is simpler conceptually, and is more likely to be what users expect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, the wsbuilder just takes the last build regardless of status. Just feels a bit off since the tfstate is different if the previous failed. 🤷‍♂️

buildNum := workspaceBuild.BuildNumber - 1
previous, err := s.Database.GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx, database.GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams{
WorkspaceID: workspaceBuild.WorkspaceID,
BuildNumber: buildNum,
})
Comment on lines +555 to +561
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What problem are we actually trying to solve here?

wsbuilder already fetches the last build parameters, if they exist:

func (b *Builder) getLastBuildParameters() ([]database.WorkspaceBuildParameter, error) {
if b.lastBuildParameters != nil {
return *b.lastBuildParameters, nil
}
bld, err := b.getLastBuild()
if xerrors.Is(err, sql.ErrNoRows) {
// if the build doesn't exist, then clearly there can be no parameters.
b.lastBuildParameters = &[]database.WorkspaceBuildParameter{}
return *b.lastBuildParameters, nil
}
if err != nil {
return nil, xerrors.Errorf("get last build to get parameters: %w", err)
}
values, err := b.store.GetWorkspaceBuildParameters(b.ctx, bld.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get last build %s parameters: %w", bld.ID, err)
}
b.lastBuildParameters = &values
return values, nil
}

Given that this is the case, why do we need to do this extra work for all jobs? Isn't this just for template version import jobs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does it in coder/coder at workspace create yes, but this passes the previous values to the terraform via env vars.

The terraform provider now enforces monotonicity: coder/terraform-provider-coder#392

So this is duplicating that check in wsbuilder at terraform apply/plan.
For dynamic parameters, we skip validating params in wsbuilder, so we need to make sure validation is applied in terraform

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I could pass the values from wsbuilder to here via the job? Rather than refetch

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that approach -- wsbuilder is then still responsible for fetching all of the various baggage related to a workspace build, but just defers the validation part to Terraform.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnstcn actually, as much as I'd like to have wsbuilder be the source of truth. We fetch everything again at this step in the workspace build.

Current params, workspace data, external auth, etc.

We store very little in the job payload:

type WorkspaceProvisionJob struct {
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
DryRun bool `json:"dry_run"`
LogLevel string `json:"log_level,omitempty"`
PrebuiltWorkspaceBuildStage sdkproto.PrebuiltWorkspaceBuildStage `json:"prebuilt_workspace_stage,omitempty"`
}

So I'm going to keep this as a refetching. Ideally I would use the same function to fetch the previous params in both cases, however at wsbuild the latestbuild is the "previous". And at the point I added code, the previous is build -1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh... gotcha. That's unfortunate :(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, I think it would be a large refactor to move all the fields into wsbuilder

if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get last build with number=%d: %w", buildNum, err)
}

if err == nil {
lastWorkspaceBuildParameters, err = s.Database.GetWorkspaceBuildParameters(ctx, previous.ID)
if err != nil {
return nil, xerrors.Errorf("get last build parameters %q: %w", previous.ID, err)
}
}
}

workspaceBuildParameters, err := s.Database.GetWorkspaceBuildParameters(ctx, workspaceBuild.ID)
if err != nil {
return nil, failJob(fmt.Sprintf("get workspace build parameters: %s", err))
Expand Down Expand Up @@ -619,12 +641,13 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo

protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{
WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
VariableValues: asVariableValues(templateVariables),
ExternalAuthProviders: externalAuthProviders,
WorkspaceBuildId: workspaceBuild.ID.String(),
WorkspaceName: workspace.Name,
State: workspaceBuild.ProvisionerState,
RichParameterValues: convertRichParameterValues(workspaceBuildParameters),
PreviousParameterValues: convertRichParameterValues(lastWorkspaceBuildParameters),
VariableValues: asVariableValues(templateVariables),
ExternalAuthProviders: externalAuthProviders,
Metadata: &sdkproto.Metadata{
CoderUrl: s.AccessURL.String(),
WorkspaceTransition: transition,
Expand Down
33 changes: 7 additions & 26 deletions codersdk/richparameters.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package codersdk

import (
"strconv"

"golang.org/x/xerrors"

"github.com/coder/terraform-provider-coder/v2/provider"
Expand Down Expand Up @@ -60,29 +58,6 @@ func validateBuildParameter(richParameter TemplateVersionParameter, buildParamet
value = richParameter.DefaultValue
}

if lastBuildParameter != nil && lastBuildParameter.Value != "" && richParameter.Type == "number" && len(richParameter.ValidationMonotonic) > 0 {
prev, err := strconv.Atoi(lastBuildParameter.Value)
if err != nil {
return xerrors.Errorf("previous parameter value is not a number: %s", lastBuildParameter.Value)
}

current, err := strconv.Atoi(buildParameter.Value)
if err != nil {
return xerrors.Errorf("current parameter value is not a number: %s", buildParameter.Value)
}

switch richParameter.ValidationMonotonic {
case MonotonicOrderIncreasing:
if prev > current {
return xerrors.Errorf("parameter value must be equal or greater than previous value: %d", prev)
}
case MonotonicOrderDecreasing:
if prev < current {
return xerrors.Errorf("parameter value must be equal or lower than previous value: %d", prev)
}
}
}

if len(richParameter.Options) > 0 {
var matched bool
for _, opt := range richParameter.Options {
Expand Down Expand Up @@ -119,7 +94,13 @@ func validateBuildParameter(richParameter TemplateVersionParameter, buildParamet
Error: richParameter.ValidationError,
Monotonic: string(richParameter.ValidationMonotonic),
}
return validation.Valid(richParameter.Type, value)
var prev *string
// Empty strings should be rejected, however the previous behavior was to
// accept the empty string ("") as a `nil` previous value.
if lastBuildParameter != nil && lastBuildParameter.Value != "" {
prev = &lastBuildParameter.Value
}
return validation.Valid(richParameter.Type, value, prev)
}

func findBuildParameter(params []WorkspaceBuildParameter, parameterName string) (*WorkspaceBuildParameter, bool) {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ require (
github.com/coder/quartz v0.1.2
github.com/coder/retry v1.5.1
github.com/coder/serpent v0.10.0
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27
github.com/coder/websocket v1.8.13
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0
github.com/coreos/go-oidc/v3 v3.14.1
Expand Down Expand Up @@ -488,7 +488,7 @@ require (

require (
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3
github.com/coder/preview v0.0.1
github.com/coder/preview v0.0.2-0.20250506195323-154d86b5a92a
github.com/fsnotify/fsnotify v1.9.0
github.com/kylecarbs/aisdk-go v0.0.8
github.com/mark3labs/mcp-go v0.25.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -907,8 +907,8 @@ github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggX
github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/preview v0.0.1 h1:2X5McKdMOZJILTIDf7qRplXKupT+91qTJBN67XUh5cA=
github.com/coder/preview v0.0.1/go.mod h1:eInDmOdSDF8cxCvapIvYkGRzmzvcvGAFL1HYqcA4g+E=
github.com/coder/preview v0.0.2-0.20250506195323-154d86b5a92a h1:tmtq3YgYE69PKA7n10WRFZPQnLp45N3jPOcpr4Ki6z4=
github.com/coder/preview v0.0.2-0.20250506195323-154d86b5a92a/go.mod h1:j2JOd9aN+pGLxBxOawtm+1pF3kgWdbn5xGvnDSqER+8=
github.com/coder/quartz v0.1.2 h1:PVhc9sJimTdKd3VbygXtS4826EOCpB1fXoRlLnCrE+s=
github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
Expand All @@ -921,8 +921,8 @@ github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e h1:nope/SZfoLB9M
github.com/coder/tailscale v1.1.1-0.20250422090654-5090e715905e/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd h1:FsIG6Fd0YOEK7D0Hl/CJywRA+Y6Gd5RQbSIa2L+/BmE=
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250417100258-c86bb5c3ddcd/go.mod h1:56/KdGYaA+VbwXJbTI8CA57XPfnuTxN8rjxbR34PbZw=
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27 h1:CLJwMqst39+wfFehYQzVOiG5uXUtC5fbAZ3/EpxOWos=
github.com/coder/terraform-provider-coder/v2 v2.4.0-pre1.0.20250506184715-e011f733bf27/go.mod h1:2kaBpn5k9ZWtgKq5k4JbkVZG9DzEqR4mJSmpdshcO+s=
github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a h1:yryP7e+IQUAArlycH4hQrjXQ64eRNbxsV5/wuVXHgME=
github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a/go.mod h1:dDvq9axp3kZsT63gY2Znd1iwzfqDq3kXbQnccIrjRYY=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
Expand Down
9 changes: 6 additions & 3 deletions provisioner/terraform/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func (s *server) Plan(

s.logger.Debug(ctx, "ran initialization")

env, err := provisionEnv(sess.Config, request.Metadata, request.RichParameterValues, request.ExternalAuthProviders)
env, err := provisionEnv(sess.Config, request.Metadata, request.PreviousParameterValues, request.RichParameterValues, request.ExternalAuthProviders)
if err != nil {
return provisionersdk.PlanErrorf("setup env: %s", err)
}
Expand Down Expand Up @@ -205,7 +205,7 @@ func (s *server) Apply(

// Earlier in the session, Plan() will have written the state file and the plan file.
statefilePath := getStateFilePath(sess.WorkDirectory)
env, err := provisionEnv(sess.Config, request.Metadata, nil, nil)
env, err := provisionEnv(sess.Config, request.Metadata, nil, nil, nil)
if err != nil {
return provisionersdk.ApplyErrorf("provision env: %s", err)
}
Expand Down Expand Up @@ -236,7 +236,7 @@ func planVars(plan *proto.PlanRequest) ([]string, error) {

func provisionEnv(
config *proto.Config, metadata *proto.Metadata,
richParams []*proto.RichParameterValue, externalAuth []*proto.ExternalAuthProvider,
previousParams, richParams []*proto.RichParameterValue, externalAuth []*proto.ExternalAuthProvider,
) ([]string, error) {
env := safeEnviron()
ownerGroups, err := json.Marshal(metadata.GetWorkspaceOwnerGroups())
Expand Down Expand Up @@ -277,6 +277,9 @@ func provisionEnv(
for key, value := range provisionersdk.AgentScriptEnv() {
env = append(env, key+"="+value)
}
for _, param := range previousParams {
env = append(env, provider.ParameterEnvironmentVariablePrevious(param.Name)+"="+param.Value)
}
for _, param := range richParams {
env = append(env, provider.ParameterEnvironmentVariable(param.Name)+"="+param.Value)
}
Expand Down
6 changes: 5 additions & 1 deletion provisioner/terraform/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -749,13 +749,17 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
if err != nil {
return nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
}
def := ""
if param.Default != nil {
def = *param.Default
}
protoParam := &proto.RichParameter{
Name: param.Name,
DisplayName: param.DisplayName,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.Default,
DefaultValue: def,
Icon: param.Icon,
Required: !param.Optional,
// #nosec G115 - Safe conversion as parameter order value is expected to be within int32 range
Expand Down
Loading
Loading
0