Skip to content
Merged
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)
Comment on lines +30 to +36
Copy link
Member Author

Choose a reason for hiding this comment

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

This is not ideal, will resolve in another PR

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)
Copy link
Member

Choose a reason for hiding this comment

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

AFAICT VariableValue.Value will always be a string?

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, which I just found strange. This is working for now, I will test complex behavior as I edit the source of this (the parse function)

}

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