feat!: patchTemplateMeta to use optional fields (#24984)

Closes https://github.com/coder/coder/issues/13112

**Breaking Change**: Removed status code `StatusNotModified` when no
diffs occur in a patch. Now the patch is always applied and a template
is always returned.
This commit is contained in:
Steven Masley
2026-05-11 12:43:52 -05:00
committed by GitHub
parent 3986aa8a51
commit 19573e8aee
18 changed files with 1028 additions and 356 deletions
+2 -1
View File
@@ -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/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
@@ -46,7 +47,7 @@ func TestStart(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, oldVersion.ID)
require.Equal(t, oldVersion.ID, template.ActiveVersionID)
template = coderdtest.UpdateTemplateMeta(t, templateAdminClient, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, template.RequireActiveVersion)
+20 -20
View File
@@ -218,20 +218,20 @@ func TestTemplateEdit(t *testing.T) {
}
template, err := ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
Name: expectedName,
Name: ptr.Ref(expectedName),
DisplayName: &expectedDisplayName,
Description: &expectedDescription,
Icon: &expectedIcon,
DefaultTTLMillis: expectedDefaultTTLMillis,
AllowUserAutostop: expectedAllowAutostop,
AllowUserAutostart: expectedAllowAutostart,
FailureTTLMillis: expectedFailureTTLMillis,
TimeTilDormantMillis: expectedDormancyMillis,
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
RequireActiveVersion: expectedRequireActiveVersion,
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
DeprecationMessage: ptr.Ref(deprecationMessage),
DisableEveryoneGroupAccess: expectedDisableEveryone,
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: expectedAutostartDaysOfWeek,
},
@@ -266,20 +266,20 @@ func TestTemplateEdit(t *testing.T) {
expectedAutoStopWeeks = 2
template, err = ownerClient.UpdateTemplateMeta(ctx, dbtemplate.ID, codersdk.UpdateTemplateMeta{
Name: expectedName,
Name: ptr.Ref(expectedName),
DisplayName: &expectedDisplayName,
Description: &expectedDescription,
Icon: &expectedIcon,
DefaultTTLMillis: expectedDefaultTTLMillis,
AllowUserAutostop: expectedAllowAutostop,
AllowUserAutostart: expectedAllowAutostart,
FailureTTLMillis: expectedFailureTTLMillis,
TimeTilDormantMillis: expectedDormancyMillis,
TimeTilDormantAutoDeleteMillis: expectedAutoDeleteMillis,
RequireActiveVersion: expectedRequireActiveVersion,
DefaultTTLMillis: ptr.Ref(expectedDefaultTTLMillis),
AllowUserAutostop: ptr.Ref(expectedAllowAutostop),
AllowUserAutostart: ptr.Ref(expectedAllowAutostart),
FailureTTLMillis: ptr.Ref(expectedFailureTTLMillis),
TimeTilDormantMillis: ptr.Ref(expectedDormancyMillis),
TimeTilDormantAutoDeleteMillis: ptr.Ref(expectedAutoDeleteMillis),
RequireActiveVersion: ptr.Ref(expectedRequireActiveVersion),
DeprecationMessage: ptr.Ref(deprecationMessage),
DisableEveryoneGroupAccess: expectedDisableEveryone,
AllowUserCancelWorkspaceJobs: expectedAllowCancelJobs,
DisableEveryoneGroupAccess: ptr.Ref(expectedDisableEveryone),
AllowUserCancelWorkspaceJobs: ptr.Ref(expectedAllowCancelJobs),
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: expectedAutostartDaysOfWeek,
},
+33 -31
View File
@@ -186,14 +186,16 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
// OK
// OK: setting the same level is a no-op under the new PATCH semantics
// (304 Not Modified) but must not be a server error.
var level codersdk.WorkspaceAgentPortShareLevel = codersdk.WorkspaceAgentPortShareLevelPublic
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
MaxPortShareLevel: &level,
})
require.NoError(t, err)
assert.Equal(t, level, updated.MaxPortShareLevel)
template, err = client.Template(ctx, template.ID)
require.NoError(t, err)
assert.Equal(t, level, template.MaxPortShareLevel)
// Invalid level
level = "invalid"
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
@@ -258,7 +260,7 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
@@ -275,7 +277,7 @@ func TestTemplates(t *testing.T) {
// Ensure a missing field is a noop
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: ptr.Ref(template.Icon + "something"),
@@ -312,7 +314,7 @@ func TestTemplates(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitLong)
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
@@ -348,12 +350,12 @@ func TestTemplates(t *testing.T) {
ctx := context.Background()
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DefaultTTLMillis: time.Hour.Milliseconds(),
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
DefaultTTLMillis: ptr.Ref(time.Hour.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"monday", "saturday"},
Weeks: 3,
@@ -402,14 +404,14 @@ func TestTemplates(t *testing.T) {
)
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
FailureTTLMillis: failureTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
require.Equal(t, failureTTL.Milliseconds(), updated.FailureTTLMillis)
@@ -471,14 +473,14 @@ func TestTemplates(t *testing.T) {
// nolint: paralleltest // context is from parent t.Run
t.Run(c.Name, func(t *testing.T) {
_, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
TimeTilDormantMillis: c.TimeTilDormantMS,
FailureTTLMillis: c.FailureTTLMS,
TimeTilDormantAutoDeleteMillis: c.DormantAutoDeleteMS,
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
TimeTilDormantMillis: ptr.Ref(c.TimeTilDormantMS),
FailureTTLMillis: ptr.Ref(c.FailureTTLMS),
TimeTilDormantAutoDeleteMillis: ptr.Ref(c.DormantAutoDeleteMS),
})
require.Error(t, err)
cerr, ok := codersdk.AsError(err)
@@ -529,7 +531,7 @@ func TestTemplates(t *testing.T) {
dormantTTL := time.Minute
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
@@ -547,7 +549,7 @@ func TestTemplates(t *testing.T) {
// Disable the time_til_dormant_auto_delete on the template, then we can assert that the workspaces
// no longer have a deleting_at field.
updated, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: 0,
TimeTilDormantAutoDeleteMillis: ptr.Ref[int64](0),
})
require.NoError(t, err)
require.EqualValues(t, 0, updated.TimeTilDormantAutoDeleteMillis)
@@ -604,8 +606,8 @@ func TestTemplates(t *testing.T) {
dormantTTL := time.Minute
//nolint:gocritic // non-template-admin cannot update template meta
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
UpdateWorkspaceDormantAt: true,
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
UpdateWorkspaceDormantAt: ptr.Ref(true),
})
require.NoError(t, err)
require.Equal(t, dormantTTL.Milliseconds(), updated.TimeTilDormantAutoDeleteMillis)
@@ -661,8 +663,8 @@ func TestTemplates(t *testing.T) {
inactivityTTL := time.Minute
updated, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactivityTTL.Milliseconds(),
UpdateWorkspaceLastUsedAt: true,
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
UpdateWorkspaceLastUsedAt: ptr.Ref(true),
})
require.NoError(t, err)
require.Equal(t, inactivityTTL.Milliseconds(), updated.TimeTilDormantMillis)
@@ -706,14 +708,14 @@ func TestTemplates(t *testing.T) {
// Update the field and assert it persists.
updatedTemplate, err := anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: false,
RequireActiveVersion: ptr.Ref(false),
})
require.NoError(t, err)
require.False(t, updatedTemplate.RequireActiveVersion)
// Flip it back to ensure we aren't hardcoding to a default value.
updatedTemplate, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.NoError(t, err)
require.True(t, updatedTemplate.RequireActiveVersion)
@@ -1003,12 +1005,12 @@ func TestTemplateACL(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 1, len(acl.Groups))
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
Name: ptr.Ref(template.Name),
DisplayName: &template.DisplayName,
Description: &template.Description,
Icon: &template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DisableEveryoneGroupAccess: true,
AllowUserCancelWorkspaceJobs: ptr.Ref(template.AllowUserCancelWorkspaceJobs),
DisableEveryoneGroupAccess: ptr.Ref(true),
})
require.NoError(t, err)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"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/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
@@ -43,7 +44,7 @@ func TestWorkspaceBuild(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplAv1.ID)
require.Equal(t, tplAv1.ID, tplA.ActiveVersionID)
tplA = coderdtest.UpdateTemplateMeta(t, ownerClient, tplA.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, tplA.RequireActiveVersion)
tplAv2 := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
@@ -57,7 +58,7 @@ func TestWorkspaceBuild(t *testing.T) {
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, tplBv1.ID)
require.Equal(t, tplBv1.ID, tplB.ActiveVersionID)
tplB = coderdtest.UpdateTemplateMeta(t, ownerClient, tplB.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.True(t, tplB.RequireActiveVersion)
+8 -8
View File
@@ -784,7 +784,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}).Do().Template
template := coderdtest.UpdateTemplateMeta(t, client, tpl.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
})
resp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
@@ -1260,7 +1260,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
require.Len(t, stats.Transitions, 0)
_, err = anotherClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: dormantTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormantTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1334,7 +1334,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Now that we've validated that the workspace is eligible for autostart
// lets cause it to become dormant.
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantMillis: inactiveTTL.Milliseconds(),
TimeTilDormantMillis: ptr.Ref(inactiveTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1433,7 +1433,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Enable auto-deletion for the template.
_, err = templateAdmin.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
TimeTilDormantAutoDeleteMillis: transitionTTL.Milliseconds(),
TimeTilDormantAutoDeleteMillis: ptr.Ref(transitionTTL.Milliseconds()),
})
require.NoError(t, err)
@@ -1538,8 +1538,8 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Update the template to require the promoted version.
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
AllowUserAutostart: true,
RequireActiveVersion: ptr.Ref(true),
AllowUserAutostart: ptr.Ref(true),
})
require.NoError(t, err)
@@ -1832,7 +1832,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
templateTTL = 72 * time.Hour.Milliseconds()
ctx := testutil.Context(t, testutil.WaitShort)
template = coderdtest.UpdateTemplateMeta(t, client, template.ID, codersdk.UpdateTemplateMeta{
DefaultTTLMillis: templateTTL,
DefaultTTLMillis: ptr.Ref(templateTTL),
})
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
@@ -4068,7 +4068,7 @@ func TestResolveAutostart(t *testing.T) {
defer cancel()
_, err := ownerClient.UpdateTemplateMeta(ctx, version1.Template.ID, codersdk.UpdateTemplateMeta{
RequireActiveVersion: true,
RequireActiveVersion: ptr.Ref(true),
})
require.NoError(t, err)