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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy