From 21e5dfacfc81c184fbd0b5aae45eeeba353ae689 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 25 Jun 2025 10:04:59 -0500 Subject: [PATCH 1/2] feat: allow new immutable parameters for existing workspaces --- coderd/dynamicparameters/rendermock/mock.go | 2 + .../rendermock/rendermock.go | 71 +++++++++++++++++++ coderd/dynamicparameters/resolver.go | 10 ++- coderd/dynamicparameters/resolver_test.go | 59 +++++++++++++++ 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 coderd/dynamicparameters/rendermock/mock.go create mode 100644 coderd/dynamicparameters/rendermock/rendermock.go create mode 100644 coderd/dynamicparameters/resolver_test.go diff --git a/coderd/dynamicparameters/rendermock/mock.go b/coderd/dynamicparameters/rendermock/mock.go new file mode 100644 index 0000000000000..ffb23780629f6 --- /dev/null +++ b/coderd/dynamicparameters/rendermock/mock.go @@ -0,0 +1,2 @@ +//go:generate mockgen -destination ./rendermock.go -package rendermock github.com/coder/coder/v2/coderd/dynamicparameters Renderer +package rendermock diff --git a/coderd/dynamicparameters/rendermock/rendermock.go b/coderd/dynamicparameters/rendermock/rendermock.go new file mode 100644 index 0000000000000..996b02a555b08 --- /dev/null +++ b/coderd/dynamicparameters/rendermock/rendermock.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coder/coder/v2/coderd/dynamicparameters (interfaces: Renderer) +// +// Generated by this command: +// +// mockgen -destination ./rendermock.go -package rendermock github.com/coder/coder/v2/coderd/dynamicparameters Renderer +// + +// Package rendermock is a generated GoMock package. +package rendermock + +import ( + context "context" + reflect "reflect" + + preview "github.com/coder/preview" + uuid "github.com/google/uuid" + hcl "github.com/hashicorp/hcl/v2" + gomock "go.uber.org/mock/gomock" +) + +// MockRenderer is a mock of Renderer interface. +type MockRenderer struct { + ctrl *gomock.Controller + recorder *MockRendererMockRecorder + isgomock struct{} +} + +// MockRendererMockRecorder is the mock recorder for MockRenderer. +type MockRendererMockRecorder struct { + mock *MockRenderer +} + +// NewMockRenderer creates a new mock instance. +func NewMockRenderer(ctrl *gomock.Controller) *MockRenderer { + mock := &MockRenderer{ctrl: ctrl} + mock.recorder = &MockRendererMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRenderer) EXPECT() *MockRendererMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockRenderer) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockRendererMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRenderer)(nil).Close)) +} + +// Render mocks base method. +func (m *MockRenderer) Render(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Render", ctx, ownerID, values) + ret0, _ := ret[0].(*preview.Output) + ret1, _ := ret[1].(hcl.Diagnostics) + return ret0, ret1 +} + +// Render indicates an expected call of Render. +func (mr *MockRendererMockRecorder) Render(ctx, ownerID, values any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Render", reflect.TypeOf((*MockRenderer)(nil).Render), ctx, ownerID, values) +} diff --git a/coderd/dynamicparameters/resolver.go b/coderd/dynamicparameters/resolver.go index f71b630858013..3cb9c59f286d6 100644 --- a/coderd/dynamicparameters/resolver.go +++ b/coderd/dynamicparameters/resolver.go @@ -169,9 +169,15 @@ func ResolveParameters( parameterNames[parameter.Name] = struct{}{} if !firstBuild && !parameter.Mutable { + originalValue, ok := originalValues[parameter.Name] // Immutable parameters should not be changed after the first build. - // They can match the original value though! - if parameter.Value.AsString() != originalValues[parameter.Name].Value { + // If the value matches the original value, that is fine. + // + // If the original value is not set, that means this is a new parameter. New + // immutable parameters are allowed. This is an opinionated choice to prevent + // workspaces failing to update or delete. Ideally we would block this, as + // immutable parameters should only be able to be set at creation time. + if ok && parameter.Value.AsString() != originalValue.Value { var src *hcl.Range if parameter.Source != nil { src = ¶meter.Source.HCLBlock().TypeRange diff --git a/coderd/dynamicparameters/resolver_test.go b/coderd/dynamicparameters/resolver_test.go new file mode 100644 index 0000000000000..ec5218613ff03 --- /dev/null +++ b/coderd/dynamicparameters/resolver_test.go @@ -0,0 +1,59 @@ +package dynamicparameters_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/dynamicparameters" + "github.com/coder/coder/v2/coderd/dynamicparameters/rendermock" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + "github.com/coder/preview" + previewtypes "github.com/coder/preview/types" + "github.com/coder/terraform-provider-coder/v2/provider" +) + +func TestResolveParameters(t *testing.T) { + t.Parallel() + + t.Run("NewImmutable", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + render := rendermock.NewMockRenderer(ctrl) + + // A single immutable parameter with no previous value. + render.EXPECT(). + Render(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&preview.Output{ + Parameters: []previewtypes.Parameter{ + { + ParameterData: previewtypes.ParameterData{ + Name: "immutable", + Type: previewtypes.ParameterTypeString, + FormType: provider.ParameterFormTypeInput, + Mutable: false, + DefaultValue: previewtypes.StringLiteral("foo"), + Required: true, + }, + Value: previewtypes.StringLiteral("foo"), + Diagnostics: nil, + }, + }, + }, nil) + + ctx := testutil.Context(t, testutil.WaitShort) + values, err := dynamicparameters.ResolveParameters(ctx, uuid.New(), render, false, + []database.WorkspaceBuildParameter{}, // No previous values + []codersdk.WorkspaceBuildParameter{}, // No new build values + []database.TemplateVersionPresetParameter{}, // No preset values + ) + require.NoError(t, err) + require.Equal(t, map[string]string{"immutable": "foo"}, values) + }) +} From aa2a8095456a59995d577fb36cc318e01f743cb4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 25 Jun 2025 10:29:18 -0500 Subject: [PATCH 2/2] test: add unit test from coderd for new immutable param --- coderd/coderdtest/dynamicparameters.go | 23 +++++++-- enterprise/coderd/dynamicparameters_test.go | 51 +++++++++++++++++++ .../testdata/parameters/immutable/main.tf | 16 ++++++ .../coderd/testdata/parameters/none/main.tf | 10 ++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 enterprise/coderd/testdata/parameters/immutable/main.tf create mode 100644 enterprise/coderd/testdata/parameters/none/main.tf diff --git a/coderd/coderdtest/dynamicparameters.go b/coderd/coderdtest/dynamicparameters.go index b5bb34a0e3468..cb295eeaae965 100644 --- a/coderd/coderdtest/dynamicparameters.go +++ b/coderd/coderdtest/dynamicparameters.go @@ -22,6 +22,9 @@ type DynamicParameterTemplateParams struct { // StaticParams is used if the provisioner daemon version does not support dynamic parameters. StaticParams []*proto.RichParameter + + // TemplateID is used to update an existing template instead of creating a new one. + TemplateID uuid.UUID } func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UUID, args DynamicParameterTemplateParams) (codersdk.Template, codersdk.TemplateVersion) { @@ -40,16 +43,30 @@ func DynamicParameterTemplate(t *testing.T, client *codersdk.Client, org uuid.UU }, }} - version := CreateTemplateVersion(t, client, org, files) + version := CreateTemplateVersion(t, client, org, files, func(request *codersdk.CreateTemplateVersionRequest) { + if args.TemplateID != uuid.Nil { + request.TemplateID = args.TemplateID + } + }) AwaitTemplateVersionJobCompleted(t, client, version.ID) - tpl := CreateTemplate(t, client, org, version.ID) + + tplID := args.TemplateID + if args.TemplateID == uuid.Nil { + tpl := CreateTemplate(t, client, org, version.ID) + tplID = tpl.ID + } var err error - tpl, err = client.UpdateTemplateMeta(t.Context(), tpl.ID, codersdk.UpdateTemplateMeta{ + tpl, err := client.UpdateTemplateMeta(t.Context(), tplID, codersdk.UpdateTemplateMeta{ UseClassicParameterFlow: ptr.Ref(false), }) require.NoError(t, err) + err = client.UpdateActiveTemplateVersion(t.Context(), tpl.ID, codersdk.UpdateActiveTemplateVersion{ + ID: version.ID, + }) + require.NoError(t, err) + return tpl, version } diff --git a/enterprise/coderd/dynamicparameters_test.go b/enterprise/coderd/dynamicparameters_test.go index 8bbe5da470f42..87d115034f247 100644 --- a/enterprise/coderd/dynamicparameters_test.go +++ b/enterprise/coderd/dynamicparameters_test.go @@ -302,6 +302,57 @@ func TestDynamicParameterBuild(t *testing.T) { require.ErrorContains(t, err, "Number must be between 0 and 10") }) }) + + t.Run("ImmutableValidation", func(t *testing.T) { + t.Parallel() + + // NewImmutable tests the case where a new immutable parameter is added to a template + // after a workspace has been created with an older version of the template. + // The test tries to delete the workspace, which should succeed. + t.Run("NewImmutable", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + // Start with a new template that has 0 parameters + empty, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{ + MainTF: string(must(os.ReadFile("testdata/parameters/none/main.tf"))), + }) + + // Create the workspace with 0 parameters + wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{ + TemplateID: empty.ID, + Name: coderdtest.RandomUsername(t), + RichParameterValues: []codersdk.WorkspaceBuildParameter{}, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID) + + // Update the template with a new immutable parameter + _, immutable := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{ + MainTF: string(must(os.ReadFile("testdata/parameters/immutable/main.tf"))), + TemplateID: empty.ID, + }) + + bld, err := templateAdmin.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: immutable.ID, // Use the new template version with the immutable parameter + Transition: codersdk.WorkspaceTransitionDelete, + DryRun: false, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, bld.ID) + + // Verify the immutable parameter is set on the workspace build + params, err := templateAdmin.WorkspaceBuildParameters(ctx, bld.ID) + require.NoError(t, err) + require.Len(t, params, 1) + require.Equal(t, "Hello World", params[0].Value) + + // Verify the workspace is deleted + deleted, err := templateAdmin.DeletedWorkspace(ctx, wrk.ID) + require.NoError(t, err) + require.Equal(t, wrk.ID, deleted.ID, "workspace should be deleted") + }) + }) } // TestDynamicParameterTemplate uses a template with some dynamic elements, and diff --git a/enterprise/coderd/testdata/parameters/immutable/main.tf b/enterprise/coderd/testdata/parameters/immutable/main.tf new file mode 100644 index 0000000000000..84b8967ac305e --- /dev/null +++ b/enterprise/coderd/testdata/parameters/immutable/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "immutable" { + name = "immutable" + type = "string" + mutable = false + default = "Hello World" +} diff --git a/enterprise/coderd/testdata/parameters/none/main.tf b/enterprise/coderd/testdata/parameters/none/main.tf new file mode 100644 index 0000000000000..74a83f752f4d8 --- /dev/null +++ b/enterprise/coderd/testdata/parameters/none/main.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy