feat: allow for default presets (#18445)

This commit is contained in:
Danny Kopping
2025-06-24 12:19:19 +02:00
committed by GitHub
parent 3fb5d0b52d
commit 6cc4cfa346
54 changed files with 2186 additions and 658 deletions
+3
View File
@@ -14519,6 +14519,9 @@ const docTemplate = `{
"codersdk.Preset": {
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"id": {
"type": "string"
},
+3
View File
@@ -13145,6 +13145,9 @@
"codersdk.Preset": {
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"id": {
"type": "string"
},
+1
View File
@@ -416,6 +416,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
DesiredInstances: preset.DesiredInstances,
InvalidateAfterSecs: preset.InvalidateAfterSecs,
SchedulingTimezone: preset.SchedulingTimezone,
IsDefault: false,
})
}
+1
View File
@@ -1303,6 +1303,7 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d
DesiredInstances: seed.DesiredInstances,
InvalidateAfterSecs: seed.InvalidateAfterSecs,
SchedulingTimezone: seed.SchedulingTimezone,
IsDefault: seed.IsDefault,
})
require.NoError(t, err, "insert preset")
return preset
+1
View File
@@ -9198,6 +9198,7 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP
Valid: true,
},
PrebuildStatus: database.PrebuildStatusHealthy,
IsDefault: arg.IsDefault,
}
q.presets = append(q.presets, preset)
return preset, nil
+4 -1
View File
@@ -1513,7 +1513,8 @@ CREATE TABLE template_version_presets (
desired_instances integer,
invalidate_after_secs integer DEFAULT 0,
prebuild_status prebuild_status DEFAULT 'healthy'::prebuild_status NOT NULL,
scheduling_timezone text DEFAULT ''::text NOT NULL
scheduling_timezone text DEFAULT ''::text NOT NULL,
is_default boolean DEFAULT false NOT NULL
);
CREATE TABLE template_version_terraform_values (
@@ -2690,6 +2691,8 @@ CREATE INDEX idx_tailnet_tunnels_dst_id ON tailnet_tunnels USING hash (dst_id);
CREATE INDEX idx_tailnet_tunnels_src_id ON tailnet_tunnels USING hash (src_id);
CREATE UNIQUE INDEX idx_template_version_presets_default ON template_version_presets USING btree (template_version_id) WHERE (is_default = true);
CREATE INDEX idx_template_versions_has_ai_task ON template_versions USING btree (has_ai_task);
CREATE UNIQUE INDEX idx_unique_preset_name ON template_version_presets USING btree (name, template_version_id);
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_template_version_presets_default;
ALTER TABLE template_version_presets DROP COLUMN IF EXISTS is_default;
@@ -0,0 +1,6 @@
ALTER TABLE template_version_presets ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT FALSE;
-- Add a unique constraint to ensure only one default preset per template version
CREATE UNIQUE INDEX idx_template_version_presets_default
ON template_version_presets (template_version_id)
WHERE is_default = TRUE;
+1
View File
@@ -3414,6 +3414,7 @@ type TemplateVersionPreset struct {
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
IsDefault bool `db:"is_default" json:"is_default"`
}
type TemplateVersionPresetParameter struct {
+15 -6
View File
@@ -6629,7 +6629,7 @@ func (q *sqlQuerier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]Te
}
const getPresetByID = `-- name: GetPresetByID :one
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tv.template_id, tv.organization_id FROM
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, tvp.prebuild_status, tvp.scheduling_timezone, tvp.is_default, tv.template_id, tv.organization_id FROM
template_version_presets tvp
INNER JOIN template_versions tv ON tvp.template_version_id = tv.id
WHERE tvp.id = $1
@@ -6644,6 +6644,7 @@ type GetPresetByIDRow struct {
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
IsDefault bool `db:"is_default" json:"is_default"`
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
@@ -6660,6 +6661,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
&i.TemplateID,
&i.OrganizationID,
)
@@ -6668,7 +6670,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
SELECT
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone
template_version_presets.id, template_version_presets.template_version_id, template_version_presets.name, template_version_presets.created_at, template_version_presets.desired_instances, template_version_presets.invalidate_after_secs, template_version_presets.prebuild_status, template_version_presets.scheduling_timezone, template_version_presets.is_default
FROM
template_version_presets
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
@@ -6688,6 +6690,7 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
)
return i, err
}
@@ -6769,7 +6772,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context,
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
SELECT
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default
FROM
template_version_presets
WHERE
@@ -6794,6 +6797,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
); err != nil {
return nil, err
}
@@ -6816,7 +6820,8 @@ INSERT INTO template_version_presets (
created_at,
desired_instances,
invalidate_after_secs,
scheduling_timezone
scheduling_timezone,
is_default
)
VALUES (
$1,
@@ -6825,8 +6830,9 @@ VALUES (
$4,
$5,
$6,
$7
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone
$7,
$8
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default
`
type InsertPresetParams struct {
@@ -6837,6 +6843,7 @@ type InsertPresetParams struct {
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
IsDefault bool `db:"is_default" json:"is_default"`
}
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
@@ -6848,6 +6855,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
arg.DesiredInstances,
arg.InvalidateAfterSecs,
arg.SchedulingTimezone,
arg.IsDefault,
)
var i TemplateVersionPreset
err := row.Scan(
@@ -6859,6 +6867,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
&i.InvalidateAfterSecs,
&i.PrebuildStatus,
&i.SchedulingTimezone,
&i.IsDefault,
)
return i, err
}
+4 -2
View File
@@ -6,7 +6,8 @@ INSERT INTO template_version_presets (
created_at,
desired_instances,
invalidate_after_secs,
scheduling_timezone
scheduling_timezone,
is_default
)
VALUES (
@id,
@@ -15,7 +16,8 @@ VALUES (
@created_at,
@desired_instances,
@invalidate_after_secs,
@scheduling_timezone
@scheduling_timezone,
@is_default
) RETURNING *;
-- name: InsertPresetParameters :many
+1
View File
@@ -106,6 +106,7 @@ const (
UniqueIndexCustomRolesNameLower UniqueConstraint = "idx_custom_roles_name_lower" // CREATE UNIQUE INDEX idx_custom_roles_name_lower ON custom_roles USING btree (lower(name));
UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)) WHERE (deleted = false);
UniqueIndexProvisionerDaemonsOrgNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_org_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_org_name_owner_key ON provisioner_daemons USING btree (organization_id, name, lower(COALESCE((tags ->> 'owner'::text), ''::text)));
UniqueIndexTemplateVersionPresetsDefault UniqueConstraint = "idx_template_version_presets_default" // CREATE UNIQUE INDEX idx_template_version_presets_default ON template_version_presets USING btree (template_version_id) WHERE (is_default = true);
UniqueIndexUniquePresetName UniqueConstraint = "idx_unique_preset_name" // CREATE UNIQUE INDEX idx_unique_preset_name ON template_version_presets USING btree (name, template_version_id);
UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false);
UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false);
+3 -2
View File
@@ -41,8 +41,9 @@ func (api *API) templateVersionPresets(rw http.ResponseWriter, r *http.Request)
var res []codersdk.Preset
for _, preset := range presets {
sdkPreset := codersdk.Preset{
ID: preset.ID,
Name: preset.Name,
ID: preset.ID,
Name: preset.Name,
Default: preset.IsDefault,
}
for _, presetParam := range presetParams {
if presetParam.TemplateVersionPresetID != preset.ID {
+92
View File
@@ -1,8 +1,10 @@
package coderd_test
import (
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -137,3 +139,93 @@ func TestTemplateVersionPresets(t *testing.T) {
})
}
}
func TestTemplateVersionPresetsDefault(t *testing.T) {
t.Parallel()
type expectedPreset struct {
name string
isDefault bool
}
cases := []struct {
name string
presets []database.InsertPresetParams
expected []expectedPreset
}{
{
name: "no presets",
presets: nil,
expected: nil,
},
{
name: "single default preset",
presets: []database.InsertPresetParams{
{Name: "Default Preset", IsDefault: true},
},
expected: []expectedPreset{
{name: "Default Preset", isDefault: true},
},
},
{
name: "single non-default preset",
presets: []database.InsertPresetParams{
{Name: "Regular Preset", IsDefault: false},
},
expected: []expectedPreset{
{name: "Regular Preset", isDefault: false},
},
},
{
name: "mixed presets",
presets: []database.InsertPresetParams{
{Name: "Default Preset", IsDefault: true},
{Name: "Regular Preset", IsDefault: false},
},
expected: []expectedPreset{
{name: "Default Preset", isDefault: true},
{name: "Regular Preset", isDefault: false},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
// Create presets
for _, preset := range tc.presets {
preset.TemplateVersionID = version.ID
_ = dbgen.Preset(t, db, preset)
}
// Get presets via API
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll)
require.NoError(t, err)
userCtx := dbauthz.As(ctx, userSubject)
gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID)
require.NoError(t, err)
// Verify results
require.Len(t, gotPresets, len(tc.expected))
for _, expected := range tc.expected {
found := slices.ContainsFunc(gotPresets, func(preset codersdk.Preset) bool {
if preset.Name != expected.name {
return false
}
return assert.Equal(t, expected.isDefault, preset.Default)
})
require.True(t, found, "Expected preset %s not found", expected.name)
}
})
}
}
@@ -2230,6 +2230,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store,
DesiredInstances: desiredInstances,
InvalidateAfterSecs: ttl,
SchedulingTimezone: schedulingTimezone,
IsDefault: protoPreset.GetDefault(),
})
if err != nil {
return xerrors.Errorf("insert preset: %w", err)