Skip to content

Commit aedc019

Browse files
authored
feat: include template variables in dynamic parameter rendering (#18819)
Closes #18671 Template variables now loaded into dynamic parameters.
1 parent 40a6367 commit aedc019

File tree

11 files changed

+328
-80
lines changed

11 files changed

+328
-80
lines changed

coderd/coderdtest/dynamicparameters.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ type DynamicParameterTemplateParams struct {
2929
// TemplateID is used to update an existing template instead of creating a new one.
3030
TemplateID uuid.UUID
3131

32-
Version func(request *codersdk.CreateTemplateVersionRequest)
32+
Version func(request *codersdk.CreateTemplateVersionRequest)
33+
Variables []codersdk.TemplateVersionVariable
3334
}
3435

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

52+
userVars := make([]codersdk.VariableValue, 0, len(args.Variables))
53+
parseVars := make([]*proto.TemplateVariable, 0, len(args.Variables))
54+
for _, argv := range args.Variables {
55+
parseVars = append(parseVars, &proto.TemplateVariable{
56+
Name: argv.Name,
57+
Description: argv.Description,
58+
Type: argv.Type,
59+
DefaultValue: argv.DefaultValue,
60+
Required: argv.Required,
61+
Sensitive: argv.Sensitive,
62+
})
63+
64+
userVars = append(userVars, codersdk.VariableValue{
65+
Name: argv.Name,
66+
Value: argv.Value,
67+
})
68+
}
69+
70+
files.Parse = []*proto.Response{{
71+
Type: &proto.Response_Parse{
72+
Parse: &proto.ParseComplete{
73+
TemplateVariables: parseVars,
74+
},
75+
},
76+
}}
77+
5178
mime := codersdk.ContentTypeTar
5279
if args.Zip {
5380
mime = codersdk.ContentTypeZip
@@ -59,6 +86,7 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU
5986
if args.Version != nil {
6087
args.Version(request)
6188
}
89+
request.UserVariableValues = userVars
6290
})
6391
AwaitTemplateVersionJobCompleted(t, client, version.ID)
6492

coderd/dynamicparameters/render.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/google/uuid"
12+
"github.com/zclconf/go-cty/cty"
1213
"golang.org/x/xerrors"
1314

1415
"github.com/coder/coder/v2/apiversion"
@@ -41,9 +42,10 @@ type loader struct {
4142
templateVersionID uuid.UUID
4243

4344
// cache of objects
44-
templateVersion *database.TemplateVersion
45-
job *database.ProvisionerJob
46-
terraformValues *database.TemplateVersionTerraformValue
45+
templateVersion *database.TemplateVersion
46+
job *database.ProvisionerJob
47+
terraformValues *database.TemplateVersionTerraformValue
48+
templateVariableValues *[]database.TemplateVersionVariable
4749
}
4850

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

66+
func WithTemplateVariableValues(vals []database.TemplateVersionVariable) func(r *loader) {
67+
return func(r *loader) {
68+
r.templateVariableValues = &vals
69+
}
70+
}
71+
6472
func WithTemplateVersion(tv database.TemplateVersion) func(r *loader) {
6573
return func(r *loader) {
6674
if tv.ID == r.templateVersionID {
@@ -127,6 +135,14 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
127135
r.terraformValues = &values
128136
}
129137

138+
if r.templateVariableValues == nil {
139+
vals, err := db.GetTemplateVersionVariables(ctx, r.templateVersion.ID)
140+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
141+
return xerrors.Errorf("template version variables: %w", err)
142+
}
143+
r.templateVariableValues = &vals
144+
}
145+
130146
return nil
131147
}
132148

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

179+
tfVarValues, err := VariableValues(*r.templateVariableValues)
180+
if err != nil {
181+
return nil, xerrors.Errorf("parse variable values: %w", err)
182+
}
183+
163184
// If they can read the template version, then they can read the file for
164185
// parameter loading purposes.
165186
//nolint:gocritic
166187
fileCtx := dbauthz.AsFileReader(ctx)
167188

168189
var templateFS fs.FS
169-
var err error
170190

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

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

