diff --git a/cli/restart.go b/cli/restart.go
index 156f506105c5a..20ee0b9b9de9d 100644
--- a/cli/restart.go
+++ b/cli/restart.go
@@ -51,8 +51,17 @@ func (r *RootCmd) restart() *serpent.Command {
return err
}
+ stopParamValues, err := asWorkspaceBuildParameters(parameterFlags.ephemeralParameters)
+ if err != nil {
+ return xerrors.Errorf("parse ephemeral parameters: %w", err)
+ }
wbr := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStop,
+ // Ephemeral parameters should be passed to both stop and start builds.
+ // TODO: maybe these values should be sourced from the previous build?
+ // It has to be manually sourced, as ephemeral parameters do not carry across
+ // builds.
+ RichParameterValues: stopParamValues,
}
if bflags.provisionerLogDebug {
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
diff --git a/cli/restart_test.go b/cli/restart_test.go
index d69344435bf28..01be7e590cebf 100644
--- a/cli/restart_test.go
+++ b/cli/restart_test.go
@@ -10,6 +10,7 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
@@ -70,8 +71,14 @@ func TestRestart(t *testing.T) {
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
- template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.UseClassicParameterFlow = ptr.Ref(true) // TODO: Remove when dynamic parameters prompt missing ephemeral parameters.
+ })
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "placeholder"},
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "restart", workspace.Name, "--prompt-ephemeral-parameters")
@@ -125,7 +132,11 @@ func TestRestart(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "placeholder"},
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "restart", workspace.Name,
@@ -178,8 +189,14 @@ func TestRestart(t *testing.T) {
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
- template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.UseClassicParameterFlow = ptr.Ref(true) // TODO: Remove when dynamic parameters prompts missing ephemeral parameters
+ })
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "placeholder"},
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "restart", workspace.Name, "--build-options")
@@ -233,7 +250,11 @@ func TestRestart(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "placeholder"},
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "restart", workspace.Name,
diff --git a/cli/start_test.go b/cli/start_test.go
index 85b7b88374f72..6e58b40e30778 100644
--- a/cli/start_test.go
+++ b/cli/start_test.go
@@ -113,10 +113,18 @@ func TestStart(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
- workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
+ workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
inv, root := clitest.New(t, "start", workspace.Name, "--prompt-ephemeral-parameters")
@@ -167,10 +175,18 @@ func TestStart(t *testing.T) {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
- workspace := coderdtest.CreateWorkspace(t, member, template.ID)
+ workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
- workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
+ workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
+ request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
+ {Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
+ }
+ })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
inv, root := clitest.New(t, "start", workspace.Name,
diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go
index f7a31d5e0c25f..732fdd5ee50b0 100644
--- a/cli/templatepush_test.go
+++ b/cli/templatepush_test.go
@@ -509,6 +509,7 @@ func TestTemplatePush(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden
index 51c2887cd1e4a..822998329be5b 100644
--- a/cli/testdata/coder_list_--output_json.golden
+++ b/cli/testdata/coder_list_--output_json.golden
@@ -15,7 +15,7 @@
"template_allow_user_cancel_workspace_jobs": false,
"template_active_version_id": "============[version ID]============",
"template_require_active_version": false,
- "template_use_classic_parameter_flow": true,
+ "template_use_classic_parameter_flow": false,
"latest_build": {
"id": "========[workspace build ID]========",
"created_at": "====[timestamp]=====",
diff --git a/cli/update_test.go b/cli/update_test.go
index 7a7480353c01d..b80218f49ab45 100644
--- a/cli/update_test.go
+++ b/cli/update_test.go
@@ -182,7 +182,7 @@ func TestUpdateWithRichParameters(t *testing.T) {
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
- {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true},
+ {Name: ephemeralParameterName, Description: ephemeralParameterDescription, Mutable: true, Ephemeral: true, DefaultValue: "unset"},
})
}
@@ -811,7 +811,9 @@ func TestUpdateValidateRichParameters(t *testing.T) {
}
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(templateParameters))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
- template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
+ template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.UseClassicParameterFlow = ptr.Ref(true) // TODO: Remove when dynamic parameters can pass this test
+ })
// Create new workspace
inv, root := clitest.New(t, "create", "my-workspace", "--yes", "--template", template.Name, "--parameter", fmt.Sprintf("%s=%s", numberParameterName, tempVal))
diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go
index 812d2e2576650..0ccefff2c42a9 100644
--- a/coderd/database/dbgen/dbgen.go
+++ b/coderd/database/dbgen/dbgen.go
@@ -147,7 +147,7 @@ func Template(t testing.TB, db database.Store, seed database.Template) database.
DisplayName: takeFirst(seed.DisplayName, testutil.GetRandomName(t)),
AllowUserCancelWorkspaceJobs: seed.AllowUserCancelWorkspaceJobs,
MaxPortSharingLevel: takeFirst(seed.MaxPortSharingLevel, database.AppSharingLevelOwner),
- UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, true),
+ UseClassicParameterFlow: takeFirst(seed.UseClassicParameterFlow, false),
})
require.NoError(t, err, "insert template")
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index bbd6ca3ce5736..67d58ad05c802 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -1750,7 +1750,7 @@ CREATE TABLE templates (
deprecated text DEFAULT ''::text NOT NULL,
activity_bump bigint DEFAULT '3600000000000'::bigint NOT NULL,
max_port_sharing_level app_sharing_level DEFAULT 'owner'::app_sharing_level NOT NULL,
- use_classic_parameter_flow boolean DEFAULT true NOT NULL
+ use_classic_parameter_flow boolean DEFAULT false NOT NULL
);
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
diff --git a/coderd/database/migrations/000352_default_dynamic_templates.down.sql b/coderd/database/migrations/000352_default_dynamic_templates.down.sql
new file mode 100644
index 0000000000000..548cd7e2c30b2
--- /dev/null
+++ b/coderd/database/migrations/000352_default_dynamic_templates.down.sql
@@ -0,0 +1 @@
+ALTER TABLE templates ALTER COLUMN use_classic_parameter_flow SET DEFAULT true;
diff --git a/coderd/database/migrations/000352_default_dynamic_templates.up.sql b/coderd/database/migrations/000352_default_dynamic_templates.up.sql
new file mode 100644
index 0000000000000..51bcab9f099f8
--- /dev/null
+++ b/coderd/database/migrations/000352_default_dynamic_templates.up.sql
@@ -0,0 +1 @@
+ALTER TABLE templates ALTER COLUMN use_classic_parameter_flow SET DEFAULT false;
diff --git a/coderd/insights_test.go b/coderd/insights_test.go
index ded030351a3b3..d916b20fea26e 100644
--- a/coderd/insights_test.go
+++ b/coderd/insights_test.go
@@ -665,10 +665,11 @@ func TestTemplateInsights_Golden(t *testing.T) {
// where we can control the template ID.
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
createdTemplate := dbgen.Template(t, db, database.Template{
- ID: template.id,
- ActiveVersionID: version.ID,
- OrganizationID: firstUser.OrganizationID,
- CreatedBy: firstUser.UserID,
+ ID: template.id,
+ ActiveVersionID: version.ID,
+ OrganizationID: firstUser.OrganizationID,
+ CreatedBy: firstUser.UserID,
+ UseClassicParameterFlow: true, // Required for testing classic parameter flow behavior
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
@@ -1556,10 +1557,11 @@ func TestUserActivityInsights_Golden(t *testing.T) {
// where we can control the template ID.
// createdTemplate := coderdtest.CreateTemplate(t, client, firstUser.OrganizationID, version.ID)
createdTemplate := dbgen.Template(t, db, database.Template{
- ID: template.id,
- ActiveVersionID: version.ID,
- OrganizationID: firstUser.OrganizationID,
- CreatedBy: firstUser.UserID,
+ ID: template.id,
+ ActiveVersionID: version.ID,
+ OrganizationID: firstUser.OrganizationID,
+ CreatedBy: firstUser.UserID,
+ UseClassicParameterFlow: true, // Required for parameter usage tracking in this test
GroupACL: database.TemplateACL{
firstUser.OrganizationID.String(): db2sdk.TemplateRoleActions(codersdk.TemplateRoleUse),
},
diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go
index c00d6f9224bfb..07c00d2ef23e3 100644
--- a/coderd/parameters_test.go
+++ b/coderd/parameters_test.go
@@ -3,10 +3,12 @@ package coderd_test
import (
"context"
"os"
+ "sync"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
+ "go.uber.org/atomic"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd"
@@ -199,8 +201,15 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
+ c := atomic.NewInt32(0)
+ reject := &dbRejectGitSSHKey{Store: db, hook: func(d *dbRejectGitSSHKey) {
+ if c.Add(1) > 1 {
+ // Second call forward, reject
+ d.SetReject(true)
+ }
+ }}
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
- db: &dbRejectGitSSHKey{Store: db},
+ db: reject,
ps: ps,
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
@@ -444,8 +453,30 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
// that is generally impossible to force an error.
type dbRejectGitSSHKey struct {
database.Store
+ rejectMu sync.RWMutex
+ reject bool
+ hook func(d *dbRejectGitSSHKey)
+}
+
+// SetReject toggles whether GetGitSSHKey should return an error or passthrough to the underlying store.
+func (d *dbRejectGitSSHKey) SetReject(reject bool) {
+ d.rejectMu.Lock()
+ defer d.rejectMu.Unlock()
+ d.reject = reject
}
-func (*dbRejectGitSSHKey) GetGitSSHKey(_ context.Context, _ uuid.UUID) (database.GitSSHKey, error) {
- return database.GitSSHKey{}, xerrors.New("forcing a fake error")
+func (d *dbRejectGitSSHKey) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
+ if d.hook != nil {
+ d.hook(d)
+ }
+
+ d.rejectMu.RLock()
+ reject := d.reject
+ d.rejectMu.RUnlock()
+
+ if reject {
+ return database.GitSSHKey{}, xerrors.New("forcing a fake error")
+ }
+
+ return d.Store.GetGitSSHKey(ctx, userID)
}
diff --git a/coderd/templates.go b/coderd/templates.go
index bba38bb033614..60f94e5cd29cc 100644
--- a/coderd/templates.go
+++ b/coderd/templates.go
@@ -197,8 +197,8 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
return
}
- // Default is true until dynamic parameters are promoted to stable.
- useClassicParameterFlow := ptr.NilToDefault(createTemplate.UseClassicParameterFlow, true)
+ // Default is false as dynamic parameters are now the preferred approach.
+ useClassicParameterFlow := ptr.NilToDefault(createTemplate.UseClassicParameterFlow, false)
// Make a temporary struct to represent the template. This is used for
// auditing if any of the following checks fail. It will be overwritten when
diff --git a/coderd/templates_test.go b/coderd/templates_test.go
index 5e7fcea75609d..0858ce83325cc 100644
--- a/coderd/templates_test.go
+++ b/coderd/templates_test.go
@@ -77,7 +77,7 @@ func TestPostTemplateByOrganization(t *testing.T) {
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Description, got.Description)
assert.Equal(t, expected.ActivityBumpMillis, got.ActivityBumpMillis)
- assert.Equal(t, expected.UseClassicParameterFlow, true) // Current default is true
+ assert.Equal(t, expected.UseClassicParameterFlow, false) // Current default is false
require.Len(t, auditor.AuditLogs(), 3)
assert.Equal(t, database.AuditActionCreate, auditor.AuditLogs()[0].Action)
@@ -1551,7 +1551,7 @@ func TestPatchTemplateMeta(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
- require.True(t, template.UseClassicParameterFlow, "default is true")
+ require.False(t, template.UseClassicParameterFlow, "default is false")
bTrue := true
bFalse := false
diff --git a/coderd/templateversions.go b/coderd/templateversions.go
index e787a6b813b18..cc106b390f73c 100644
--- a/coderd/templateversions.go
+++ b/coderd/templateversions.go
@@ -1471,7 +1471,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
return
}
- var dynamicTemplate bool
+ dynamicTemplate := true // Default to using dynamic templates
if req.TemplateID != uuid.Nil {
tpl, err := api.Database.GetTemplateByID(ctx, req.TemplateID)
if httpapi.Is404Error(err) {
diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go
index 1ad06bae38aee..912bca1c5fec1 100644
--- a/coderd/templateversions_test.go
+++ b/coderd/templateversions_test.go
@@ -275,6 +275,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
files map[string]string
reqTags map[string]string
wantTags map[string]string
+ variables []codersdk.VariableValue
expectError string
}{
{
@@ -290,6 +291,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -311,6 +313,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -335,6 +338,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -365,6 +369,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -395,6 +400,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -429,11 +435,12 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
}
}`,
},
- reqTags: map[string]string{"a": "b"},
- wantTags: map[string]string{"owner": "", "scope": "organization", "a": "b"},
+ reqTags: map[string]string{"a": "b"},
+ wantTags: map[string]string{"owner": "", "scope": "organization", "a": "b"},
+ variables: []codersdk.VariableValue{{Name: "a", Value: "b"}},
},
{
- name: "main.tf with disallowed workspace tag value",
+ name: "main.tf with resource reference",
files: map[string]string{
`main.tf`: `
variable "a" {
@@ -441,6 +448,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
default = "1"
}
data "coder_parameter" "b" {
+ name = "b"
type = string
default = "2"
}
@@ -461,38 +469,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
}
}`,
},
- expectError: `Unknown variable; There is no variable named "null_resource".`,
- },
- {
- name: "main.tf with disallowed function in tag value",
- files: map[string]string{
- `main.tf`: `
- variable "a" {
- type = string
- default = "1"
- }
- data "coder_parameter" "b" {
- type = string
- default = "2"
- }
- data "coder_parameter" "unrelated" {
- name = "unrelated"
- type = "list(string)"
- default = jsonencode(["a", "b"])
- }
- resource "null_resource" "test" {
- name = "foo"
- }
- data "coder_workspace_tags" "tags" {
- tags = {
- "foo": "bar",
- "a": var.a,
- "b": data.coder_parameter.b.value,
- "test": pathexpand("~/file.txt"),
- }
- }`,
- },
- expectError: `function "pathexpand" may not be used here`,
+ reqTags: map[string]string{"foo": "bar", "a": "1", "b": "2", "test": "foo"},
+ wantTags: map[string]string{"owner": "", "scope": "organization", "foo": "bar", "a": "1", "b": "2", "test": "foo"},
},
// We will allow coder_workspace_tags to set the scope on a template version import job
// BUT the user ID will be ultimately determined by the API key in the scope.
@@ -618,11 +596,12 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
// Create a template version from the archive
tvName := testutil.GetRandomNameHyphenated(t)
tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
- Name: tvName,
- StorageMethod: codersdk.ProvisionerStorageMethodFile,
- Provisioner: codersdk.ProvisionerTypeTerraform,
- FileID: fi.ID,
- ProvisionerTags: tt.reqTags,
+ Name: tvName,
+ StorageMethod: codersdk.ProvisionerStorageMethodFile,
+ Provisioner: codersdk.ProvisionerTypeTerraform,
+ FileID: fi.ID,
+ ProvisionerTags: tt.reqTags,
+ UserVariableValues: tt.variables,
})
if tt.expectError == "" {
diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go
index 141c62ff3a4b3..9fe066aae6284 100644
--- a/coderd/workspaces_test.go
+++ b/coderd/workspaces_test.go
@@ -431,9 +431,9 @@ func TestWorkspace(t *testing.T) {
// Test Utility variables
templateVersionParameters := []*proto.RichParameter{
- {Name: "param1", Type: "string", Required: false},
- {Name: "param2", Type: "string", Required: false},
- {Name: "param3", Type: "string", Required: false},
+ {Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
+ {Name: "param2", Type: "string", Required: false, DefaultValue: "default2"},
+ {Name: "param3", Type: "string", Required: false, DefaultValue: "default3"},
}
presetParameters := []*proto.PresetParameter{
{Name: "param1", Value: "value1"},
@@ -3842,7 +3842,9 @@ func TestWorkspaceWithEphemeralRichParameters(t *testing.T) {
}},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
- template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+ template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
+ request.UseClassicParameterFlow = ptr.Ref(true) // TODO: Remove this when dynamic parameters handles this case
+ })
// Create workspace with default values
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go
index 031af97317aca..4bb2a1dd6b78b 100644
--- a/provisioner/echo/serve.go
+++ b/provisioner/echo/serve.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strings"
+ "text/template"
"github.com/google/uuid"
"golang.org/x/xerrors"
@@ -377,6 +378,45 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
logger.Debug(context.Background(), "extra file written", slog.F("name", name), slog.F("bytes_written", n))
}
+
+ // Write a main.tf with the appropriate parameters. This is to write terraform
+ // that matches the parameters defined in the responses. Dynamic parameters
+ // parsed these, even in the echo provisioner.
+ var mainTF bytes.Buffer
+ for _, respPlan := range responses.ProvisionPlan {
+ plan := respPlan.GetPlan()
+ if plan == nil {
+ continue
+ }
+
+ for _, param := range plan.Parameters {
+ paramTF, err := ParameterTerraform(param)
+ if err != nil {
+ return nil, xerrors.Errorf("parameter terraform: %w", err)
+ }
+ _, _ = mainTF.WriteString(paramTF)
+ }
+ }
+
+ if mainTF.Len() > 0 {
+ mainTFData := `
+terraform {
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ }
+ }
+}
+` + mainTF.String()
+
+ _ = writer.WriteHeader(&tar.Header{
+ Name: `main.tf`,
+ Size: int64(len(mainTFData)),
+ Mode: 0o644,
+ })
+ _, _ = writer.Write([]byte(mainTFData))
+ }
+
// `writer.Close()` function flushes the writer buffer, and adds extra padding to create a legal tarball.
err := writer.Close()
if err != nil {
@@ -385,6 +425,69 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response
return buffer.Bytes(), nil
}
+// ParameterTerraform will create a Terraform data block for the provided parameter.
+func ParameterTerraform(param *proto.RichParameter) (string, error) {
+ tmpl := template.Must(template.New("parameter").Funcs(map[string]any{
+ "showValidation": func(v *proto.RichParameter) bool {
+ return v != nil && (v.ValidationMax != nil || v.ValidationMin != nil ||
+ v.ValidationError != "" || v.ValidationRegex != "" ||
+ v.ValidationMonotonic != "")
+ },
+ "formType": func(v *proto.RichParameter) string {
+ s, _ := proto.ProviderFormType(v.FormType)
+ return string(s)
+ },
+ }).Parse(`
+data "coder_parameter" "{{ .Name }}" {
+ name = "{{ .Name }}"
+ display_name = "{{ .DisplayName }}"
+ description = "{{ .Description }}"
+ icon = "{{ .Icon }}"
+ mutable = {{ .Mutable }}
+ ephemeral = {{ .Ephemeral }}
+ order = {{ .Order }}
+{{- if .DefaultValue }}
+ default = {{ .DefaultValue }}
+{{- end }}
+{{- if .Type }}
+ type = "{{ .Type }}"
+{{- end }}
+{{- if .FormType }}
+ form_type = "{{ formType . }}"
+{{- end }}
+{{- range .Options }}
+ option {
+ name = "{{ .Name }}"
+ value = "{{ .Value }}"
+ }
+{{- end }}
+{{- if showValidation .}}
+ validation {
+ {{- if .ValidationRegex }}
+ regex = "{{ .ValidationRegex }}"
+ {{- end }}
+ {{- if .ValidationError }}
+ error = "{{ .ValidationError }}"
+ {{- end }}
+ {{- if .ValidationMin }}
+ min = {{ .ValidationMin }}
+ {{- end }}
+ {{- if .ValidationMax }}
+ max = {{ .ValidationMax }}
+ {{- end }}
+ {{- if .ValidationMonotonic }}
+ monotonic = "{{ .ValidationMonotonic }}"
+ {{- end }}
+ }
+{{- end }}
+}
+`))
+
+ var buf bytes.Buffer
+ err := tmpl.Execute(&buf, param)
+ return buf.String(), err
+}
+
func WithResources(resources []*proto.Resource) *Responses {
return &Responses{
Parse: ParseComplete,
diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts
index a738899b25f2c..768a7d477f992 100644
--- a/site/e2e/helpers.ts
+++ b/site/e2e/helpers.ts
@@ -1203,3 +1203,36 @@ export async function addUserToOrganization(
}
await page.mouse.click(10, 10); // close the popover by clicking outside of it
}
+
+/**
+ * disableDynamicParameters navigates to the template settings page and disables
+ * dynamic parameters by unchecking the "Enable dynamic parameters" checkbox.
+ */
+export const disableDynamicParameters = async (
+ page: Page,
+ templateName: string,
+ orgName = defaultOrganizationName,
+) => {
+ await page.goto(`/templates/${orgName}/${templateName}/settings`, {
+ waitUntil: "domcontentloaded",
+ });
+
+ // Find and uncheck the "Enable dynamic parameters" checkbox
+ const dynamicParamsCheckbox = page.getByRole("checkbox", {
+ name: /Enable dynamic parameters for workspace creation/,
+ });
+
+ // If the checkbox is checked, uncheck it
+ if (await dynamicParamsCheckbox.isChecked()) {
+ await dynamicParamsCheckbox.click();
+ }
+
+ // Save the changes
+ await page.getByRole("button", { name: /save/i }).click();
+
+ // Wait for the success message or page to update
+ await page.waitForSelector("text=Template updated successfully", {
+ state: "visible",
+ timeout: 10000,
+ });
+};
diff --git a/site/e2e/tests/workspaces/createWorkspace.spec.ts b/site/e2e/tests/workspaces/createWorkspace.spec.ts
index 452c6e9969f37..e9d2d5efcca6f 100644
--- a/site/e2e/tests/workspaces/createWorkspace.spec.ts
+++ b/site/e2e/tests/workspaces/createWorkspace.spec.ts
@@ -4,6 +4,7 @@ import {
StarterTemplates,
createTemplate,
createWorkspace,
+ disableDynamicParameters,
echoResponsesWithParameters,
login,
openTerminalWindow,
@@ -35,6 +36,9 @@ test("create workspace", async ({ page }) => {
apply: [{ apply: { resources: [{ name: "example" }] } }],
});
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
await createWorkspace(page, template);
});
@@ -51,6 +55,9 @@ test("create workspace with default immutable parameters", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
@@ -68,6 +75,9 @@ test("create workspace with default mutable parameters", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
await verifyParameters(page, workspaceName, richParameters, [
@@ -95,6 +105,9 @@ test("create workspace with default and required parameters", async ({
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template, {
richParameters,
@@ -127,6 +140,9 @@ test("create workspace and overwrite default parameters", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template, {
richParameters,
@@ -147,6 +163,9 @@ test("create workspace with disable_param search params", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, templateName);
+
await login(page, users.member);
await page.goto(
`/templates/${templateName}/workspace?disable_params=first_parameter,second_parameter`,
@@ -165,6 +184,9 @@ test.skip("create docker workspace", async ({ context, page }) => {
await login(page, users.templateAdmin);
const template = await createTemplate(page, StarterTemplates.STARTER_DOCKER);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
diff --git a/site/e2e/tests/workspaces/restartWorkspace.spec.ts b/site/e2e/tests/workspaces/restartWorkspace.spec.ts
index 444ff891f0fdc..2ec24c6d251bf 100644
--- a/site/e2e/tests/workspaces/restartWorkspace.spec.ts
+++ b/site/e2e/tests/workspaces/restartWorkspace.spec.ts
@@ -4,6 +4,7 @@ import {
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
+ disableDynamicParameters,
echoResponsesWithParameters,
verifyParameters,
} from "../../helpers";
@@ -24,6 +25,9 @@ test("restart workspace with ephemeral parameters", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
diff --git a/site/e2e/tests/workspaces/startWorkspace.spec.ts b/site/e2e/tests/workspaces/startWorkspace.spec.ts
index 90fac440046ea..ea8a5c21c88bd 100644
--- a/site/e2e/tests/workspaces/startWorkspace.spec.ts
+++ b/site/e2e/tests/workspaces/startWorkspace.spec.ts
@@ -4,6 +4,7 @@ import {
buildWorkspaceWithParameters,
createTemplate,
createWorkspace,
+ disableDynamicParameters,
echoResponsesWithParameters,
stopWorkspace,
verifyParameters,
@@ -25,6 +26,9 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
diff --git a/site/e2e/tests/workspaces/updateWorkspace.spec.ts b/site/e2e/tests/workspaces/updateWorkspace.spec.ts
index 48c341eb63956..8a242a2dc7238 100644
--- a/site/e2e/tests/workspaces/updateWorkspace.spec.ts
+++ b/site/e2e/tests/workspaces/updateWorkspace.spec.ts
@@ -3,6 +3,7 @@ import { users } from "../../constants";
import {
createTemplate,
createWorkspace,
+ disableDynamicParameters,
echoResponsesWithParameters,
updateTemplate,
updateWorkspace,
@@ -34,6 +35,9 @@ test("update workspace, new optional, immutable parameter added", async ({
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
@@ -77,6 +81,9 @@ test("update workspace, new required, mutable parameter added", async ({
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
@@ -122,6 +129,9 @@ test("update workspace with ephemeral parameter enabled", async ({ page }) => {
echoResponsesWithParameters(richParameters),
);
+ // Disable dynamic parameters to use classic parameter flow for this test
+ await disableDynamicParameters(page, template);
+
await login(page, users.member);
const workspaceName = await createWorkspace(page, template);
diff --git a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx
new file mode 100644
index 0000000000000..f68bb273f26a0
--- /dev/null
+++ b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from "@testing-library/react";
+import { ClassicParameterFlowDeprecationWarning } from "./ClassicParameterFlowDeprecationWarning";
+
+jest.mock("modules/navigation", () => ({
+ useLinks: () => () => "/mock-link",
+ linkToTemplate: () => "/mock-template-link",
+}));
+
+describe("ClassicParameterFlowDeprecationWarning", () => {
+ const defaultProps = {
+ organizationName: "test-org",
+ templateName: "test-template",
+ };
+
+ it("renders warning when enabled and user has template update permissions", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("deprecated")).toBeInTheDocument();
+ expect(screen.getByText("Go to Template Settings")).toBeInTheDocument();
+ });
+
+ it("does not render when enabled is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx
new file mode 100644
index 0000000000000..d6afd3be464bf
--- /dev/null
+++ b/site/src/modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning.tsx
@@ -0,0 +1,38 @@
+import { Alert } from "components/Alert/Alert";
+import { Link } from "components/Link/Link";
+import type { FC } from "react";
+import { docs } from "utils/docs";
+
+interface ClassicParameterFlowDeprecationWarningProps {
+ templateSettingsLink: string;
+ isEnabled: boolean;
+}
+
+export const ClassicParameterFlowDeprecationWarning: FC<
+ ClassicParameterFlowDeprecationWarningProps
+> = ({ templateSettingsLink, isEnabled }) => {
+ if (!isEnabled) {
+ return null;
+ }
+
+ return (
+
+
+ This template is using the classic parameter flow, which will be{" "}
+ deprecated and removed in a future release. Please
+ migrate to{" "}
+
+ dynamic parameters
+ {" "}
+ on template settings for improved functionality.
+
- The new workspace form allows you to design your template
- with new form types and identity-aware conditional
- parameters. The form will only present options that are
- compatible and available.
+ The dynamic workspace form allows you to design your
+ template with additional form types and identity-aware
+ conditional parameters. This is the default option for new
+ templates. The classic workspace creation flow will be
+ deprecated in a future release.