diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go index 91809d3a037d6..98a5d546eaffc 100644 --- a/coderd/parameters_test.go +++ b/coderd/parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/provisioner/echo" @@ -211,6 +212,86 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) { require.Zero(t, setup.api.FileCache.Count()) }) + t.Run("RebuildParameters", func(t *testing.T) { + t.Parallel() + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf") + require.NoError(t, err) + + modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules")) + require.NoError(t, err) + + setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{ + provisionerDaemonVersion: provProto.CurrentVersion.String(), + mainTF: dynamicParametersTerraformSource, + modulesArchive: modulesArchive, + plan: nil, + static: nil, + }) + + ctx := testutil.Context(t, testutil.WaitMedium) + 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) + require.Equal(t, "jetbrains_ide", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid) + require.Equal(t, "CL", preview.Parameters[0].Value.Value) + _ = stream.Close(websocket.StatusGoingAway) + + wrk := coderdtest.CreateWorkspace(t, setup.client, setup.template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.RichParameterValues = []codersdk.WorkspaceBuildParameter{ + { + Name: preview.Parameters[0].Name, + Value: "GO", + }, + } + }) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID) + + params, err := setup.client.WorkspaceBuildParameters(ctx, wrk.LatestBuild.ID) + require.NoError(t, err) + require.Len(t, params, 1) + require.Equal(t, "jetbrains_ide", params[0].Name) + require.Equal(t, "GO", params[0].Value) + + // A helper function to assert params + doTransition := func(t *testing.T, trans codersdk.WorkspaceTransition) { + t.Helper() + + fooVal := coderdtest.RandomUsername(t) + bld, err := setup.client.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: setup.template.ActiveVersionID, + Transition: trans, + RichParameterValues: []codersdk.WorkspaceBuildParameter{ + // No validation, so this should work as is. + // Overwrite the value on each transition + {Name: "foo", Value: fooVal}, + }, + EnableDynamicParameters: ptr.Ref(true), + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID) + + latestParams, err := setup.client.WorkspaceBuildParameters(ctx, bld.ID) + require.NoError(t, err) + require.ElementsMatch(t, latestParams, []codersdk.WorkspaceBuildParameter{ + {Name: "jetbrains_ide", Value: "GO"}, + {Name: "foo", Value: fooVal}, + }) + } + + // Restart the workspace, then delete. Asserting params on all builds. + doTransition(t, codersdk.WorkspaceTransitionStop) + doTransition(t, codersdk.WorkspaceTransitionStart) + doTransition(t, codersdk.WorkspaceTransitionDelete) + }) + t.Run("BadOwner", func(t *testing.T) { t.Parallel() @@ -266,9 +347,10 @@ type setupDynamicParamsTestParams struct { } type dynamicParamsTest struct { - client *codersdk.Client - api *coderd.API - stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest] + client *codersdk.Client + api *coderd.API + stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest] + template codersdk.Template } func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dynamicParamsTest { @@ -300,7 +382,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) - _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) ctx := testutil.Context(t, testutil.WaitShort) stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID) @@ -321,9 +403,10 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn }) return dynamicParamsTest{ - client: ownerClient, - stream: stream, - api: api, + client: ownerClient, + api: api, + stream: stream, + template: tpl, } } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 46035f28dda77..bcc2cef40ebdc 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -623,6 +623,11 @@ func (b *Builder) getParameters() (names, values []string, err error) { return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err} } + lastBuildParameterValues := db2sdk.WorkspaceBuildParameters(lastBuildParameters) + resolver := codersdk.ParameterResolver{ + Rich: lastBuildParameterValues, + } + // Dynamic parameters skip all parameter validation. // Deleting a workspace also should skip parameter validation. // Pass the user's input as is. @@ -632,19 +637,34 @@ func (b *Builder) getParameters() (names, values []string, err error) { // conditional parameter existence, the static frame of reference // is not sufficient. So assume the user is correct, or pull in the // dynamic param code to find the actual parameters. + latestValues := make(map[string]string, len(b.richParameterValues)) + for _, latest := range b.richParameterValues { + latestValues[latest.Name] = latest.Value + } + + // Merge the inputs with values from the previous build. + for _, last := range lastBuildParameterValues { + // TODO: Ideally we use the resolver here and look at parameter + // fields such as 'ephemeral'. This requires loading the terraform + // files. For now, just send the previous inputs as is. + if _, exists := latestValues[last.Name]; exists { + // latestValues take priority, so skip this previous value. + continue + } + names = append(names, last.Name) + values = append(values, last.Value) + } + for _, value := range b.richParameterValues { names = append(names, value.Name) values = append(values, value.Value) } + b.parameterNames = &names b.parameterValues = &values return names, values, nil } - resolver := codersdk.ParameterResolver{ - Rich: db2sdk.WorkspaceBuildParameters(lastBuildParameters), - } - for _, templateVersionParameter := range templateVersionParameters { tvp, err := db2sdk.TemplateVersionParameter(templateVersionParameter) if err != nil {