mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow for default presets (#18445)
This commit is contained in:
Generated
+3
@@ -14519,6 +14519,9 @@ const docTemplate = `{
|
||||
"codersdk.Preset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -13145,6 +13145,9 @@
|
||||
"codersdk.Preset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -416,6 +416,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
|
||||
DesiredInstances: preset.DesiredInstances,
|
||||
InvalidateAfterSecs: preset.InvalidateAfterSecs,
|
||||
SchedulingTimezone: preset.SchedulingTimezone,
|
||||
IsDefault: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+4
-1
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user