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)
|
||||
|
||||
@@ -2627,6 +2627,21 @@ func TestWorkspaceTemplateParamsChange(t *testing.T) {
|
||||
require.Equal(t, codersdk.WorkspaceStatusDeleted, build.Status)
|
||||
}
|
||||
|
||||
type testWorkspaceTagsTerraformCase struct {
|
||||
name string
|
||||
// tags to apply to the external provisioner
|
||||
provisionerTags map[string]string
|
||||
// tags to apply to the create template version request
|
||||
createTemplateVersionRequestTags map[string]string
|
||||
// the coder_workspace_tags bit of main.tf.
|
||||
// you can add more stuff here if you need
|
||||
tfWorkspaceTags string
|
||||
templateImportUserVariableValues []codersdk.VariableValue
|
||||
// if we need to set parameters on workspace build
|
||||
workspaceBuildParameters []codersdk.WorkspaceBuildParameter
|
||||
skipCreateWorkspace bool
|
||||
}
|
||||
|
||||
// TestWorkspaceTagsTerraform tests that a workspace can be created with tags.
|
||||
// This is an end-to-end-style test, meaning that we actually run the
|
||||
// real Terraform provisioner and validate that the workspace is created
|
||||
@@ -2636,7 +2651,7 @@ func TestWorkspaceTemplateParamsChange(t *testing.T) {
|
||||
// config file so that we only reference those
|
||||
// nolint:paralleltest // t.Setenv
|
||||
func TestWorkspaceTagsTerraform(t *testing.T) {
|
||||
mainTfTemplate := `
|
||||
coderProviderTemplate := `
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
@@ -2644,33 +2659,11 @@ func TestWorkspaceTagsTerraform(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
provider "coder" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_parameter" "unrelated" {
|
||||
name = "unrelated"
|
||||
type = "list(string)"
|
||||
default = jsonencode(["a", "b"])
|
||||
}
|
||||
%s
|
||||
`
|
||||
tfCliConfigPath := downloadProviders(t, fmt.Sprintf(mainTfTemplate, ""))
|
||||
tfCliConfigPath := downloadProviders(t, coderProviderTemplate)
|
||||
t.Setenv("TF_CLI_CONFIG_FILE", tfCliConfigPath)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
// tags to apply to the external provisioner
|
||||
provisionerTags map[string]string
|
||||
// tags to apply to the create template version request
|
||||
createTemplateVersionRequestTags map[string]string
|
||||
// the coder_workspace_tags bit of main.tf.
|
||||
// you can add more stuff here if you need
|
||||
tfWorkspaceTags string
|
||||
templateImportUserVariableValues []codersdk.VariableValue
|
||||
// if we need to set parameters on workspace build
|
||||
workspaceBuildParameters []codersdk.WorkspaceBuildParameter
|
||||
skipCreateWorkspace bool
|
||||
}{
|
||||
for _, tc := range []testWorkspaceTagsTerraformCase{
|
||||
{
|
||||
name: "no tags",
|
||||
tfWorkspaceTags: ``,
|
||||
@@ -2803,56 +2796,114 @@ func TestWorkspaceTagsTerraform(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
// We intentionally do not run a built-in provisioner daemon here.
|
||||
IncludeProvisionerDaemon: false,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
},
|
||||
},
|
||||
t.Run("dynamic", func(t *testing.T) {
|
||||
workspaceTagsTerraform(t, tc, true)
|
||||
})
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
_ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags)
|
||||
|
||||
// This can take a while, so set a relatively long timeout.
|
||||
ctx := testutil.Context(t, 2*testutil.WaitSuperLong)
|
||||
|
||||
// Creating a template as a template admin must succeed
|
||||
templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)}
|
||||
tarBytes := testutil.CreateTar(t, templateFiles)
|
||||
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes))
|
||||
require.NoError(t, err, "failed to upload file")
|
||||
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
Name: testutil.GetRandomName(t),
|
||||
FileID: fi.ID,
|
||||
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||
Provisioner: codersdk.ProvisionerTypeTerraform,
|
||||
ProvisionerTags: tc.createTemplateVersionRequestTags,
|
||||
UserVariableValues: tc.templateImportUserVariableValues,
|
||||
// classic uses tfparse for tags. This sub test can be
|
||||
// removed when tf parse is removed.
|
||||
t.Run("classic", func(t *testing.T) {
|
||||
workspaceTagsTerraform(t, tc, false)
|
||||
})
|
||||
require.NoError(t, err, "failed to create template version")
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID)
|
||||
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID)
|
||||
|
||||
if !tc.skipCreateWorkspace {
|
||||
// Creating a workspace as a non-privileged user must succeed
|
||||
ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: tpl.ID,
|
||||
Name: coderdtest.RandomUsername(t),
|
||||
RichParameterValues: tc.workspaceBuildParameters,
|
||||
})
|
||||
require.NoError(t, err, "failed to create workspace")
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func workspaceTagsTerraform(t *testing.T, tc testWorkspaceTagsTerraformCase, dynamic bool) {
|
||||
mainTfTemplate := `
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_parameter" "unrelated" {
|
||||
name = "unrelated"
|
||||
type = "list(string)"
|
||||
default = jsonencode(["a", "b"])
|
||||
}
|
||||
%s
|
||||
`
|
||||
|
||||
client, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
// We intentionally do not run a built-in provisioner daemon here.
|
||||
IncludeProvisionerDaemon: false,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureExternalProvisionerDaemons: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
// This can take a while, so set a relatively long timeout.
|
||||
ctx := testutil.Context(t, 2*testutil.WaitSuperLong)
|
||||
|
||||
emptyTar := testutil.CreateTar(t, map[string]string{"main.tf": ""})
|
||||
emptyFi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(emptyTar))
|
||||
require.NoError(t, err)
|
||||
|
||||
// This template version does not need to succeed in being created.
|
||||
// It will be in pending forever. We just need it to create a template.
|
||||
emptyTv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
Name: testutil.GetRandomName(t),
|
||||
FileID: emptyFi.ID,
|
||||
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||
Provisioner: codersdk.ProvisionerTypeTerraform,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, emptyTv.ID, func(request *codersdk.CreateTemplateRequest) {
|
||||
request.UseClassicParameterFlow = ptr.Ref(!dynamic)
|
||||
})
|
||||
|
||||
// The provisioner for the next template version
|
||||
_ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags)
|
||||
|
||||
// Creating a template as a template admin must succeed
|
||||
templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)}
|
||||
tarBytes := testutil.CreateTar(t, templateFiles)
|
||||
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes))
|
||||
require.NoError(t, err, "failed to upload file")
|
||||
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
|
||||
Name: testutil.GetRandomName(t),
|
||||
FileID: fi.ID,
|
||||
StorageMethod: codersdk.ProvisionerStorageMethodFile,
|
||||
Provisioner: codersdk.ProvisionerTypeTerraform,
|
||||
ProvisionerTags: tc.createTemplateVersionRequestTags,
|
||||
UserVariableValues: tc.templateImportUserVariableValues,
|
||||
TemplateID: tpl.ID,
|
||||
})
|
||||
require.NoError(t, err, "failed to create template version")
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID)
|
||||
|
||||
err = templateAdmin.UpdateActiveTemplateVersion(ctx, tpl.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: tv.ID,
|
||||
})
|
||||
require.NoError(t, err, "set to active template version")
|
||||
|
||||
if !tc.skipCreateWorkspace {
|
||||
// Creating a workspace as a non-privileged user must succeed
|
||||
ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{
|
||||
TemplateID: tpl.ID,
|
||||
Name: coderdtest.RandomUsername(t),
|
||||
RichParameterValues: tc.workspaceBuildParameters,
|
||||
})
|
||||
require.NoError(t, err, "failed to create workspace")
|
||||
tagJSON, _ := json.Marshal(ws.LatestBuild.Job.Tags)
|
||||
t.Logf("Created workspace build [%s] with tags: %s", ws.LatestBuild.Job.Type, tagJSON)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// downloadProviders is a test helper that creates a temporary file and writes a
|
||||
// terraform CLI config file with a provider_installation stanza for coder/coder
|
||||
// using dev_overrides. It also fetches the latest provider release from GitHub
|
||||
@@ -3124,7 +3175,7 @@ func TestWorkspaceLock(t *testing.T) {
|
||||
require.NotNil(t, workspace.DeletingAt)
|
||||
require.NotNil(t, workspace.DormantAt)
|
||||
require.Equal(t, workspace.DormantAt.Add(dormantTTL), *workspace.DeletingAt)
|
||||
require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second*10), time.Now())
|
||||
require.WithinRange(t, *workspace.DormantAt, time.Now().Add(-time.Second), time.Now())
|
||||
// Locking a workspace shouldn't update the last_used_at.
|
||||
require.Equal(t, lastUsedAt, workspace.LastUsedAt)
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ require (
|
||||
require (
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393
|
||||
github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mark3labs/mcp-go v0.34.0
|
||||
)
|
||||
|
||||
@@ -916,8 +916,8 @@ github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102 h1:ahTJlTRmTogsubgRVGO
|
||||
github.com/coder/pq v1.10.5-0.20250630052411-a259f96b6102/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
|
||||
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
|
||||
github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393 h1:l+m2liikn8JoEv6C22QIV4qseolUfvNsyUNA6JJsD6Y=
|
||||
github.com/coder/preview v1.0.3-0.20250701142654-c3d6e86b9393/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM=
|
||||
github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448 h1:S86sFp4Dr4dUn++fXOMOTu6ClnEZ/NrGCYv7bxZjYYc=
|
||||
github.com/coder/preview v1.0.3-0.20250714153828-a737d4750448/go.mod h1:hQtBEqOFMJ3SHl9Q9pVvDA9CpeCEXBwbONNK29+3MLk=
|
||||
github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE=
|
||||
github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
|
||||
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
)
|
||||
|
||||
// Parse extracts Terraform variables from source-code.
|
||||
// TODO: This Parse is incomplete. It uses tfparse instead of terraform.
|
||||
// The inputs are incomplete, as values such as the user context, parameters,
|
||||
// etc are all important to the parsing process. This should be replaced with
|
||||
// preview and have all inputs.
|
||||
func (s *server) Parse(sess *provisionersdk.Session, _ *proto.ParseRequest, _ <-chan struct{}) *proto.ParseComplete {
|
||||
ctx := sess.Context()
|
||||
_, span := s.startTrace(ctx, tracing.FuncName())
|
||||
|
||||
Reference in New Issue
Block a user