mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: include template variables in dynamic parameter rendering (#18819)
Closes https://github.com/coder/coder/issues/18671 Template variables now loaded into dynamic parameters.
This commit is contained in:
@@ -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) {
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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 &
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -355,6 +385,7 @@ type setupDynamicParamsTestParams struct {
|
||||
|
||||
static []*proto.RichParameter
|
||||
expectWebsocketError bool
|
||||
variables []codersdk.TemplateVersionVariable
|
||||
}
|
||||
|
||||
type dynamicParamsTest struct {
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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 {
|
||||
|
||||
+30
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user