mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: make dynamic parameters opt-in by default for new templates (#19006)
resolves #18975 --------- Co-authored-by: Steven Masley <stevenmasley@gmail.com> Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+27
-6
@@ -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,
|
||||
|
||||
+20
-4
@@ -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,
|
||||
|
||||
@@ -509,6 +509,7 @@ func TestTemplatePush(t *testing.T) {
|
||||
default = "1"
|
||||
}
|
||||
data "coder_parameter" "b" {
|
||||
name = "b"
|
||||
type = string
|
||||
default = "2"
|
||||
}
|
||||
|
||||
+1
-1
@@ -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]=====",
|
||||
|
||||
+4
-2
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -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.';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE templates ALTER COLUMN use_classic_parameter_flow SET DEFAULT true;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE templates ALTER COLUMN use_classic_parameter_flow SET DEFAULT false;
|
||||
+10
-8
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
func (*dbRejectGitSSHKey) GetGitSSHKey(_ context.Context, _ uuid.UUID) (database.GitSSHKey, error) {
|
||||
return database.GitSSHKey{}, xerrors.New("forcing a fake error")
|
||||
// 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 (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)
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+39
@@ -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(
|
||||
<ClassicParameterFlowDeprecationWarning
|
||||
templateSettingsLink={`/templates/${defaultProps.organizationName}/${defaultProps.templateName}/settings`}
|
||||
{...defaultProps}
|
||||
isEnabled={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("deprecated")).toBeInTheDocument();
|
||||
expect(screen.getByText("Go to Template Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when enabled is false", () => {
|
||||
const { container } = render(
|
||||
<ClassicParameterFlowDeprecationWarning
|
||||
templateSettingsLink={`/templates/${defaultProps.organizationName}/${defaultProps.templateName}/settings`}
|
||||
{...defaultProps}
|
||||
isEnabled={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
+38
@@ -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 (
|
||||
<Alert severity="warning" className="mb-2">
|
||||
<div>
|
||||
This template is using the classic parameter flow, which will be{" "}
|
||||
<strong>deprecated</strong> and removed in a future release. Please
|
||||
migrate to{" "}
|
||||
<a
|
||||
href={docs("/admin/templates/extending-templates/dynamic-parameters")}
|
||||
className="text-content-link"
|
||||
>
|
||||
dynamic parameters
|
||||
</a>{" "}
|
||||
on template settings for improved functionality.
|
||||
</div>
|
||||
|
||||
<Link className="text-xs" href={templateSettingsLink}>
|
||||
Go to Template Settings
|
||||
</Link>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -72,6 +72,20 @@ const CreateWorkspacePage: FC = () => {
|
||||
}),
|
||||
enabled: !!templateQuery.data,
|
||||
});
|
||||
const templatePermissionsQuery = useQuery({
|
||||
...checkAuthorization({
|
||||
checks: {
|
||||
canUpdateTemplate: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
resource_id: templateQuery.data?.id ?? "",
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: !!templateQuery.data,
|
||||
});
|
||||
const realizedVersionId =
|
||||
customVersionId ?? templateQuery.data?.active_version_id;
|
||||
const organizationId = templateQuery.data?.organization_id;
|
||||
@@ -93,9 +107,13 @@ const CreateWorkspacePage: FC = () => {
|
||||
const isLoadingFormData =
|
||||
templateQuery.isLoading ||
|
||||
permissionsQuery.isLoading ||
|
||||
templatePermissionsQuery.isLoading ||
|
||||
richParametersQuery.isLoading;
|
||||
const loadFormDataError =
|
||||
templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error;
|
||||
templateQuery.error ??
|
||||
permissionsQuery.error ??
|
||||
templatePermissionsQuery.error ??
|
||||
richParametersQuery.error;
|
||||
|
||||
const title = autoCreateWorkspaceMutation.isPending
|
||||
? "Creating workspace..."
|
||||
@@ -211,7 +229,9 @@ const CreateWorkspacePage: FC = () => {
|
||||
startPollingExternalAuth={startPollingExternalAuth}
|
||||
hasAllRequiredExternalAuth={hasAllRequiredExternalAuth}
|
||||
permissions={permissionsQuery.data as CreateWorkspacePermissions}
|
||||
canUpdateTemplate={permissionsQuery.data?.canUpdateTemplate}
|
||||
templatePermissions={
|
||||
templatePermissionsQuery.data as { canUpdateTemplate: boolean }
|
||||
}
|
||||
parameters={realizedParameters as TemplateVersionParameter[]}
|
||||
presets={templateVersionPresetsQuery.data ?? []}
|
||||
creatingWorkspace={createWorkspaceMutation.isPending}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MockUserOwner,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
|
||||
|
||||
const meta: Meta<typeof CreateWorkspacePageView> = {
|
||||
@@ -31,7 +32,9 @@ const meta: Meta<typeof CreateWorkspacePageView> = {
|
||||
canUpdateTemplate: false,
|
||||
},
|
||||
onCancel: action("onCancel"),
|
||||
templatePermissions: { canUpdateTemplate: true },
|
||||
},
|
||||
decorators: [withDashboardProvider],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -28,6 +28,8 @@ import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { type FormikContextType, useFormik } from "formik";
|
||||
import type { ExternalAuthPollingState } from "hooks/useExternalAuth";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { linkToTemplate, useLinks } from "modules/navigation";
|
||||
import { ClassicParameterFlowDeprecationWarning } from "modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning";
|
||||
import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName";
|
||||
import { type FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -68,6 +70,7 @@ interface CreateWorkspacePageViewProps {
|
||||
autofillParameters: AutofillBuildParameter[];
|
||||
presets: TypesGen.Preset[];
|
||||
permissions: CreateWorkspacePermissions;
|
||||
templatePermissions: { canUpdateTemplate: boolean };
|
||||
creatingWorkspace: boolean;
|
||||
canUpdateTemplate?: boolean;
|
||||
onCancel: () => void;
|
||||
@@ -94,11 +97,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
autofillParameters,
|
||||
presets = [],
|
||||
permissions,
|
||||
templatePermissions,
|
||||
creatingWorkspace,
|
||||
canUpdateTemplate,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const getLink = useLinks();
|
||||
const [owner, setOwner] = useState(defaultOwner);
|
||||
const [suggestedName, setSuggestedName] = useState(() =>
|
||||
generateWorkspaceName(),
|
||||
@@ -261,6 +266,13 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
|
||||
<ClassicParameterFlowDeprecationWarning
|
||||
templateSettingsLink={`${getLink(
|
||||
linkToTemplate(template.organization_name, template.name),
|
||||
)}/settings`}
|
||||
isEnabled={templatePermissions.canUpdateTemplate}
|
||||
/>
|
||||
|
||||
<HorizontalForm
|
||||
name="create-workspace-form"
|
||||
onSubmit={form.handleSubmit}
|
||||
|
||||
@@ -407,7 +407,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
<br />
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
@@ -555,7 +555,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
parameters cannot be modified once the workspace is created.
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
|
||||
+7
-6
@@ -245,19 +245,20 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
label={
|
||||
<StackLabel>
|
||||
<span className="flex flex-row gap-2">
|
||||
Enable dynamic parameters for workspace creation
|
||||
Enable dynamic parameters for workspace creation (recommended)
|
||||
</span>
|
||||
<StackLabelHelperText>
|
||||
<div>
|
||||
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.
|
||||
</div>
|
||||
<Link
|
||||
className="text-xs"
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#dynamic-parameters-beta",
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
|
||||
+8
-2
@@ -14,6 +14,7 @@ import {
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import { useFormik } from "formik";
|
||||
import { ClassicParameterFlowDeprecationWarning } from "modules/workspaces/ClassicParameterFlowDeprecationWarning/ClassicParameterFlowDeprecationWarning";
|
||||
import type { FC } from "react";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
import {
|
||||
@@ -33,6 +34,7 @@ interface WorkspaceParameterFormProps {
|
||||
autofillParams: AutofillBuildParameter[];
|
||||
isSubmitting: boolean;
|
||||
canChangeVersions: boolean;
|
||||
templatePermissions: { canUpdateTemplate: boolean } | undefined;
|
||||
error: unknown;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: WorkspaceParametersFormValues) => void;
|
||||
@@ -46,6 +48,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
autofillParams,
|
||||
error,
|
||||
canChangeVersions,
|
||||
templatePermissions,
|
||||
isSubmitting,
|
||||
}) => {
|
||||
const form = useFormik<WorkspaceParametersFormValues>({
|
||||
@@ -81,12 +84,15 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||
return (
|
||||
<>
|
||||
{disabled && (
|
||||
<Alert severity="warning" css={{ marginBottom: 48 }}>
|
||||
<Alert severity="warning">
|
||||
The template for this workspace requires automatic updates. Update the
|
||||
workspace to edit parameters.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ClassicParameterFlowDeprecationWarning
|
||||
templateSettingsLink={`/templates/${workspace.organization_name}/${workspace.template_name}/settings`}
|
||||
isEnabled={templatePermissions?.canUpdateTemplate ?? false}
|
||||
/>
|
||||
<HorizontalForm onSubmit={form.handleSubmit} data-testid="form">
|
||||
{hasNonEphemeralParameters && (
|
||||
<FormSection
|
||||
|
||||
+23
@@ -51,6 +51,25 @@ const WorkspaceParametersPage: FC = () => {
|
||||
const permissions = permissionsQuery.data as WorkspacePermissions | undefined;
|
||||
const canChangeVersions = Boolean(permissions?.updateWorkspaceVersion);
|
||||
|
||||
const templatePermissionsQuery = useQuery({
|
||||
...checkAuthorization({
|
||||
checks: {
|
||||
canUpdateTemplate: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
resource_id: workspace.template_id,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: workspace !== undefined,
|
||||
});
|
||||
|
||||
const templatePermissions = templatePermissionsQuery.data as
|
||||
| { canUpdateTemplate: boolean }
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -60,6 +79,7 @@ const WorkspaceParametersPage: FC = () => {
|
||||
<WorkspaceParametersPageView
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
templatePermissions={templatePermissions}
|
||||
data={parameters.data}
|
||||
submitError={updateParameters.error}
|
||||
isSubmitting={updateParameters.isPending}
|
||||
@@ -94,6 +114,7 @@ const WorkspaceParametersPage: FC = () => {
|
||||
type WorkspaceParametersPageViewProps = {
|
||||
workspace: Workspace;
|
||||
canChangeVersions: boolean;
|
||||
templatePermissions: { canUpdateTemplate: boolean } | undefined;
|
||||
data: Awaited<ReturnType<typeof API.getWorkspaceParameters>> | undefined;
|
||||
submitError: unknown;
|
||||
isSubmitting: boolean;
|
||||
@@ -106,6 +127,7 @@ export const WorkspaceParametersPageView: FC<
|
||||
> = ({
|
||||
workspace,
|
||||
canChangeVersions,
|
||||
templatePermissions,
|
||||
data,
|
||||
submitError,
|
||||
onSubmit,
|
||||
@@ -129,6 +151,7 @@ export const WorkspaceParametersPageView: FC<
|
||||
<WorkspaceParametersForm
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
templatePermissions={templatePermissions}
|
||||
autofillParams={data.buildParameters.map((p) => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
|
||||
+4
-2
@@ -226,7 +226,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
<br />
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
@@ -261,7 +261,9 @@ const WorkspaceParametersPageExperimental: FC = () => {
|
||||
message="This workspace has no parameters"
|
||||
cta={
|
||||
<Link
|
||||
href={docs("/admin/templates/extending-templates/parameters")}
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
Learn more about parameters
|
||||
</Link>
|
||||
|
||||
+1
-1
@@ -210,7 +210,7 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
parameters cannot be modified once the workspace is created.
|
||||
<Link
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/parameters#enable-dynamic-parameters-early-access",
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
View docs
|
||||
|
||||
@@ -826,7 +826,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||
deprecated: false,
|
||||
deprecation_message: "",
|
||||
max_port_share_level: "public",
|
||||
use_classic_parameter_flow: true,
|
||||
use_classic_parameter_flow: false,
|
||||
};
|
||||
|
||||
const MockTemplateVersionFiles: TemplateVersionFiles = {
|
||||
|
||||
Reference in New Issue
Block a user