Skip to content

[pull] main from coder:main #104

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 7 commits into from
Jul 21, 2025
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
6 changes: 3 additions & 3 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ reviews:
# Other review settings (only apply if manually requested)
profile: "chill"
request_changes_workflow: false
high_level_summary: true
high_level_summary: false
poem: false
review_status: true
review_status: false
collapse_walkthrough: true
high_level_summary_in_walkthrough: true

chat:
auto_reply: true # Allow automatic chat replies

# Note: With auto_review.enabled: false, CodeRabbit will only perform initial
# reviews when manually requested, but incremental reviews and chat replies remain enabled

30 changes: 29 additions & 1 deletion coderd/coderdtest/dynamicparameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ type DynamicParameterTemplateParams struct {
// TemplateID is used to update an existing template instead of creating a new one.
TemplateID uuid.UUID

Version func(request *codersdk.CreateTemplateVersionRequest)
Version func(request *codersdk.CreateTemplateVersionRequest)
Variables []codersdk.TemplateVersionVariable
}

func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UUID, args DynamicParameterTemplateParams) (codersdk.Template, codersdk.TemplateVersion) {
Expand All @@ -48,6 +49,32 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU
},
}}

userVars := make([]codersdk.VariableValue, 0, len(args.Variables))
parseVars := make([]*proto.TemplateVariable, 0, len(args.Variables))
for _, argv := range args.Variables {
parseVars = append(parseVars, &proto.TemplateVariable{
Name: argv.Name,
Description: argv.Description,
Type: argv.Type,
DefaultValue: argv.DefaultValue,
Required: argv.Required,
Sensitive: argv.Sensitive,
})

userVars = append(userVars, codersdk.VariableValue{
Name: argv.Name,
Value: argv.Value,
})
}

files.Parse = []*proto.Response{{
Type: &proto.Response_Parse{
Parse: &proto.ParseComplete{
TemplateVariables: parseVars,
},
},
}}

mime := codersdk.ContentTypeTar
if args.Zip {
mime = codersdk.ContentTypeZip
Expand All @@ -59,6 +86,7 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU
if args.Version != nil {
args.Version(request)
}
request.UserVariableValues = userVars
})
AwaitTemplateVersionJobCompleted(t, client, version.ID)

Expand Down
31 changes: 27 additions & 4 deletions coderd/dynamicparameters/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/zclconf/go-cty/cty"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/apiversion"
Expand Down Expand Up @@ -41,9 +42,10 @@ type loader struct {
templateVersionID uuid.UUID

// cache of objects
templateVersion *database.TemplateVersion
job *database.ProvisionerJob
terraformValues *database.TemplateVersionTerraformValue
templateVersion *database.TemplateVersion
job *database.ProvisionerJob
terraformValues *database.TemplateVersionTerraformValue
templateVariableValues *[]database.TemplateVersionVariable
}

// Prepare is the entrypoint for this package. It loads the necessary objects &
Expand All @@ -61,6 +63,12 @@ func Prepare(ctx context.Context, db database.Store, cache files.FileAcquirer, v
return l.Renderer(ctx, db, cache)
}

func WithTemplateVariableValues(vals []database.TemplateVersionVariable) func(r *loader) {
return func(r *loader) {
r.templateVariableValues = &vals
}
}

func WithTemplateVersion(tv database.TemplateVersion) func(r *loader) {
return func(r *loader) {
if tv.ID == r.templateVersionID {
Expand Down Expand Up @@ -127,6 +135,14 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
r.terraformValues = &values
}

if r.templateVariableValues == nil {
vals, err := db.GetTemplateVersionVariables(ctx, r.templateVersion.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("template version variables: %w", err)
}
r.templateVariableValues = &vals
}

return nil
}

Expand Down Expand Up @@ -160,13 +176,17 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache *
}
}()

tfVarValues, err := VariableValues(*r.templateVariableValues)
if err != nil {
return nil, xerrors.Errorf("parse variable values: %w", err)
}

// If they can read the template version, then they can read the file for
// parameter loading purposes.
//nolint:gocritic
fileCtx := dbauthz.AsFileReader(ctx)

var templateFS fs.FS
var err error

templateFS, err = cache.Acquire(fileCtx, db, r.job.FileID)
if err != nil {
Expand All @@ -189,6 +209,7 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache *
db: db,
ownerErrors: make(map[uuid.UUID]error),
close: cache.Close,
tfvarValues: tfVarValues,
}, nil
}

