fix: fix for prebuilds claiming and deletion (#17624)

PR contains:
- fix for claiming & deleting prebuilds with immutable params
- unit test for claiming scenario
- unit test for deletion scenario

The parameter resolver was failing when deleting/claiming prebuilds
because a value for a previously-used parameter was provided to the
resolver, but since the value was unchanged (it's coming from the
preset) it failed in the resolver. The resolver was missing a check to
see if the old value != new value; if the values match then there's no
mutation of an immutable parameter.

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
This commit is contained in:
Yevhenii Shcherbina
2025-05-01 04:52:23 -04:00
committed by GitHub
parent c7fc7b91ec
commit 98e5611e16
4 changed files with 81 additions and 13 deletions

View File

@ -164,7 +164,7 @@ type ParameterResolver struct {
// resolves the correct value. It returns the value of the parameter, if valid, and an error if invalid. // resolves the correct value. It returns the value of the parameter, if valid, and an error if invalid.
func (r *ParameterResolver) ValidateResolve(p TemplateVersionParameter, v *WorkspaceBuildParameter) (value string, err error) { func (r *ParameterResolver) ValidateResolve(p TemplateVersionParameter, v *WorkspaceBuildParameter) (value string, err error) {
prevV := r.findLastValue(p) prevV := r.findLastValue(p)
if !p.Mutable && v != nil && prevV != nil { if !p.Mutable && v != nil && prevV != nil && v.Value != prevV.Value {
return "", xerrors.Errorf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", p.Name) return "", xerrors.Errorf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", p.Name)
} }
if p.Required && v == nil && prevV == nil { if p.Required && v == nil && prevV == nil {

View File

@ -1,6 +1,7 @@
package codersdk_test package codersdk_test
import ( import (
"fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -121,20 +122,60 @@ func TestParameterResolver_ValidateResolve_NewOverridesOld(t *testing.T) {
func TestParameterResolver_ValidateResolve_Immutable(t *testing.T) { func TestParameterResolver_ValidateResolve_Immutable(t *testing.T) {
t.Parallel() t.Parallel()
uut := codersdk.ParameterResolver{ uut := codersdk.ParameterResolver{
Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "5"}}, Rich: []codersdk.WorkspaceBuildParameter{{Name: "n", Value: "old"}},
} }
p := codersdk.TemplateVersionParameter{ p := codersdk.TemplateVersionParameter{
Name: "n", Name: "n",
Type: "number", Type: "string",
Required: true, Required: true,
Mutable: false, Mutable: false,
} }
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n", cases := []struct {
Value: "6", name string
}) newValue string
require.Error(t, err) expectedErr string
require.Equal(t, "", v) }{
{
name: "mutation",
newValue: "new", // "new" != "old"
expectedErr: fmt.Sprintf("Parameter %q is not mutable", p.Name),
},
{
// Values are case-sensitive.
name: "case change",
newValue: "Old", // "Old" != "old"
expectedErr: fmt.Sprintf("Parameter %q is not mutable", p.Name),
},
{
name: "default",
newValue: "", // "" != "old"
expectedErr: fmt.Sprintf("Parameter %q is not mutable", p.Name),
},
{
name: "no change",
newValue: "old", // "old" == "old"
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
v, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
Name: "n",
Value: tc.newValue,
})
if tc.expectedErr == "" {
require.NoError(t, err)
require.Equal(t, tc.newValue, v)
} else {
require.ErrorContains(t, err, tc.expectedErr)
require.Equal(t, "", v)
}
})
}
} }
func TestRichParameterValidation(t *testing.T) { func TestRichParameterValidation(t *testing.T) {

View File

@ -329,9 +329,8 @@ func TestClaimPrebuild(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
stopBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ stopBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: version.ID, TemplateVersionID: version.ID,
Transition: codersdk.WorkspaceTransitionStop, Transition: codersdk.WorkspaceTransitionStop,
RichParameterValues: wp,
}) })
require.NoError(t, err) require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, stopBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, stopBuild.ID)
@ -369,6 +368,17 @@ func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Resp
}, },
}, },
}, },
// Make sure immutable params don't break claiming logic
Parameters: []*proto.RichParameter{
{
Name: "k1",
Description: "immutable param",
Type: "string",
DefaultValue: "",
Required: false,
Mutable: false,
},
},
Presets: []*proto.Preset{ Presets: []*proto.Preset{
{ {
Name: "preset-a", Name: "preset-a",

View File

@ -901,6 +901,16 @@ func setupTestDBTemplateVersion(
ID: templateID, ID: templateID,
ActiveVersionID: templateVersion.ID, ActiveVersionID: templateVersion.ID,
})) }))
// Make sure immutable params don't break prebuilt workspace deletion logic
dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{
TemplateVersionID: templateVersion.ID,
Name: "test",
Description: "required & immutable param",
Type: "string",
DefaultValue: "",
Required: true,
Mutable: false,
})
return templateVersion.ID return templateVersion.ID
} }
@ -999,7 +1009,7 @@ func setupTestDBWorkspace(
OrganizationID: orgID, OrganizationID: orgID,
Error: buildError, Error: buildError,
}) })
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID, WorkspaceID: workspace.ID,
InitiatorID: initiatorID, InitiatorID: initiatorID,
TemplateVersionID: templateVersionID, TemplateVersionID: templateVersionID,
@ -1008,6 +1018,13 @@ func setupTestDBWorkspace(
Transition: transition, Transition: transition,
CreatedAt: clock.Now(), CreatedAt: clock.Now(),
}) })
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
{
WorkspaceBuildID: workspaceBuild.ID,
Name: "test",
Value: "test",
},
})
return workspace return workspace
} }