200221
ownerErrors map[uuid.UUID]error
201222
currentOwner *previewtypes.WorkspaceOwner
223+
tfvarValues map[string]cty.Value
202224

203225
once sync.Once
204226
close func()
@@ -229,6 +251,7 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values
229251
PlanJSON: r.data.terraformValues.CachedPlan,
230252
ParameterValues: values,
231253
Owner: *r.currentOwner,
254+
TFVars: r.tfvarValues,
232255
// Do not emit parser logs to coderd output logs.
233256
// TODO: Returning this logs in the output would benefit the caller.
234257
// Unsure how large the logs can be, so for now we just discard them.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package dynamicparameters
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/zclconf/go-cty/cty"
7+
"github.com/zclconf/go-cty/cty/json"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/coderd/database"
11+
)
12+
13+
// VariableValues is a helper function that converts a slice of TemplateVersionVariable
14+
// into a map of cty.Value for use in coder/preview.
15+
func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Value, error) {
16+
ctyVals := make(map[string]cty.Value, len(vals))
17+
for _, v := range vals {
18+
value := v.Value
19+
if value == "" && v.DefaultValue != "" {
20+
value = v.DefaultValue
21+
}
22+
23+
if value == "" {
24+
// Empty strings are unsupported I guess?
25+
continue // omit non-set vals
26+
}
27+
28+
var err error
29+
switch v.Type {
30+
// Defaulting the empty type to "string"
31+
// TODO: This does not match the terraform behavior, however it is too late
32+
// at this point in the code to determine this, as the database type stores all values
33+
// as strings. The code needs to be fixed in the `Parse` step of the provisioner.
34+
// That step should determine the type of the variable correctly and store it in the database.
35+
case "string", "":
36+
ctyVals[v.Name] = cty.StringVal(value)
37+
case "number":
38+
ctyVals[v.Name], err = cty.ParseNumberVal(value)
39+
if err != nil {
40+
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
41+
}
42+
case "bool":
43+
parsed, err := strconv.ParseBool(value)
44+
if err != nil {
45+
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
46+
}
47+
ctyVals[v.Name] = cty.BoolVal(parsed)
48+
default:
49+
// If it is a complex type, let the cty json code give it a try.
50+
// TODO: Ideally we parse `list` & `map` and build the type ourselves.
51+
ty, err := json.ImpliedType([]byte(value))
52+
if err != nil {
53+
return nil, xerrors.Errorf("implied type for variable %q: %w", v.Name, err)
54+
}
55+
56+
jv, err := json.Unmarshal([]byte(value), ty)
57+
if err != nil {
58+
return nil, xerrors.Errorf("unmarshal variable %q: %w", v.Name, err)
59+
}
60+
ctyVals[v.Name] = jv
61+
}
62+
}
63+
64+
return ctyVals, nil
65+
}

coderd/parameters_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,36 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
343343
require.Len(t, preview.Diagnostics, 1)
344344
require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found")
345345
})
346+
347+
t.Run("TemplateVariables", func(t *testing.T) {
348+
t.Parallel()
349+
350+
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/variables/main.tf")
351+
require.NoError(t, err)
352+
353+
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
354+
provisionerDaemonVersion: provProto.CurrentVersion.String(),
355+
mainTF: dynamicParametersTerraformSource,
356+
variables: []codersdk.TemplateVersionVariable{
357+
{Name: "one", Value: "austin", DefaultValue: "alice", Type: "string"},
358+
},
359+
plan: nil,
360+
static: nil,
361+
})
362+
363+
ctx := testutil.Context(t, testutil.WaitShort)
364+
stream := setup.stream
365+
previews := stream.Chan()
366+
367+
// Should see the output of the module represented
368+
preview := testutil.RequireReceive(ctx, t, previews)
369+
require.Equal(t, -1, preview.ID)
370+
require.Empty(t, preview.Diagnostics)
371+
372+
require.Len(t, preview.Parameters, 1)
373+
coderdtest.AssertParameter(t, "variable_values", preview.Parameters).
374+
Exists().Value("austin")
375+
})
346376
}
347377