Expand All @@ -199,6 +220,7 @@ type dynamicRenderer struct {

ownerErrors map[uuid.UUID]error
currentOwner *previewtypes.WorkspaceOwner
tfvarValues map[string]cty.Value

once sync.Once
close func()
Expand Down Expand Up @@ -229,6 +251,7 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values
PlanJSON: r.data.terraformValues.CachedPlan,
ParameterValues: values,
Owner: *r.currentOwner,
TFVars: r.tfvarValues,
// Do not emit parser logs to coderd output logs.
// TODO: Returning this logs in the output would benefit the caller.
// Unsure how large the logs can be, so for now we just discard them.
Expand Down
65 changes: 65 additions & 0 deletions coderd/dynamicparameters/variablevalues.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dynamicparameters

import (
"strconv"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/json"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/database"
)

// VariableValues is a helper function that converts a slice of TemplateVersionVariable
// into a map of cty.Value for use in coder/preview.
func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Value, error) {
ctyVals := make(map[string]cty.Value, len(vals))
for _, v := range vals {
value := v.Value
if value == "" && v.DefaultValue != "" {
value = v.DefaultValue
}

if value == "" {
// Empty strings are unsupported I guess?
continue // omit non-set vals
}

var err error
switch v.Type {
// Defaulting the empty type to "string"
// TODO: This does not match the terraform behavior, however it is too late
// at this point in the code to determine this, as the database type stores all values
// as strings. The code needs to be fixed in the `Parse` step of the provisioner.
// That step should determine the type of the variable correctly and store it in the database.
case "string", "":
ctyVals[v.Name] = cty.StringVal(value)
case "number":
ctyVals[v.Name], err = cty.ParseNumberVal(value)
if err != nil {
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
}
case "bool":
parsed, err := strconv.ParseBool(value)
if err != nil {
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
}
ctyVals[v.Name] = cty.BoolVal(parsed)
default:
// If it is a complex type, let the cty json code give it a try.
// TODO: Ideally we parse `list` & `map` and build the type ourselves.
ty, err := json.ImpliedType([]byte(value))
if err != nil {
return nil, xerrors.Errorf("implied type for variable %q: %w", v.Name, err)
}

jv, err := json.Unmarshal([]byte(value), ty)
if err != nil {
return nil, xerrors.Errorf("unmarshal variable %q: %w", v.Name, err)
}
ctyVals[v.Name] = jv
}
}

return ctyVals, nil
}
32 changes: 32 additions & 0 deletions coderd/parameters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,36 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
require.Len(t, preview.Diagnostics, 1)
require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found")
})

t.Run("TemplateVariables", func(t *testing.T) {
t.Parallel()

dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/variables/main.tf")
require.NoError(t, err)

setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
variables: []codersdk.TemplateVersionVariable{
{Name: "one", Value: "austin", DefaultValue: "alice", Type: "string"},
},
plan: nil,
static: nil,
})

ctx := testutil.Context(t, testutil.WaitShort)
stream := setup.stream
previews := stream.Chan()

// Should see the output of the module represented
preview := testutil.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)

require.Len(t, preview.Parameters, 1)
coderdtest.AssertParameter(t, "variable_values", preview.Parameters).
Exists().Value("austin")
})
}

type setupDynamicParamsTestParams struct {
Expand All @@ -355,6 +385,7 @@ type setupDynamicParamsTestParams struct {

static []*proto.RichParameter
expectWebsocketError bool
variables []codersdk.TemplateVersionVariable
}

type dynamicParamsTest struct {
Expand All @@ -380,6 +411,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
Plan: args.plan,
ModulesArchive: args.modulesArchive,
StaticParams: args.static,
Variables: args.variables,
})

ctx := testutil.Context(t, testutil.WaitShort)
Expand Down
13 changes: 11 additions & 2 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/google/uuid"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/sqlc-dev/pqtype"
"github.com/zclconf/go-cty/cty"
"golang.org/x/xerrors"

"cdr.dev/slog"
Expand Down Expand Up @@ -1585,7 +1586,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
var parsedTags map[string]string
var ok bool
if dynamicTemplate {
parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file)
parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file, req.UserVariableValues)
if !ok {
return
}
Expand Down Expand Up @@ -1762,7 +1763,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
warnings))
}

func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.ResponseWriter, orgID uuid.UUID, owner uuid.UUID, file database.File) (map[string]string, bool) {
func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.ResponseWriter, orgID uuid.UUID, owner uuid.UUID, file database.File, templateVariables []codersdk.VariableValue) (map[string]string, bool) {
ownerData, err := dynamicparameters.WorkspaceOwner(ctx, api.Database, orgID, owner)
if err != nil {
if httpapi.Is404Error(err) {
Expand Down Expand Up @@ -1800,11 +1801,19 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response
return nil, false
}

// Pass in any manually specified template variables as TFVars.
// TODO: Does this break if the type is not a string?
tfVarValues := make(map[string]cty.Value)
for _, variable := range templateVariables {
tfVarValues[variable.Name] = cty.StringVal(variable.Value)
}

output, diags := preview.Preview(ctx, preview.Input{
PlanJSON: nil, // Template versions are before `terraform plan`
ParameterValues: nil, // No user-specified parameters
Owner: *ownerData,
Logger: stdslog.New(stdslog.DiscardHandler),
TFVars: tfVarValues,
}, files)
tagErr := dynamicparameters.CheckTags(output, diags)
if tagErr != nil {
Expand Down
30 changes: 30 additions & 0 deletions coderd/testdata/parameters/variables/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Base case for workspace tags + parameters.
terraform {
required_providers {
coder = {
source = "coder/coder"
}
docker = {
source = "kreuzwerker/docker"
version = "3.0.2"
}
}
}

variable "one" {
default = "alice"
type = string
}


data "coder_parameter" "variable_values" {
name = "variable_values"
description = "Just to show the variable values"
type = "string"
default = var.one

option {
name = "one"
value = var.one
}
}
6 changes: 6 additions & 0 deletions coderd/wsbuilder/wsbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,16 @@ func (b *Builder) getDynamicParameterRenderer() (dynamicparameters.Renderer, err
return nil, xerrors.Errorf("get template version terraform values: %w", err)
}

variableValues, err := b.getTemplateVersionVariables()
if err != nil {
return nil, xerrors.Errorf("get template version variables: %w", err)
}

renderer, err := dynamicparameters.Prepare(b.ctx, b.store, b.fileCache, tv.ID,
dynamicparameters.WithTemplateVersion(*tv),
dynamicparameters.WithProvisionerJob(*job),
dynamicparameters.WithTerraformValues(*tfVals),
dynamicparameters.WithTemplateVariableValues(variableValues),
)
if err != nil {
return nil, xerrors.Errorf("get template version renderer: %w", err)
Expand Down
Loading
Loading