mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add prebuild invalidation via last_invalidated_at timestamp (#20582)
Updates #17917
This commit is contained in:
Generated
+60
@@ -6002,6 +6002,41 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/prebuilds/invalidate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Invalidate presets for template",
|
||||
"operationId": "invalidate-presets-for-template",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template ID",
|
||||
"name": "template",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.InvalidatePresetsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -14889,6 +14924,31 @@ const docTemplate = `{
|
||||
"InsightsReportIntervalWeek"
|
||||
]
|
||||
},
|
||||
"codersdk.InvalidatePresetsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invalidated": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.InvalidatedPreset"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.InvalidatedPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"preset_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_version_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
Generated
+56
@@ -5309,6 +5309,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/prebuilds/invalidate": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Invalidate presets for template",
|
||||
"operationId": "invalidate-presets-for-template",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template ID",
|
||||
"name": "template",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.InvalidatePresetsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templates/{template}/versions": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -13487,6 +13518,31 @@
|
||||
"InsightsReportIntervalWeek"
|
||||
]
|
||||
},
|
||||
"codersdk.InvalidatePresetsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invalidated": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.InvalidatedPreset"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.InvalidatedPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"preset_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"template_version_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.IssueReconnectingPTYSignedTokenRequest": {
|
||||
"type": "object",
|
||||
"required": ["agentID", "url"],
|
||||
|
||||
@@ -1021,6 +1021,18 @@ func AIBridgeToolUsage(usage database.AIBridgeToolUsage) codersdk.AIBridgeToolUs
|
||||
}
|
||||
}
|
||||
|
||||
func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidatedAtRow) []codersdk.InvalidatedPreset {
|
||||
var presets []codersdk.InvalidatedPreset
|
||||
for _, p := range invalidatedPresets {
|
||||
presets = append(presets, codersdk.InvalidatedPreset{
|
||||
TemplateName: p.TemplateName,
|
||||
TemplateVersionName: p.TemplateVersionName,
|
||||
PresetName: p.TemplateVersionPresetName,
|
||||
})
|
||||
}
|
||||
return presets
|
||||
}
|
||||
|
||||
func jsonOrEmptyMap(rawMessage pqtype.NullRawMessage) map[string]any {
|
||||
var m map[string]any
|
||||
if !rawMessage.Valid {
|
||||
|
||||
@@ -4972,6 +4972,20 @@ func (q *querier) UpdatePresetPrebuildStatus(ctx context.Context, arg database.U
|
||||
return q.db.UpdatePresetPrebuildStatus(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
|
||||
// Fetch template to check authorization
|
||||
template, err := q.db.GetTemplateByID(ctx, arg.TemplateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, template); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return q.db.UpdatePresetsLastInvalidatedAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1315,6 +1315,13 @@ func (s *MethodTestSuite) TestTemplate() {
|
||||
dbm.EXPECT().UpsertTemplateUsageStats(gomock.Any()).Return(nil).AnyTimes()
|
||||
check.Asserts(rbac.ResourceSystem, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("UpdatePresetsLastInvalidatedAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
t1 := testutil.Fake(s.T(), faker, database.Template{})
|
||||
arg := database.UpdatePresetsLastInvalidatedAtParams{LastInvalidatedAt: sql.NullTime{Valid: true, Time: dbtime.Now()}, TemplateID: t1.ID}
|
||||
dbm.EXPECT().GetTemplateByID(gomock.Any(), t1.ID).Return(t1, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdatePresetsLastInvalidatedAt(gomock.Any(), arg).Return([]database.UpdatePresetsLastInvalidatedAtRow{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(t1, policy.ActionUpdate)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestUser() {
|
||||
|
||||
@@ -613,6 +613,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse {
|
||||
IsDefault: false,
|
||||
Description: preset.Description,
|
||||
Icon: preset.Icon,
|
||||
LastInvalidatedAt: preset.LastInvalidatedAt,
|
||||
})
|
||||
t.logger.Debug(context.Background(), "added preset",
|
||||
slog.F("preset_id", prst.ID),
|
||||
|
||||
@@ -1428,6 +1428,7 @@ func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) d
|
||||
IsDefault: seed.IsDefault,
|
||||
Description: seed.Description,
|
||||
Icon: seed.Icon,
|
||||
LastInvalidatedAt: seed.LastInvalidatedAt,
|
||||
})
|
||||
require.NoError(t, err, "insert preset")
|
||||
return preset
|
||||
|
||||
@@ -3070,6 +3070,13 @@ func (m queryMetricsStore) UpdatePresetPrebuildStatus(ctx context.Context, arg d
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdatePresetsLastInvalidatedAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdatePresetsLastInvalidatedAt").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg)
|
||||
|
||||
@@ -6598,6 +6598,21 @@ func (mr *MockStoreMockRecorder) UpdatePresetPrebuildStatus(ctx, arg any) *gomoc
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePresetPrebuildStatus", reflect.TypeOf((*MockStore)(nil).UpdatePresetPrebuildStatus), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdatePresetsLastInvalidatedAt mocks base method.
|
||||
func (m *MockStore) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg database.UpdatePresetsLastInvalidatedAtParams) ([]database.UpdatePresetsLastInvalidatedAtRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdatePresetsLastInvalidatedAt", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.UpdatePresetsLastInvalidatedAtRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdatePresetsLastInvalidatedAt indicates an expected call of UpdatePresetsLastInvalidatedAt.
|
||||
func (mr *MockStoreMockRecorder) UpdatePresetsLastInvalidatedAt(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePresetsLastInvalidatedAt", reflect.TypeOf((*MockStore)(nil).UpdatePresetsLastInvalidatedAt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateProvisionerDaemonLastSeenAt mocks base method.
|
||||
func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+2
-1
@@ -2170,7 +2170,8 @@ CREATE TABLE template_version_presets (
|
||||
scheduling_timezone text DEFAULT ''::text NOT NULL,
|
||||
is_default boolean DEFAULT false NOT NULL,
|
||||
description character varying(128) DEFAULT ''::character varying NOT NULL,
|
||||
icon character varying(256) DEFAULT ''::character varying NOT NULL
|
||||
icon character varying(256) DEFAULT ''::character varying NOT NULL,
|
||||
last_invalidated_at timestamp with time zone
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN template_version_presets.description IS 'Short text describing the preset (max 128 characters).';
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE template_version_presets DROP COLUMN last_invalidated_at;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE template_version_presets ADD COLUMN last_invalidated_at TIMESTAMPTZ;
|
||||
@@ -4452,7 +4452,8 @@ type TemplateVersionPreset struct {
|
||||
// Short text describing the preset (max 128 characters).
|
||||
Description string `db:"description" json:"description"`
|
||||
// URL or path to an icon representing the preset (max 256 characters).
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
||||
}
|
||||
|
||||
type TemplateVersionPresetParameter struct {
|
||||
|
||||
@@ -673,6 +673,7 @@ type sqlcQuerier interface {
|
||||
// This is an optimization to clean up stale pending jobs.
|
||||
UpdatePrebuildProvisionerJobWithCancel(ctx context.Context, arg UpdatePrebuildProvisionerJobWithCancelParams) ([]UpdatePrebuildProvisionerJobWithCancelRow, error)
|
||||
UpdatePresetPrebuildStatus(ctx context.Context, arg UpdatePresetPrebuildStatusParams) error
|
||||
UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error)
|
||||
UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error
|
||||
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
|
||||
UpdateProvisionerJobLogsLength(ctx context.Context, arg UpdateProvisionerJobLogsLengthParams) error
|
||||
|
||||
@@ -8709,6 +8709,7 @@ SELECT
|
||||
tvp.scheduling_timezone,
|
||||
tvp.invalidate_after_secs AS ttl,
|
||||
tvp.prebuild_status,
|
||||
tvp.last_invalidated_at,
|
||||
t.deleted,
|
||||
t.deprecated != '' AS deprecated
|
||||
FROM templates t
|
||||
@@ -8734,6 +8735,7 @@ type GetTemplatePresetsWithPrebuildsRow struct {
|
||||
SchedulingTimezone string `db:"scheduling_timezone" json:"scheduling_timezone"`
|
||||
Ttl sql.NullInt32 `db:"ttl" json:"ttl"`
|
||||
PrebuildStatus PrebuildStatus `db:"prebuild_status" json:"prebuild_status"`
|
||||
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Deprecated bool `db:"deprecated" json:"deprecated"`
|
||||
}
|
||||
@@ -8764,6 +8766,7 @@ func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templa
|
||||
&i.SchedulingTimezone,
|
||||
&i.Ttl,
|
||||
&i.PrebuildStatus,
|
||||
&i.LastInvalidatedAt,
|
||||
&i.Deleted,
|
||||
&i.Deprecated,
|
||||
); err != nil {
|
||||
@@ -8897,7 +8900,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, tvp.is_default, tvp.description, tvp.icon, 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, tvp.description, tvp.icon, tvp.last_invalidated_at, 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
|
||||
@@ -8915,6 +8918,7 @@ type GetPresetByIDRow struct {
|
||||
IsDefault bool `db:"is_default" json:"is_default"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
||||
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
@@ -8934,6 +8938,7 @@ func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (Get
|
||||
&i.IsDefault,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.LastInvalidatedAt,
|
||||
&i.TemplateID,
|
||||
&i.OrganizationID,
|
||||
)
|
||||
@@ -8942,7 +8947,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.is_default, template_version_presets.description, template_version_presets.icon
|
||||
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, template_version_presets.description, template_version_presets.icon, template_version_presets.last_invalidated_at
|
||||
FROM
|
||||
template_version_presets
|
||||
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
|
||||
@@ -8965,6 +8970,7 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
|
||||
&i.IsDefault,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.LastInvalidatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -9046,7 +9052,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, is_default, description, icon
|
||||
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
|
||||
FROM
|
||||
template_version_presets
|
||||
WHERE
|
||||
@@ -9074,6 +9080,7 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
||||
&i.IsDefault,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.LastInvalidatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -9099,7 +9106,8 @@ INSERT INTO template_version_presets (
|
||||
scheduling_timezone,
|
||||
is_default,
|
||||
description,
|
||||
icon
|
||||
icon,
|
||||
last_invalidated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
@@ -9111,8 +9119,9 @@ VALUES (
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
$10
|
||||
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon
|
||||
$10,
|
||||
$11
|
||||
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs, prebuild_status, scheduling_timezone, is_default, description, icon, last_invalidated_at
|
||||
`
|
||||
|
||||
type InsertPresetParams struct {
|
||||
@@ -9126,6 +9135,7 @@ type InsertPresetParams struct {
|
||||
IsDefault bool `db:"is_default" json:"is_default"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
|
||||
@@ -9140,6 +9150,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
|
||||
arg.IsDefault,
|
||||
arg.Description,
|
||||
arg.Icon,
|
||||
arg.LastInvalidatedAt,
|
||||
)
|
||||
var i TemplateVersionPreset
|
||||
err := row.Scan(
|
||||
@@ -9154,6 +9165,7 @@ func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (
|
||||
&i.IsDefault,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.LastInvalidatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -9249,6 +9261,57 @@ func (q *sqlQuerier) UpdatePresetPrebuildStatus(ctx context.Context, arg UpdateP
|
||||
return err
|
||||
}
|
||||
|
||||
const updatePresetsLastInvalidatedAt = `-- name: UpdatePresetsLastInvalidatedAt :many
|
||||
UPDATE
|
||||
template_version_presets tvp
|
||||
SET
|
||||
last_invalidated_at = $1
|
||||
FROM
|
||||
templates t
|
||||
JOIN template_versions tv ON tv.id = t.active_version_id
|
||||
WHERE
|
||||
t.id = $2
|
||||
AND tvp.template_version_id = tv.id
|
||||
RETURNING
|
||||
t.name AS template_name,
|
||||
tv.name AS template_version_name,
|
||||
tvp.name AS template_version_preset_name
|
||||
`
|
||||
|
||||
type UpdatePresetsLastInvalidatedAtParams struct {
|
||||
LastInvalidatedAt sql.NullTime `db:"last_invalidated_at" json:"last_invalidated_at"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
}
|
||||
|
||||
type UpdatePresetsLastInvalidatedAtRow struct {
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
|
||||
TemplateVersionPresetName string `db:"template_version_preset_name" json:"template_version_preset_name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdatePresetsLastInvalidatedAt(ctx context.Context, arg UpdatePresetsLastInvalidatedAtParams) ([]UpdatePresetsLastInvalidatedAtRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, updatePresetsLastInvalidatedAt, arg.LastInvalidatedAt, arg.TemplateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []UpdatePresetsLastInvalidatedAtRow
|
||||
for rows.Next() {
|
||||
var i UpdatePresetsLastInvalidatedAtRow
|
||||
if err := rows.Scan(&i.TemplateName, &i.TemplateVersionName, &i.TemplateVersionPresetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const deleteOldProvisionerDaemons = `-- name: DeleteOldProvisionerDaemons :exec
|
||||
DELETE FROM provisioner_daemons WHERE (
|
||||
(created_at < (NOW() - INTERVAL '7 days') AND last_seen_at IS NULL) OR
|
||||
|
||||
@@ -51,6 +51,7 @@ SELECT
|
||||
tvp.scheduling_timezone,
|
||||
tvp.invalidate_after_secs AS ttl,
|
||||
tvp.prebuild_status,
|
||||
tvp.last_invalidated_at,
|
||||
t.deleted,
|
||||
t.deprecated != '' AS deprecated
|
||||
FROM templates t
|
||||
|
||||
@@ -9,7 +9,8 @@ INSERT INTO template_version_presets (
|
||||
scheduling_timezone,
|
||||
is_default,
|
||||
description,
|
||||
icon
|
||||
icon,
|
||||
last_invalidated_at
|
||||
)
|
||||
VALUES (
|
||||
@id,
|
||||
@@ -21,7 +22,8 @@ VALUES (
|
||||
@scheduling_timezone,
|
||||
@is_default,
|
||||
@description,
|
||||
@icon
|
||||
@icon,
|
||||
@last_invalidated_at
|
||||
) RETURNING *;
|
||||
|
||||
-- name: InsertPresetParameters :many
|
||||
@@ -103,3 +105,19 @@ WHERE
|
||||
tv.id = t.active_version_id
|
||||
AND NOT t.deleted
|
||||
AND t.deprecated = '';
|
||||
|
||||
-- name: UpdatePresetsLastInvalidatedAt :many
|
||||
UPDATE
|
||||
template_version_presets tvp
|
||||
SET
|
||||
last_invalidated_at = @last_invalidated_at
|
||||
FROM
|
||||
templates t
|
||||
JOIN template_versions tv ON tv.id = t.active_version_id
|
||||
WHERE
|
||||
t.id = @template_id
|
||||
AND tvp.template_version_id = tv.id
|
||||
RETURNING
|
||||
t.name AS template_name,
|
||||
tv.name AS template_version_name,
|
||||
tvp.name AS template_version_preset_name;
|
||||
|
||||
@@ -125,20 +125,29 @@ func (s GlobalSnapshot) IsHardLimited(presetID uuid.UUID) bool {
|
||||
}
|
||||
|
||||
// filterExpiredWorkspaces splits running workspaces into expired and non-expired
|
||||
// based on the preset's TTL.
|
||||
// If TTL is missing or zero, all workspaces are considered non-expired.
|
||||
// based on the preset's TTL and last_invalidated_at timestamp.
|
||||
// A prebuild is considered expired if:
|
||||
// 1. The preset has been invalidated (last_invalidated_at is set), OR
|
||||
// 2. It exceeds the preset's TTL (if TTL is set)
|
||||
// If TTL is missing or zero, only last_invalidated_at is checked.
|
||||
func filterExpiredWorkspaces(preset database.GetTemplatePresetsWithPrebuildsRow, runningWorkspaces []database.GetRunningPrebuiltWorkspacesRow) (nonExpired []database.GetRunningPrebuiltWorkspacesRow, expired []database.GetRunningPrebuiltWorkspacesRow) {
|
||||
if !preset.Ttl.Valid {
|
||||
return runningWorkspaces, expired
|
||||
}
|
||||
|
||||
ttl := time.Duration(preset.Ttl.Int32) * time.Second
|
||||
if ttl <= 0 {
|
||||
return runningWorkspaces, expired
|
||||
}
|
||||
|
||||
for _, prebuild := range runningWorkspaces {
|
||||
if time.Since(prebuild.CreatedAt) > ttl {
|
||||
isExpired := false
|
||||
|
||||
// Check if prebuild was created before last invalidation
|
||||
if preset.LastInvalidatedAt.Valid && prebuild.CreatedAt.Before(preset.LastInvalidatedAt.Time) {
|
||||
isExpired = true
|
||||
}
|
||||
|
||||
// Check TTL expiration if set
|
||||
if !isExpired && preset.Ttl.Valid {
|
||||
ttl := time.Duration(preset.Ttl.Int32) * time.Second
|
||||
if ttl > 0 && time.Since(prebuild.CreatedAt) > ttl {
|
||||
isExpired = true
|
||||
}
|
||||
}
|
||||
|
||||
if isExpired {
|
||||
expired = append(expired, prebuild)
|
||||
} else {
|
||||
nonExpired = append(nonExpired, prebuild)
|
||||
|
||||
@@ -600,6 +600,9 @@ func TestExpiredPrebuilds(t *testing.T) {
|
||||
running int32
|
||||
desired int32
|
||||
expired int32
|
||||
|
||||
invalidated int32
|
||||
|
||||
checkFn func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions)
|
||||
}{
|
||||
// With 2 running prebuilds, none of which are expired, and the desired count is met,
|
||||
@@ -708,6 +711,52 @@ func TestExpiredPrebuilds(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preset has been invalidated - both instances expired",
|
||||
running: 2,
|
||||
desired: 2,
|
||||
expired: 0,
|
||||
invalidated: 2,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 2}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID, runningPrebuilds[1].ID},
|
||||
},
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 2,
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preset has been invalidated, but one prebuild instance is newer",
|
||||
running: 2,
|
||||
desired: 2,
|
||||
expired: 0,
|
||||
invalidated: 1,
|
||||
checkFn: func(runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, state prebuilds.ReconciliationState, actions []*prebuilds.ReconciliationActions) {
|
||||
expectedState := prebuilds.ReconciliationState{Actual: 2, Desired: 2, Expired: 1}
|
||||
expectedActions := []*prebuilds.ReconciliationActions{
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeDelete,
|
||||
DeleteIDs: []uuid.UUID{runningPrebuilds[0].ID},
|
||||
},
|
||||
{
|
||||
ActionType: prebuilds.ActionTypeCreate,
|
||||
Create: 1,
|
||||
},
|
||||
}
|
||||
|
||||
validateState(t, expectedState, state)
|
||||
validateActions(t, expectedActions, actions)
|
||||
},
|
||||
@@ -719,7 +768,17 @@ func TestExpiredPrebuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// GIVEN: a preset.
|
||||
defaultPreset := preset(true, tc.desired, current)
|
||||
now := time.Now()
|
||||
invalidatedAt := now.Add(1 * time.Minute)
|
||||
|
||||
var muts []func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow
|
||||
if tc.invalidated > 0 {
|
||||
muts = append(muts, func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow {
|
||||
row.LastInvalidatedAt = sql.NullTime{Valid: true, Time: invalidatedAt}
|
||||
return row
|
||||
})
|
||||
}
|
||||
defaultPreset := preset(true, tc.desired, current, muts...)
|
||||
presets := []database.GetTemplatePresetsWithPrebuildsRow{
|
||||
defaultPreset,
|
||||
}
|
||||
@@ -727,11 +786,22 @@ func TestExpiredPrebuilds(t *testing.T) {
|
||||
// GIVEN: running prebuilt workspaces for the preset.
|
||||
running := make([]database.GetRunningPrebuiltWorkspacesRow, 0, tc.running)
|
||||
expiredCount := 0
|
||||
invalidatedCount := 0
|
||||
ttlDuration := time.Duration(defaultPreset.Ttl.Int32)
|
||||
for range tc.running {
|
||||
name, err := prebuilds.GenerateName()
|
||||
require.NoError(t, err)
|
||||
|
||||
prebuildCreateAt := time.Now()
|
||||
if int(tc.invalidated) > invalidatedCount {
|
||||
prebuildCreateAt = prebuildCreateAt.Add(-ttlDuration - 10*time.Second)
|
||||
invalidatedCount++
|
||||
} else if invalidatedCount > 0 {
|
||||
// Only `tc.invalidated` instances have been invalidated,
|
||||
// so the next instance is assumed to be created after `invalidatedAt`.
|
||||
prebuildCreateAt = invalidatedAt.Add(1 * time.Minute)
|
||||
}
|
||||
|
||||
if int(tc.expired) > expiredCount {
|
||||
// Update the prebuild workspace createdAt to exceed its TTL (5 seconds)
|
||||
prebuildCreateAt = prebuildCreateAt.Add(-ttlDuration - 10*time.Second)
|
||||
|
||||
@@ -2581,6 +2581,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store,
|
||||
IsDefault: protoPreset.GetDefault(),
|
||||
Description: protoPreset.Description,
|
||||
Icon: protoPreset.Icon,
|
||||
LastInvalidatedAt: sql.NullTime{},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert preset: %w", err)
|
||||
|
||||
@@ -513,3 +513,34 @@ func (c *Client) StarterTemplates(ctx context.Context) ([]TemplateExample, error
|
||||
var templateExamples []TemplateExample
|
||||
return templateExamples, json.NewDecoder(res.Body).Decode(&templateExamples)
|
||||
}
|
||||
|
||||
type InvalidatePresetsResponse struct {
|
||||
Invalidated []InvalidatedPreset `json:"invalidated"`
|
||||
}
|
||||
|
||||
type InvalidatedPreset struct {
|
||||
TemplateName string `json:"template_name"`
|
||||
TemplateVersionName string `json:"template_version_name"`
|
||||
PresetName string `json:"preset_name"`
|
||||
}
|
||||
|
||||
// InvalidateTemplatePresets invalidates all presets for the
|
||||
// template's active version by setting last_invalidated_at timestamp.
|
||||
// The reconciler will then mark these prebuilds as expired and create new ones.
|
||||
func (c *Client) InvalidateTemplatePresets(ctx context.Context, template uuid.UUID) (InvalidatePresetsResponse, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost,
|
||||
fmt.Sprintf("/api/v2/templates/%s/prebuilds/invalidate", template),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return InvalidatePresetsResponse{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return InvalidatePresetsResponse{}, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var response InvalidatePresetsResponse
|
||||
return response, json.NewDecoder(res.Body).Decode(&response)
|
||||
}
|
||||
|
||||
Generated
+43
@@ -3788,6 +3788,49 @@ Status Code **200**
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Invalidate presets for template
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/templates/{template}/prebuilds/invalidate \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /templates/{template}/prebuilds/invalidate`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|------------|------|--------------|----------|-------------|
|
||||
| `template` | path | string(uuid) | true | Template ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"invalidated": [
|
||||
{
|
||||
"preset_name": "string",
|
||||
"template_name": "string",
|
||||
"template_version_name": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|------------------------------------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.InvalidatePresetsResponse](schemas.md#codersdkinvalidatepresetsresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get user quiet hours schedule
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+38
@@ -4715,6 +4715,44 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `day` |
|
||||
| `week` |
|
||||
|
||||
## codersdk.InvalidatePresetsResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"invalidated": [
|
||||
{
|
||||
"preset_name": "string",
|
||||
"template_name": "string",
|
||||
"template_version_name": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------------|-------------------------------------------------------------------|----------|--------------|-------------|
|
||||
| `invalidated` | array of [codersdk.InvalidatedPreset](#codersdkinvalidatedpreset) | false | | |
|
||||
|
||||
## codersdk.InvalidatedPreset
|
||||
|
||||
```json
|
||||
{
|
||||
"preset_name": "string",
|
||||
"template_name": "string",
|
||||
"template_version_name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-------------------------|--------|----------|--------------|-------------|
|
||||
| `preset_name` | string | false | | |
|
||||
| `template_name` | string | false | | |
|
||||
| `template_version_name` | string | false | | |
|
||||
|
||||
## codersdk.IssueReconnectingPTYSignedTokenRequest
|
||||
|
||||
```json
|
||||
|
||||
@@ -458,6 +458,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Get("/", api.templateACL)
|
||||
r.Patch("/", api.patchTemplateACL)
|
||||
})
|
||||
r.Route("/templates/{template}/prebuilds", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.templateRBACEnabledMW,
|
||||
apiKeyMiddleware,
|
||||
httpmw.ExtractTemplateParam(api.Database),
|
||||
)
|
||||
r.Post("/invalidate", api.postInvalidateTemplatePresets)
|
||||
})
|
||||
|
||||
r.Route("/groups", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.templateRBACEnabledMW,
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
@@ -338,3 +340,45 @@ func (api *API) RequireFeatureMW(feat codersdk.FeatureName) func(http.Handler) h
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Invalidate presets for template
|
||||
// @ID invalidate-presets-for-template
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param template path string true "Template ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.InvalidatePresetsResponse
|
||||
// @Router /templates/{template}/prebuilds/invalidate [post]
|
||||
func (api *API) postInvalidateTemplatePresets(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
template := httpmw.TemplateParam(r)
|
||||
|
||||
// Authorization: user must be able to update the template
|
||||
if !api.Authorize(r, policy.ActionUpdate, template) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Update last_invalidated_at for all presets of the active template version
|
||||
invalidatedPresets, err := api.Database.UpdatePresetsLastInvalidatedAt(ctx, database.UpdatePresetsLastInvalidatedAtParams{
|
||||
TemplateID: template.ID,
|
||||
LastInvalidatedAt: sql.NullTime{Time: api.Clock.Now(), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to invalidate presets.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
api.Logger.Info(ctx, "invalidated presets",
|
||||
slog.F("template_id", template.ID),
|
||||
slog.F("template_name", template.Name),
|
||||
slog.F("preset_count", len(invalidatedPresets)),
|
||||
)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.InvalidatePresetsResponse{
|
||||
Invalidated: db2sdk.InvalidatedPresets(invalidatedPresets),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2111,3 +2111,100 @@ func TestMultipleOrganizationTemplates(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateTemplatePrebuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given the following parameters and presets...
|
||||
templateVersionParameters := []*proto.RichParameter{
|
||||
{Name: "param1", Type: "string", Required: false, DefaultValue: "default1"},
|
||||
{Name: "param2", Type: "string", Required: false, DefaultValue: "default2"},
|
||||
{Name: "param3", Type: "string", Required: false, DefaultValue: "default3"},
|
||||
}
|
||||
presetWithParameters1 := &proto.Preset{
|
||||
Name: "Preset With Parameters 1",
|
||||
Parameters: []*proto.PresetParameter{
|
||||
{Name: "param1", Value: "value1"},
|
||||
{Name: "param2", Value: "value2"},
|
||||
{Name: "param3", Value: "value3"},
|
||||
},
|
||||
}
|
||||
presetWithParameters2 := &proto.Preset{
|
||||
Name: "Preset With Parameters 2",
|
||||
Parameters: []*proto.PresetParameter{
|
||||
{Name: "param1", Value: "value4"},
|
||||
{Name: "param2", Value: "value5"},
|
||||
{Name: "param3", Value: "value6"},
|
||||
},
|
||||
}
|
||||
|
||||
presetWithParameters3 := &proto.Preset{
|
||||
Name: "Preset With Parameters 3",
|
||||
Parameters: []*proto.PresetParameter{
|
||||
{Name: "param1", Value: "value7"},
|
||||
{Name: "param2", Value: "value8"},
|
||||
{Name: "param3", Value: "value9"},
|
||||
},
|
||||
}
|
||||
|
||||
// Given the template versions and template...
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
buildPlanResponse := func(presets ...*proto.Preset) *proto.Response {
|
||||
return &proto.Response{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Presets: presets,
|
||||
Parameters: templateVersionParameters,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
version1 := coderdtest.CreateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters1, presetWithParameters2)},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, templateAdminClient, owner.OrganizationID, version1.ID)
|
||||
|
||||
// When
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
invalidated, err := templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then
|
||||
require.Len(t, invalidated.Invalidated, 2)
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version1.Name, PresetName: presetWithParameters1.Name}, invalidated.Invalidated[0])
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version1.Name, PresetName: presetWithParameters2.Name}, invalidated.Invalidated[1])
|
||||
|
||||
// Given the template is updated...
|
||||
version2 := coderdtest.UpdateTemplateVersion(t, templateAdminClient, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{buildPlanResponse(presetWithParameters2, presetWithParameters3)},
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}, template.ID)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version2.ID)
|
||||
err = templateAdminClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{ID: version2.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// When
|
||||
invalidated, err = templateAdminClient.InvalidateTemplatePresets(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: it should only invalidate the presets from the currently active version (preset2 and preset3)
|
||||
require.Len(t, invalidated.Invalidated, 2)
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters2.Name}, invalidated.Invalidated[0])
|
||||
require.Equal(t, codersdk.InvalidatedPreset{TemplateName: template.Name, TemplateVersionName: version2.Name, PresetName: presetWithParameters3.Name}, invalidated.Invalidated[1])
|
||||
}
|
||||
|
||||
Generated
+12
@@ -2481,6 +2481,18 @@ export const InsightsReportIntervals: InsightsReportInterval[] = [
|
||||
"week",
|
||||
];
|
||||
|
||||
// From codersdk/templates.go
|
||||
export interface InvalidatePresetsResponse {
|
||||
readonly invalidated: readonly InvalidatedPreset[];
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
export interface InvalidatedPreset {
|
||||
readonly template_name: string;
|
||||
readonly template_version_name: string;
|
||||
readonly preset_name: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface IssueReconnectingPTYSignedTokenRequest {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user