348378
type setupDynamicParamsTestParams struct {
@@ -355,6 +385,7 @@ type setupDynamicParamsTestParams struct {
355385

356386
static []*proto.RichParameter
357387
expectWebsocketError bool
388+
variables []codersdk.TemplateVersionVariable
358389
}
359390

360391
type dynamicParamsTest struct {
@@ -380,6 +411,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
380411
Plan: args.plan,
381412
ModulesArchive: args.modulesArchive,
382413
StaticParams: args.static,
414+
Variables: args.variables,
383415
})
384416

385417
ctx := testutil.Context(t, testutil.WaitShort)

coderd/templateversions.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/google/uuid"
1919
"github.com/moby/moby/pkg/namesgenerator"
2020
"github.com/sqlc-dev/pqtype"
21+
"github.com/zclconf/go-cty/cty"
2122
"golang.org/x/xerrors"
2223

2324
"cdr.dev/slog"
@@ -1585,7 +1586,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
15851586
var parsedTags map[string]string
15861587
var ok bool
15871588
if dynamicTemplate {
1588-
parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file)
1589+
parsedTags, ok = api.dynamicTemplateVersionTags(ctx, rw, organization.ID, apiKey.UserID, file, req.UserVariableValues)
15891590
if !ok {
15901591
return
15911592
}
@@ -1762,7 +1763,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
17621763
warnings))
17631764
}
17641765

1765-
func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.ResponseWriter, orgID uuid.UUID, owner uuid.UUID, file database.File) (map[string]string, bool) {
1766+
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) {
17661767
ownerData, err := dynamicparameters.WorkspaceOwner(ctx, api.Database, orgID, owner)
17671768
if err != nil {
17681769
if httpapi.Is404Error(err) {
@@ -1800,11 +1801,19 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response
18001801
return nil, false
18011802
}
18021803

1804+
// Pass in any manually specified template variables as TFVars.
1805+
// TODO: Does this break if the type is not a string?
1806+
tfVarValues := make(map[string]cty.Value)
1807+
for _, variable := range templateVariables {
1808+
tfVarValues[variable.Name] = cty.StringVal(variable.Value)
1809+
}
1810+
18031811
output, diags := preview.Preview(ctx, preview.Input{
18041812
PlanJSON: nil, // Template versions are before `terraform plan`
18051813
ParameterValues: nil, // No user-specified parameters
18061814
Owner: *ownerData,
18071815
Logger: stdslog.New(stdslog.DiscardHandler),
1816+
TFVars: tfVarValues,
18081817
}, files)
18091818
tagErr := dynamicparameters.CheckTags(output, diags)
18101819
if tagErr != nil {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Base case for workspace tags + parameters.
2+
terraform {
3+
required_providers {
4+
coder = {
5+
source = "coder/coder"
6+
}
7+
docker = {
8+
source = "kreuzwerker/docker"
9+
version = "3.0.2"
10+
}
11+
}
12+
}
13+
14+
variable "one" {
15+
default = "alice"
16+
type = string
17+
}
18+
19+
20+
data "coder_parameter" "variable_values" {
21+
name = "variable_values"
22+
description = "Just to show the variable values"
23+
type = "string"
24+
default = var.one
25+
26+
option {
27+
name = "one"
28+
value = var.one
29+
}
30+
}

coderd/wsbuilder/wsbuilder.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,10 +633,16 @@ func (b *Builder) getDynamicParameterRenderer() (dynamicparameters.Renderer, err
633633
return nil, xerrors.Errorf("get template version terraform values: %w", err)
634634
}
635635

636+
variableValues, err := b.getTemplateVersionVariables()
637+
if err != nil {
638+
return nil, xerrors.Errorf("get template version variables: %w", err)
639+
}
640+
636641
renderer, err := dynamicparameters.Prepare(b.ctx, b.store, b.fileCache, tv.ID,
637642
dynamicparameters.WithTemplateVersion(*tv),
638643
dynamicparameters.WithProvisionerJob(*job),
639644
dynamicparameters.WithTerraformValues(*tfVals),
645+
dynamicparameters.WithTemplateVariableValues(variableValues),
640646
)
641647
if err != nil {
642648
return nil, xerrors.Errorf("get template version renderer: %w", err)

0 commit comments

Comments
 (0)