mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add migrations and queries to support prebuilds (#16891)
Depends on https://github.com/coder/coder/pull/16916 _(change base to `main` once it is merged)_ Closes https://github.com/coder/internal/issues/514 _This is one of several PRs to decompose the `dk/prebuilds` feature branch into separate PRs to merge into `main`._ --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||
|
||||
@@ -361,6 +362,27 @@ var (
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
subjectPrebuildsOrchestrator = rbac.Subject{
|
||||
FriendlyName: "Prebuilds Orchestrator",
|
||||
ID: prebuilds.SystemUserID.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "prebuilds-orchestrator"},
|
||||
DisplayName: "Coder",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
// May use template, read template-related info, & insert template-related resources (preset prebuilds).
|
||||
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionUse, policy.ActionViewInsights},
|
||||
// May CRUD workspaces, and start/stop them.
|
||||
rbac.ResourceWorkspace.Type: {
|
||||
policy.ActionCreate, policy.ActionDelete, policy.ActionRead, policy.ActionUpdate,
|
||||
policy.ActionWorkspaceStart, policy.ActionWorkspaceStop,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
)
|
||||
|
||||
// AsProvisionerd returns a context with an actor that has permissions required
|
||||
@@ -415,6 +437,12 @@ func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons)
|
||||
}
|
||||
|
||||
// AsPrebuildsOrchestrator returns a context with an actor that has permissions
|
||||
// to read orchestrator workspace prebuilds.
|
||||
func AsPrebuildsOrchestrator(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectPrebuildsOrchestrator)
|
||||
}
|
||||
|
||||
var AsRemoveActor = rbac.Subject{
|
||||
ID: "remove-actor",
|
||||
}
|
||||
@@ -1109,6 +1137,31 @@ func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg data
|
||||
return q.db.BulkMarkNotificationMessagesSent(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
|
||||
empty := database.ClaimPrebuiltWorkspaceRow{}
|
||||
|
||||
preset, err := q.db.GetPresetByID(ctx, arg.PresetID)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
workspaceObject := rbac.ResourceWorkspace.WithOwner(arg.NewUserID.String()).InOrg(preset.OrganizationID)
|
||||
err = q.authorizeContext(ctx, policy.ActionCreate, workspaceObject.RBACObject())
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
tpl, err := q.GetTemplateByID(ctx, preset.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return empty, xerrors.Errorf("verify template by id: %w", err)
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUse, tpl); err != nil {
|
||||
return empty, xerrors.Errorf("use template for workspace: %w", err)
|
||||
}
|
||||
|
||||
return q.db.ClaimPrebuiltWorkspace(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) CleanTailnetCoordinators(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
|
||||
return err
|
||||
@@ -1130,6 +1183,13 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
|
||||
return q.db.CleanTailnetTunnels(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) CountInProgressPrebuilds(ctx context.Context) ([]database.CountInProgressPrebuildsRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.CountInProgressPrebuilds(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceInboxNotification.WithOwner(userID.String())); err != nil {
|
||||
return 0, err
|
||||
@@ -2096,6 +2156,30 @@ func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUI
|
||||
return q.db.GetParameterSchemasByJobID(ctx, jobID)
|
||||
}
|
||||
|
||||
func (q *querier) GetPrebuildMetrics(ctx context.Context) ([]database.GetPrebuildMetricsRow, error) {
|
||||
// GetPrebuildMetrics returns metrics related to prebuilt workspaces,
|
||||
// such as the number of created and failed prebuilt workspaces.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetPrebuildMetrics(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
|
||||
empty := database.GetPresetByIDRow{}
|
||||
|
||||
preset, err := q.db.GetPresetByID(ctx, presetID)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
_, err = q.GetTemplateByID(ctx, preset.TemplateID.UUID)
|
||||
if err != nil {
|
||||
return empty, err
|
||||
}
|
||||
|
||||
return preset, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate); err != nil {
|
||||
return database.TemplateVersionPreset{}, err
|
||||
@@ -2113,6 +2197,14 @@ func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, te
|
||||
return q.db.GetPresetParametersByTemplateVersionID(ctx, templateVersionID)
|
||||
}
|
||||
|
||||
func (q *querier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) {
|
||||
// GetPresetsBackoff returns a list of template version presets along with metadata such as the number of failed prebuilds.
|
||||
if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetPresetsBackoff(ctx, lookback)
|
||||
}
|
||||
|
||||
func (q *querier) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||
// An actor can read template version presets if they can read the related template version.
|
||||
_, err := q.GetTemplateVersionByID(ctx, templateVersionID)
|
||||
@@ -2164,13 +2256,13 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data
|
||||
// can read the job.
|
||||
_, err := q.GetWorkspaceBuildByJobID(ctx, id)
|
||||
if err != nil {
|
||||
return database.ProvisionerJob{}, err
|
||||
return database.ProvisionerJob{}, xerrors.Errorf("fetch related workspace build: %w", err)
|
||||
}
|
||||
case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport:
|
||||
// Authorized call to get template version.
|
||||
_, err := authorizedTemplateVersionFromJob(ctx, q, job)
|
||||
if err != nil {
|
||||
return database.ProvisionerJob{}, err
|
||||
return database.ProvisionerJob{}, xerrors.Errorf("fetch related template version: %w", err)
|
||||
}
|
||||
default:
|
||||
return database.ProvisionerJob{}, xerrors.Errorf("unknown job type: %q", job.Type)
|
||||
@@ -2263,6 +2355,14 @@ func (q *querier) GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Ti
|
||||
return q.db.GetReplicasUpdatedAfter(ctx, updatedAt)
|
||||
}
|
||||
|
||||
func (q *querier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]database.GetRunningPrebuiltWorkspacesRow, error) {
|
||||
// This query returns only prebuilt workspaces, but we decided to require permissions for all workspaces.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceWorkspace.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetRunningPrebuiltWorkspaces(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return "", err
|
||||
@@ -2387,6 +2487,15 @@ func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database
|
||||
return q.db.GetTemplateParameterInsights(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]database.GetTemplatePresetsWithPrebuildsRow, error) {
|
||||
// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
|
||||
// Presets and prebuilds are part of the template, so if you can access templates - you can access them as well.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetTemplatePresetsWithPrebuilds(ctx, templateID)
|
||||
}
|
||||
|
||||
func (q *querier) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||
if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4838,6 +4838,96 @@ func (s *MethodTestSuite) TestNotifications() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestPrebuilds() {
|
||||
s.Run("ClaimPrebuiltWorkspace", s.Subtest(func(db database.Store, check *expects) {
|
||||
org := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
user := dbgen.User(s.T(), db, database.User{})
|
||||
template := dbgen.Template(s.T(), db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
preset := dbgen.Preset(s.T(), db, database.InsertPresetParams{
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
})
|
||||
check.Args(database.ClaimPrebuiltWorkspaceParams{
|
||||
NewUserID: user.ID,
|
||||
NewName: "",
|
||||
PresetID: preset.ID,
|
||||
}).Asserts(
|
||||
rbac.ResourceWorkspace.WithOwner(user.ID.String()).InOrg(org.ID), policy.ActionCreate,
|
||||
template, policy.ActionRead,
|
||||
template, policy.ActionUse,
|
||||
).ErrorsWithInMemDB(dbmem.ErrUnimplemented).
|
||||
ErrorsWithPG(sql.ErrNoRows)
|
||||
}))
|
||||
s.Run("GetPrebuildMetrics", s.Subtest(func(_ database.Store, check *expects) {
|
||||
check.Args().
|
||||
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead).
|
||||
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
|
||||
}))
|
||||
s.Run("CountInProgressPrebuilds", s.Subtest(func(_ database.Store, check *expects) {
|
||||
check.Args().
|
||||
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead).
|
||||
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
|
||||
}))
|
||||
s.Run("GetPresetsBackoff", s.Subtest(func(_ database.Store, check *expects) {
|
||||
check.Args(time.Time{}).
|
||||
Asserts(rbac.ResourceTemplate.All(), policy.ActionViewInsights).
|
||||
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
|
||||
}))
|
||||
s.Run("GetRunningPrebuiltWorkspaces", s.Subtest(func(_ database.Store, check *expects) {
|
||||
check.Args().
|
||||
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead).
|
||||
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
|
||||
}))
|
||||
s.Run("GetTemplatePresetsWithPrebuilds", s.Subtest(func(db database.Store, check *expects) {
|
||||
user := dbgen.User(s.T(), db, database.User{})
|
||||
check.Args(uuid.NullUUID{UUID: user.ID, Valid: true}).
|
||||
Asserts(rbac.ResourceTemplate.All(), policy.ActionRead).
|
||||
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
|
||||
}))
|
||||
s.Run("GetPresetByID", s.Subtest(func(db database.Store, check *expects) {
|
||||
org := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
user := dbgen.User(s.T(), db, database.User{})
|
||||
template := dbgen.Template(s.T(), db, database.Template{
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
OrganizationID: org.ID,
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
preset := dbgen.Preset(s.T(), db, database.InsertPresetParams{
|
||||
TemplateVersionID: templateVersion.ID,
|
||||
})
|
||||
check.Args(preset.ID).
|
||||
Asserts(template, policy.ActionRead).
|
||||
Returns(database.GetPresetByIDRow{
|
||||
ID: preset.ID,
|
||||
TemplateVersionID: preset.TemplateVersionID,
|
||||
Name: preset.Name,
|
||||
CreatedAt: preset.CreatedAt,
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: template.ID,
|
||||
Valid: true,
|
||||
},
|
||||
OrganizationID: org.ID,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
||||
s.Run("GetOAuth2ProviderApps", s.Subtest(func(db database.Store, check *expects) {
|
||||
apps := []database.OAuth2ProviderApp{
|
||||
|
||||
@@ -1196,6 +1196,29 @@ func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem)
|
||||
return item
|
||||
}
|
||||
|
||||
func Preset(t testing.TB, db database.Store, seed database.InsertPresetParams) database.TemplateVersionPreset {
|
||||
preset, err := db.InsertPreset(genCtx, database.InsertPresetParams{
|
||||
TemplateVersionID: takeFirst(seed.TemplateVersionID, uuid.New()),
|
||||
Name: takeFirst(seed.Name, testutil.GetRandomName(t)),
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
DesiredInstances: seed.DesiredInstances,
|
||||
InvalidateAfterSecs: seed.InvalidateAfterSecs,
|
||||
})
|
||||
require.NoError(t, err, "insert preset")
|
||||
return preset
|
||||
}
|
||||
|
||||
func PresetParameter(t testing.TB, db database.Store, seed database.InsertPresetParametersParams) []database.TemplateVersionPresetParameter {
|
||||
parameters, err := db.InsertPresetParameters(genCtx, database.InsertPresetParametersParams{
|
||||
TemplateVersionPresetID: takeFirst(seed.TemplateVersionPresetID, uuid.New()),
|
||||
Names: takeFirstSlice(seed.Names, []string{testutil.GetRandomName(t)}),
|
||||
Values: takeFirstSlice(seed.Values, []string{testutil.GetRandomName(t)}),
|
||||
})
|
||||
|
||||
require.NoError(t, err, "insert preset parameters")
|
||||
return parameters
|
||||
}
|
||||
|
||||
func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
|
||||
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
|
||||
JobID: takeFirst(seed.JobID, uuid.New()),
|
||||
|
||||
@@ -1741,6 +1741,10 @@ func (*FakeQuerier) BulkMarkNotificationMessagesSent(_ context.Context, arg data
|
||||
return int64(len(arg.IDs)), nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
|
||||
return database.ClaimPrebuiltWorkspaceRow{}, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (*FakeQuerier) CleanTailnetCoordinators(_ context.Context) error {
|
||||
return ErrUnimplemented
|
||||
}
|
||||
@@ -1753,6 +1757,10 @@ func (*FakeQuerier) CleanTailnetTunnels(context.Context) error {
|
||||
return ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) CountInProgressPrebuilds(ctx context.Context) ([]database.CountInProgressPrebuildsRow, error) {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) CountUnreadInboxNotificationsByUserID(_ context.Context, userID uuid.UUID) (int64, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -4212,6 +4220,44 @@ func (q *FakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
|
||||
return parameters, nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) GetPrebuildMetrics(_ context.Context) ([]database.GetPrebuildMetricsRow, error) {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
empty := database.GetPresetByIDRow{}
|
||||
|
||||
// Create an index for faster lookup
|
||||
versionMap := make(map[uuid.UUID]database.TemplateVersionTable)
|
||||
for _, tv := range q.templateVersions {
|
||||
versionMap[tv.ID] = tv
|
||||
}
|
||||
|
||||
for _, preset := range q.presets {
|
||||
if preset.ID == presetID {
|
||||
tv, ok := versionMap[preset.TemplateVersionID]
|
||||
if !ok {
|
||||
return empty, fmt.Errorf("template version %v does not exist", preset.TemplateVersionID)
|
||||
}
|
||||
return database.GetPresetByIDRow{
|
||||
ID: preset.ID,
|
||||
TemplateVersionID: preset.TemplateVersionID,
|
||||
Name: preset.Name,
|
||||
CreatedAt: preset.CreatedAt,
|
||||
DesiredInstances: preset.DesiredInstances,
|
||||
InvalidateAfterSecs: preset.InvalidateAfterSecs,
|
||||
TemplateID: tv.TemplateID,
|
||||
OrganizationID: tv.OrganizationID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return empty, fmt.Errorf("preset %v does not exist", presetID)
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -4254,6 +4300,10 @@ func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context,
|
||||
return parameters, nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) GetPresetsBackoff(_ context.Context, _ time.Time) ([]database.GetPresetsBackoffRow, error) {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetPresetsByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -4917,6 +4967,10 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.
|
||||
return replicas, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]database.GetRunningPrebuiltWorkspacesRow, error) {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetRuntimeConfig(_ context.Context, key string) (string, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
@@ -5956,6 +6010,10 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func (*FakeQuerier) GetTemplatePresetsWithPrebuilds(_ context.Context, _ uuid.NullUUID) ([]database.GetTemplatePresetsWithPrebuildsRow, error) {
|
||||
return nil, ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetTemplateUsageStats(_ context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
@@ -6426,6 +6484,10 @@ func (q *FakeQuerier) GetUserCount(_ context.Context, includeSystem bool) (int64
|
||||
if !u.Deleted {
|
||||
existing++
|
||||
}
|
||||
|
||||
if !includeSystem && u.IsSystem {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
@@ -158,6 +158,13 @@ func (m queryMetricsStore) BulkMarkNotificationMessagesSent(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ClaimPrebuiltWorkspace(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("ClaimPrebuiltWorkspace").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CleanTailnetCoordinators(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
err := m.s.CleanTailnetCoordinators(ctx)
|
||||
@@ -179,6 +186,13 @@ func (m queryMetricsStore) CleanTailnetTunnels(ctx context.Context) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountInProgressPrebuilds(ctx context.Context) ([]database.CountInProgressPrebuildsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountInProgressPrebuilds(ctx)
|
||||
m.queryLatencies.WithLabelValues("CountInProgressPrebuilds").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountUnreadInboxNotificationsByUserID(ctx, userID)
|
||||
@@ -1075,6 +1089,20 @@ func (m queryMetricsStore) GetParameterSchemasByJobID(ctx context.Context, jobID
|
||||
return schemas, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetPrebuildMetrics(ctx context.Context) ([]database.GetPrebuildMetricsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetPrebuildMetrics(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetPrebuildMetrics").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetPresetByID(ctx, presetID)
|
||||
m.queryLatencies.WithLabelValues("GetPresetByID").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetPresetByWorkspaceBuildID(ctx, workspaceBuildID)
|
||||
@@ -1089,6 +1117,13 @@ func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Co
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetPresetsBackoff(ctx, lookback)
|
||||
m.queryLatencies.WithLabelValues("GetPresetsBackoff").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetPresetsByTemplateVersionID(ctx, templateVersionID)
|
||||
@@ -1222,6 +1257,13 @@ func (m queryMetricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedA
|
||||
return replicas, err
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]database.GetRunningPrebuiltWorkspacesRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetRunningPrebuiltWorkspaces(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetRunningPrebuiltWorkspaces").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetRuntimeConfig(ctx, key)
|
||||
@@ -1348,6 +1390,13 @@ func (m queryMetricsStore) GetTemplateParameterInsights(ctx context.Context, arg
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]database.GetTemplatePresetsWithPrebuildsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplatePresetsWithPrebuilds(ctx, templateID)
|
||||
m.queryLatencies.WithLabelValues("GetTemplatePresetsWithPrebuilds").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetTemplateUsageStats(ctx, arg)
|
||||
|
||||
@@ -190,6 +190,21 @@ func (mr *MockStoreMockRecorder) BulkMarkNotificationMessagesSent(ctx, arg any)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BulkMarkNotificationMessagesSent", reflect.TypeOf((*MockStore)(nil).BulkMarkNotificationMessagesSent), ctx, arg)
|
||||
}
|
||||
|
||||
// ClaimPrebuiltWorkspace mocks base method.
|
||||
func (m *MockStore) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ClaimPrebuiltWorkspace", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ClaimPrebuiltWorkspaceRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ClaimPrebuiltWorkspace indicates an expected call of ClaimPrebuiltWorkspace.
|
||||
func (mr *MockStoreMockRecorder) ClaimPrebuiltWorkspace(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClaimPrebuiltWorkspace", reflect.TypeOf((*MockStore)(nil).ClaimPrebuiltWorkspace), ctx, arg)
|
||||
}
|
||||
|
||||
// CleanTailnetCoordinators mocks base method.
|
||||
func (m *MockStore) CleanTailnetCoordinators(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -232,6 +247,21 @@ func (mr *MockStoreMockRecorder) CleanTailnetTunnels(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetTunnels", reflect.TypeOf((*MockStore)(nil).CleanTailnetTunnels), ctx)
|
||||
}
|
||||
|
||||
// CountInProgressPrebuilds mocks base method.
|
||||
func (m *MockStore) CountInProgressPrebuilds(ctx context.Context) ([]database.CountInProgressPrebuildsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CountInProgressPrebuilds", ctx)
|
||||
ret0, _ := ret[0].([]database.CountInProgressPrebuildsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CountInProgressPrebuilds indicates an expected call of CountInProgressPrebuilds.
|
||||
func (mr *MockStoreMockRecorder) CountInProgressPrebuilds(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountInProgressPrebuilds", reflect.TypeOf((*MockStore)(nil).CountInProgressPrebuilds), ctx)
|
||||
}
|
||||
|
||||
// CountUnreadInboxNotificationsByUserID mocks base method.
|
||||
func (m *MockStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2194,6 +2224,36 @@ func (mr *MockStoreMockRecorder) GetParameterSchemasByJobID(ctx, jobID any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParameterSchemasByJobID", reflect.TypeOf((*MockStore)(nil).GetParameterSchemasByJobID), ctx, jobID)
|
||||
}
|
||||
|
||||
// GetPrebuildMetrics mocks base method.
|
||||
func (m *MockStore) GetPrebuildMetrics(ctx context.Context) ([]database.GetPrebuildMetricsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPrebuildMetrics", ctx)
|
||||
ret0, _ := ret[0].([]database.GetPrebuildMetricsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPrebuildMetrics indicates an expected call of GetPrebuildMetrics.
|
||||
func (mr *MockStoreMockRecorder) GetPrebuildMetrics(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrebuildMetrics", reflect.TypeOf((*MockStore)(nil).GetPrebuildMetrics), ctx)
|
||||
}
|
||||
|
||||
// GetPresetByID mocks base method.
|
||||
func (m *MockStore) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPresetByID", ctx, presetID)
|
||||
ret0, _ := ret[0].(database.GetPresetByIDRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPresetByID indicates an expected call of GetPresetByID.
|
||||
func (mr *MockStoreMockRecorder) GetPresetByID(ctx, presetID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetByID", reflect.TypeOf((*MockStore)(nil).GetPresetByID), ctx, presetID)
|
||||
}
|
||||
|
||||
// GetPresetByWorkspaceBuildID mocks base method.
|
||||
func (m *MockStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2224,6 +2284,21 @@ func (mr *MockStoreMockRecorder) GetPresetParametersByTemplateVersionID(ctx, tem
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetParametersByTemplateVersionID", reflect.TypeOf((*MockStore)(nil).GetPresetParametersByTemplateVersionID), ctx, templateVersionID)
|
||||
}
|
||||
|
||||
// GetPresetsBackoff mocks base method.
|
||||
func (m *MockStore) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPresetsBackoff", ctx, lookback)
|
||||
ret0, _ := ret[0].([]database.GetPresetsBackoffRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPresetsBackoff indicates an expected call of GetPresetsBackoff.
|
||||
func (mr *MockStoreMockRecorder) GetPresetsBackoff(ctx, lookback any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetsBackoff", reflect.TypeOf((*MockStore)(nil).GetPresetsBackoff), ctx, lookback)
|
||||
}
|
||||
|
||||
// GetPresetsByTemplateVersionID mocks base method.
|
||||
func (m *MockStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2509,6 +2584,21 @@ func (mr *MockStoreMockRecorder) GetReplicasUpdatedAfter(ctx, updatedAt any) *go
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicasUpdatedAfter", reflect.TypeOf((*MockStore)(nil).GetReplicasUpdatedAfter), ctx, updatedAt)
|
||||
}
|
||||
|
||||
// GetRunningPrebuiltWorkspaces mocks base method.
|
||||
func (m *MockStore) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]database.GetRunningPrebuiltWorkspacesRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetRunningPrebuiltWorkspaces", ctx)
|
||||
ret0, _ := ret[0].([]database.GetRunningPrebuiltWorkspacesRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetRunningPrebuiltWorkspaces indicates an expected call of GetRunningPrebuiltWorkspaces.
|
||||
func (mr *MockStoreMockRecorder) GetRunningPrebuiltWorkspaces(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRunningPrebuiltWorkspaces", reflect.TypeOf((*MockStore)(nil).GetRunningPrebuiltWorkspaces), ctx)
|
||||
}
|
||||
|
||||
// GetRuntimeConfig mocks base method.
|
||||
func (m *MockStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2794,6 +2884,21 @@ func (mr *MockStoreMockRecorder) GetTemplateParameterInsights(ctx, arg any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplateParameterInsights", reflect.TypeOf((*MockStore)(nil).GetTemplateParameterInsights), ctx, arg)
|
||||
}
|
||||
|
||||
// GetTemplatePresetsWithPrebuilds mocks base method.
|
||||
func (m *MockStore) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]database.GetTemplatePresetsWithPrebuildsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetTemplatePresetsWithPrebuilds", ctx, templateID)
|
||||
ret0, _ := ret[0].([]database.GetTemplatePresetsWithPrebuildsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetTemplatePresetsWithPrebuilds indicates an expected call of GetTemplatePresetsWithPrebuilds.
|
||||
func (mr *MockStoreMockRecorder) GetTemplatePresetsWithPrebuilds(ctx, templateID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTemplatePresetsWithPrebuilds", reflect.TypeOf((*MockStore)(nil).GetTemplatePresetsWithPrebuilds), ctx, templateID)
|
||||
}
|
||||
|
||||
// GetTemplateUsageStats mocks base method.
|
||||
func (m *MockStore) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+104
-36
@@ -1401,7 +1401,9 @@ CREATE TABLE template_version_presets (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
template_version_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
desired_instances integer,
|
||||
invalidate_after_secs integer DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE template_version_terraform_values (
|
||||
@@ -1991,6 +1993,19 @@ CREATE VIEW workspace_build_with_user AS
|
||||
|
||||
COMMENT ON VIEW workspace_build_with_user IS 'Joins in the username + avatar url of the initiated by user.';
|
||||
|
||||
CREATE VIEW workspace_latest_builds AS
|
||||
SELECT DISTINCT ON (wb.workspace_id) wb.id,
|
||||
wb.workspace_id,
|
||||
wb.template_version_id,
|
||||
wb.job_id,
|
||||
wb.template_version_preset_id,
|
||||
wb.transition,
|
||||
wb.created_at,
|
||||
pj.job_status
|
||||
FROM (workspace_builds wb
|
||||
JOIN provisioner_jobs pj ON ((wb.job_id = pj.id)))
|
||||
ORDER BY wb.workspace_id, wb.build_number DESC;
|
||||
|
||||
CREATE TABLE workspace_modules (
|
||||
id uuid NOT NULL,
|
||||
job_id uuid NOT NULL,
|
||||
@@ -2001,6 +2016,92 @@ CREATE TABLE workspace_modules (
|
||||
created_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE VIEW workspace_prebuild_builds AS
|
||||
SELECT workspace_builds.id,
|
||||
workspace_builds.workspace_id,
|
||||
workspace_builds.template_version_id,
|
||||
workspace_builds.transition,
|
||||
workspace_builds.job_id,
|
||||
workspace_builds.template_version_preset_id,
|
||||
workspace_builds.build_number
|
||||
FROM workspace_builds
|
||||
WHERE (workspace_builds.initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid);
|
||||
|
||||
CREATE TABLE workspace_resources (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
job_id uuid NOT NULL,
|
||||
transition workspace_transition NOT NULL,
|
||||
type character varying(192) NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
hide boolean DEFAULT false NOT NULL,
|
||||
icon character varying(256) DEFAULT ''::character varying NOT NULL,
|
||||
instance_type character varying(256),
|
||||
daily_cost integer DEFAULT 0 NOT NULL,
|
||||
module_path text
|
||||
);
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
template_id uuid NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
autostart_schedule text,
|
||||
ttl bigint,
|
||||
last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
|
||||
dormant_at timestamp with time zone,
|
||||
deleting_at timestamp with time zone,
|
||||
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL,
|
||||
favorite boolean DEFAULT false NOT NULL,
|
||||
next_start_at timestamp with time zone
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
|
||||
|
||||
CREATE VIEW workspace_prebuilds AS
|
||||
WITH all_prebuilds AS (
|
||||
SELECT w.id,
|
||||
w.name,
|
||||
w.template_id,
|
||||
w.created_at
|
||||
FROM workspaces w
|
||||
WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid)
|
||||
), workspaces_with_latest_presets AS (
|
||||
SELECT DISTINCT ON (workspace_builds.workspace_id) workspace_builds.workspace_id,
|
||||
workspace_builds.template_version_preset_id
|
||||
FROM workspace_builds
|
||||
WHERE (workspace_builds.template_version_preset_id IS NOT NULL)
|
||||
ORDER BY workspace_builds.workspace_id, workspace_builds.build_number DESC
|
||||
), workspaces_with_agents_status AS (
|
||||
SELECT w.id AS workspace_id,
|
||||
bool_and((wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS ready
|
||||
FROM (((workspaces w
|
||||
JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id)))
|
||||
JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id)))
|
||||
JOIN workspace_agents wa ON ((wa.resource_id = wr.id)))
|
||||
WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid)
|
||||
GROUP BY w.id
|
||||
), current_presets AS (
|
||||
SELECT w.id AS prebuild_id,
|
||||
wlp.template_version_preset_id
|
||||
FROM (workspaces w
|
||||
JOIN workspaces_with_latest_presets wlp ON ((wlp.workspace_id = w.id)))
|
||||
WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid)
|
||||
)
|
||||
SELECT p.id,
|
||||
p.name,
|
||||
p.template_id,
|
||||
p.created_at,
|
||||
COALESCE(a.ready, false) AS ready,
|
||||
cp.template_version_preset_id AS current_preset_id
|
||||
FROM ((all_prebuilds p
|
||||
LEFT JOIN workspaces_with_agents_status a ON ((a.workspace_id = p.id)))
|
||||
JOIN current_presets cp ON ((cp.prebuild_id = p.id)));
|
||||
|
||||
CREATE TABLE workspace_proxies (
|
||||
id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
@@ -2057,41 +2158,6 @@ CREATE SEQUENCE workspace_resource_metadata_id_seq
|
||||
|
||||
ALTER SEQUENCE workspace_resource_metadata_id_seq OWNED BY workspace_resource_metadata.id;
|
||||
|
||||
CREATE TABLE workspace_resources (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
job_id uuid NOT NULL,
|
||||
transition workspace_transition NOT NULL,
|
||||
type character varying(192) NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
hide boolean DEFAULT false NOT NULL,
|
||||
icon character varying(256) DEFAULT ''::character varying NOT NULL,
|
||||
instance_type character varying(256),
|
||||
daily_cost integer DEFAULT 0 NOT NULL,
|
||||
module_path text
|
||||
);
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
id uuid NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
template_id uuid NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
name character varying(64) NOT NULL,
|
||||
autostart_schedule text,
|
||||
ttl bigint,
|
||||
last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
|
||||
dormant_at timestamp with time zone,
|
||||
deleting_at timestamp with time zone,
|
||||
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL,
|
||||
favorite boolean DEFAULT false NOT NULL,
|
||||
next_start_at timestamp with time zone
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN workspaces.favorite IS 'Favorite is true if the workspace owner has favorited the workspace.';
|
||||
|
||||
CREATE VIEW workspaces_expanded AS
|
||||
SELECT workspaces.id,
|
||||
workspaces.created_at,
|
||||
@@ -2465,6 +2531,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_unique_preset_name ON template_version_presets USING btree (name, template_version_id);
|
||||
|
||||
CREATE INDEX idx_user_deleted_deleted_at ON user_deleted USING btree (deleted_at);
|
||||
|
||||
CREATE INDEX idx_user_status_changes_changed_at ON user_status_changes USING btree (changed_at);
|
||||
|
||||
@@ -12,6 +12,8 @@ const (
|
||||
LockIDDBPurge
|
||||
LockIDNotificationsReportGenerator
|
||||
LockIDCryptoKeyRotation
|
||||
LockIDReconcileTemplatePrebuilds
|
||||
LockIDDeterminePrebuildsState
|
||||
)
|
||||
|
||||
// GenLockID generates a unique and consistent lock ID from a given string.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Revert prebuild views
|
||||
DROP VIEW IF EXISTS workspace_prebuild_builds;
|
||||
DROP VIEW IF EXISTS workspace_prebuilds;
|
||||
DROP VIEW IF EXISTS workspace_latest_builds;
|
||||
@@ -0,0 +1,62 @@
|
||||
-- workspace_latest_builds contains latest build for every workspace
|
||||
CREATE VIEW workspace_latest_builds AS
|
||||
SELECT DISTINCT ON (workspace_id)
|
||||
wb.id,
|
||||
wb.workspace_id,
|
||||
wb.template_version_id,
|
||||
wb.job_id,
|
||||
wb.template_version_preset_id,
|
||||
wb.transition,
|
||||
wb.created_at,
|
||||
pj.job_status
|
||||
FROM workspace_builds wb
|
||||
INNER JOIN provisioner_jobs pj ON wb.job_id = pj.id
|
||||
ORDER BY wb.workspace_id, wb.build_number DESC;
|
||||
|
||||
-- workspace_prebuilds contains all prebuilt workspaces with corresponding agent information
|
||||
-- (including lifecycle_state which indicates is agent ready or not) and corresponding preset_id for prebuild
|
||||
CREATE VIEW workspace_prebuilds AS
|
||||
WITH
|
||||
-- All workspaces owned by the "prebuilds" user.
|
||||
all_prebuilds AS (
|
||||
SELECT w.id, w.name, w.template_id, w.created_at
|
||||
FROM workspaces w
|
||||
WHERE w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' -- The system user responsible for prebuilds.
|
||||
),
|
||||
-- We can't rely on the template_version_preset_id in the workspace_builds table because this value is only set on the
|
||||
-- initial workspace creation. Subsequent stop/start transitions will not have a value for template_version_preset_id,
|
||||
-- and therefore we can't rely on (say) the latest build's chosen template_version_preset_id.
|
||||
--
|
||||
-- See https://github.com/coder/internal/issues/398
|
||||
workspaces_with_latest_presets AS (
|
||||
SELECT DISTINCT ON (workspace_id) workspace_id, template_version_preset_id
|
||||
FROM workspace_builds
|
||||
WHERE template_version_preset_id IS NOT NULL
|
||||
ORDER BY workspace_id, build_number DESC
|
||||
),
|
||||
-- workspaces_with_agents_status contains workspaces owned by the "prebuilds" user,
|
||||
-- along with the readiness status of their agents.
|
||||
-- A workspace is marked as 'ready' only if ALL of its agents are ready.
|
||||
workspaces_with_agents_status AS (
|
||||
SELECT w.id AS workspace_id,
|
||||
BOOL_AND(wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state) AS ready
|
||||
FROM workspaces w
|
||||
INNER JOIN workspace_latest_builds wlb ON wlb.workspace_id = w.id
|
||||
INNER JOIN workspace_resources wr ON wr.job_id = wlb.job_id
|
||||
INNER JOIN workspace_agents wa ON wa.resource_id = wr.id
|
||||
WHERE w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0' -- The system user responsible for prebuilds.
|
||||
GROUP BY w.id
|
||||
),
|
||||
current_presets AS (SELECT w.id AS prebuild_id, wlp.template_version_preset_id
|
||||
FROM workspaces w
|
||||
INNER JOIN workspaces_with_latest_presets wlp ON wlp.workspace_id = w.id
|
||||
WHERE w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0') -- The system user responsible for prebuilds.
|
||||
SELECT p.id, p.name, p.template_id, p.created_at, COALESCE(a.ready, false) AS ready, cp.template_version_preset_id AS current_preset_id
|
||||
FROM all_prebuilds p
|
||||
LEFT JOIN workspaces_with_agents_status a ON a.workspace_id = p.id
|
||||
INNER JOIN current_presets cp ON cp.prebuild_id = p.id;
|
||||
|
||||
CREATE VIEW workspace_prebuild_builds AS
|
||||
SELECT id, workspace_id, template_version_id, transition, job_id, template_version_preset_id, build_number
|
||||
FROM workspace_builds
|
||||
WHERE initiator_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'; -- The system user responsible for prebuilds.
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE template_version_presets
|
||||
DROP COLUMN desired_instances,
|
||||
DROP COLUMN invalidate_after_secs;
|
||||
|
||||
DROP INDEX IF EXISTS idx_unique_preset_name;
|
||||
@@ -0,0 +1,19 @@
|
||||
ALTER TABLE template_version_presets
|
||||
ADD COLUMN desired_instances INT NULL,
|
||||
ADD COLUMN invalidate_after_secs INT NULL DEFAULT 0;
|
||||
|
||||
-- Ensure that the idx_unique_preset_name index creation won't fail.
|
||||
-- This is necessary because presets were released before the index was introduced,
|
||||
-- so existing data might violate the uniqueness constraint.
|
||||
WITH ranked AS (
|
||||
SELECT id, name, template_version_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY name, template_version_id ORDER BY id) AS row_num
|
||||
FROM template_version_presets
|
||||
)
|
||||
UPDATE template_version_presets
|
||||
SET name = ranked.name || '_auto_' || row_num
|
||||
FROM ranked
|
||||
WHERE template_version_presets.id = ranked.id AND row_num > 1;
|
||||
|
||||
-- We should not be able to have presets with the same name for a particular template version.
|
||||
CREATE UNIQUE INDEX idx_unique_preset_name ON template_version_presets (name, template_version_id);
|
||||
+22
@@ -7,4 +7,26 @@ INSERT INTO public.template_versions (id, template_id, organization_id, created_
|
||||
|
||||
INSERT INTO public.template_version_presets (id, template_version_id, name, created_at) VALUES ('28b42cc0-c4fe-4907-a0fe-e4d20f1e9bfe', 'af58bd62-428c-4c33-849b-d43a3be07d93', 'test', '0001-01-01 00:00:00.000000 +00:00');
|
||||
|
||||
-- Add presets with the same template version ID and name
|
||||
-- to ensure they're correctly handled by the 00031*_preset_prebuilds migration.
|
||||
INSERT INTO public.template_version_presets (
|
||||
id, template_version_id, name, created_at
|
||||
)
|
||||
VALUES (
|
||||
'c9dd1a63-f0cf-446e-8d6f-2d29d7c8e38b',
|
||||
'af58bd62-428c-4c33-849b-d43a3be07d93',
|
||||
'duplicate_name',
|
||||
'0001-01-01 00:00:00.000000 +00:00'
|
||||
);
|
||||
|
||||
INSERT INTO public.template_version_presets (
|
||||
id, template_version_id, name, created_at
|
||||
)
|
||||
VALUES (
|
||||
'80f93d57-3948-487a-8990-bb011fb80a18',
|
||||
'af58bd62-428c-4c33-849b-d43a3be07d93',
|
||||
'duplicate_name',
|
||||
'0001-01-01 00:00:00.000000 +00:00'
|
||||
);
|
||||
|
||||
INSERT INTO public.template_version_preset_parameters (id, template_version_preset_id, name, value) VALUES ('ea90ccd2-5024-459e-87e4-879afd24de0f', '28b42cc0-c4fe-4907-a0fe-e4d20f1e9bfe', 'test', 'test');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE template_version_presets
|
||||
SET desired_instances = 1
|
||||
WHERE id = '28b42cc0-c4fe-4907-a0fe-e4d20f1e9bfe';
|
||||
@@ -3170,10 +3170,12 @@ type TemplateVersionParameter struct {
|
||||
}
|
||||
|
||||
type TemplateVersionPreset struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
||||
}
|
||||
|
||||
type TemplateVersionPresetParameter struct {
|
||||
@@ -3636,6 +3638,17 @@ type WorkspaceBuildTable struct {
|
||||
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
||||
}
|
||||
|
||||
type WorkspaceLatestBuild struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
||||
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
JobStatus ProvisionerJobStatus `db:"job_status" json:"job_status"`
|
||||
}
|
||||
|
||||
type WorkspaceModule struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
@@ -3646,6 +3659,25 @@ type WorkspaceModule struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
type WorkspacePrebuild struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
Ready bool `db:"ready" json:"ready"`
|
||||
CurrentPresetID uuid.NullUUID `db:"current_preset_id" json:"current_preset_id"`
|
||||
}
|
||||
|
||||
type WorkspacePrebuildBuild struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
||||
JobID uuid.UUID `db:"job_id" json:"job_id"`
|
||||
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
||||
BuildNumber int32 `db:"build_number" json:"build_number"`
|
||||
}
|
||||
|
||||
type WorkspaceProxy struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
|
||||
@@ -60,9 +60,13 @@ type sqlcQuerier interface {
|
||||
BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error
|
||||
BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error)
|
||||
BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error)
|
||||
ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error)
|
||||
CleanTailnetCoordinators(ctx context.Context) error
|
||||
CleanTailnetLostPeers(ctx context.Context) error
|
||||
CleanTailnetTunnels(ctx context.Context) error
|
||||
// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition.
|
||||
// Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state.
|
||||
CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error)
|
||||
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
|
||||
DeleteAPIKeyByID(ctx context.Context, id string) error
|
||||
@@ -230,8 +234,25 @@ type sqlcQuerier interface {
|
||||
GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error)
|
||||
GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error)
|
||||
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
|
||||
GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error)
|
||||
GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error)
|
||||
GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error)
|
||||
GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error)
|
||||
// GetPresetsBackoff groups workspace builds by preset ID.
|
||||
// Each preset is associated with exactly one template version ID.
|
||||
// For each group, the query checks up to N of the most recent jobs that occurred within the
|
||||
// lookback period, where N equals the number of desired instances for the corresponding preset.
|
||||
// If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
|
||||
// Query returns a list of preset IDs for which we should backoff.
|
||||
// Only active template versions with configured presets are considered.
|
||||
// We also return the number of failed workspace builds that occurred during the lookback period.
|
||||
//
|
||||
// NOTE:
|
||||
// - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
|
||||
// - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
|
||||
//
|
||||
// The number of failed builds is used downstream to determine the backoff duration.
|
||||
GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]GetPresetsBackoffRow, error)
|
||||
GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error)
|
||||
GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error)
|
||||
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
|
||||
@@ -253,6 +274,7 @@ type sqlcQuerier interface {
|
||||
GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error)
|
||||
GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error)
|
||||
GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]Replica, error)
|
||||
GetRunningPrebuiltWorkspaces(ctx context.Context) ([]GetRunningPrebuiltWorkspacesRow, error)
|
||||
GetRuntimeConfig(ctx context.Context, key string) (string, error)
|
||||
GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
|
||||
GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, error)
|
||||
@@ -295,6 +317,10 @@ type sqlcQuerier interface {
|
||||
// created in the timeframe and return the aggregate usage counts of parameter
|
||||
// values.
|
||||
GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error)
|
||||
// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
|
||||
// It also returns the number of desired instances for each preset.
|
||||
// If template_id is specified, only template versions associated with that template will be returned.
|
||||
GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]GetTemplatePresetsWithPrebuildsRow, error)
|
||||
GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error)
|
||||
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/sloggers/slogtest"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
@@ -3587,6 +3588,782 @@ func TestOrganizationDeleteTrigger(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type templateVersionWithPreset struct {
|
||||
database.TemplateVersion
|
||||
preset database.TemplateVersionPreset
|
||||
}
|
||||
|
||||
func createTemplate(t *testing.T, db database.Store, orgID uuid.UUID, userID uuid.UUID) database.Template {
|
||||
// create template
|
||||
tmpl := dbgen.Template(t, db, database.Template{
|
||||
OrganizationID: orgID,
|
||||
CreatedBy: userID,
|
||||
ActiveVersionID: uuid.New(),
|
||||
})
|
||||
|
||||
return tmpl
|
||||
}
|
||||
|
||||
type tmplVersionOpts struct {
|
||||
DesiredInstances int32
|
||||
}
|
||||
|
||||
func createTmplVersionAndPreset(
|
||||
t *testing.T,
|
||||
db database.Store,
|
||||
tmpl database.Template,
|
||||
versionID uuid.UUID,
|
||||
now time.Time,
|
||||
opts *tmplVersionOpts,
|
||||
) templateVersionWithPreset {
|
||||
// Create template version with corresponding preset and preset prebuild
|
||||
tmplVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
ID: versionID,
|
||||
TemplateID: uuid.NullUUID{
|
||||
UUID: tmpl.ID,
|
||||
Valid: true,
|
||||
},
|
||||
OrganizationID: tmpl.OrganizationID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedBy: tmpl.CreatedBy,
|
||||
})
|
||||
desiredInstances := int32(1)
|
||||
if opts != nil {
|
||||
desiredInstances = opts.DesiredInstances
|
||||
}
|
||||
preset := dbgen.Preset(t, db, database.InsertPresetParams{
|
||||
TemplateVersionID: tmplVersion.ID,
|
||||
Name: "preset",
|
||||
DesiredInstances: sql.NullInt32{
|
||||
Int32: desiredInstances,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
|
||||
return templateVersionWithPreset{
|
||||
TemplateVersion: tmplVersion,
|
||||
preset: preset,
|
||||
}
|
||||
}
|
||||
|
||||
type createPrebuiltWorkspaceOpts struct {
|
||||
failedJob bool
|
||||
createdAt time.Time
|
||||
readyAgents int
|
||||
notReadyAgents int
|
||||
}
|
||||
|
||||
func createPrebuiltWorkspace(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
db database.Store,
|
||||
tmpl database.Template,
|
||||
extTmplVersion templateVersionWithPreset,
|
||||
orgID uuid.UUID,
|
||||
now time.Time,
|
||||
opts *createPrebuiltWorkspaceOpts,
|
||||
) {
|
||||
// Create job with corresponding resource and agent
|
||||
jobError := sql.NullString{}
|
||||
if opts != nil && opts.failedJob {
|
||||
jobError = sql.NullString{String: "failed", Valid: true}
|
||||
}
|
||||
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
Type: database.ProvisionerJobTypeWorkspaceBuild,
|
||||
OrganizationID: orgID,
|
||||
|
||||
CreatedAt: now.Add(-1 * time.Minute),
|
||||
Error: jobError,
|
||||
})
|
||||
|
||||
// create ready agents
|
||||
readyAgents := 0
|
||||
if opts != nil {
|
||||
readyAgents = opts.readyAgents
|
||||
}
|
||||
for i := 0; i < readyAgents; i++ {
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: job.ID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
err := db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
|
||||
ID: agent.ID,
|
||||
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// create not ready agents
|
||||
notReadyAgents := 1
|
||||
if opts != nil {
|
||||
notReadyAgents = opts.notReadyAgents
|
||||
}
|
||||
for i := 0; i < notReadyAgents; i++ {
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: job.ID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
err := db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
|
||||
ID: agent.ID,
|
||||
LifecycleState: database.WorkspaceAgentLifecycleStateCreated,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create corresponding workspace and workspace build
|
||||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: uuid.MustParse("c42fdf75-3097-471c-8c33-fb52454d81c0"),
|
||||
OrganizationID: tmpl.OrganizationID,
|
||||
TemplateID: tmpl.ID,
|
||||
})
|
||||
createdAt := now
|
||||
if opts != nil {
|
||||
createdAt = opts.createdAt
|
||||
}
|
||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||
CreatedAt: createdAt,
|
||||
WorkspaceID: workspace.ID,
|
||||
TemplateVersionID: extTmplVersion.ID,
|
||||
BuildNumber: 1,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
InitiatorID: tmpl.CreatedBy,
|
||||
JobID: job.ID,
|
||||
TemplateVersionPresetID: uuid.NullUUID{
|
||||
UUID: extTmplVersion.preset.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspacePrebuildsView(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
now := dbtime.Now()
|
||||
orgID := uuid.New()
|
||||
userID := uuid.New()
|
||||
|
||||
type workspacePrebuild struct {
|
||||
ID uuid.UUID
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
Ready bool
|
||||
CurrentPresetID uuid.UUID
|
||||
}
|
||||
getWorkspacePrebuilds := func(sqlDB *sql.DB) []*workspacePrebuild {
|
||||
rows, err := sqlDB.Query("SELECT id, name, created_at, ready, current_preset_id FROM workspace_prebuilds")
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
|
||||
workspacePrebuilds := make([]*workspacePrebuild, 0)
|
||||
for rows.Next() {
|
||||
var wp workspacePrebuild
|
||||
err := rows.Scan(&wp.ID, &wp.Name, &wp.CreatedAt, &wp.Ready, &wp.CurrentPresetID)
|
||||
require.NoError(t, err)
|
||||
|
||||
workspacePrebuilds = append(workspacePrebuilds, &wp)
|
||||
}
|
||||
|
||||
return workspacePrebuilds
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
readyAgents int
|
||||
notReadyAgents int
|
||||
expectReady bool
|
||||
}{
|
||||
{
|
||||
name: "one ready agent",
|
||||
readyAgents: 1,
|
||||
notReadyAgents: 0,
|
||||
expectReady: true,
|
||||
},
|
||||
{
|
||||
name: "one not ready agent",
|
||||
readyAgents: 0,
|
||||
notReadyAgents: 1,
|
||||
expectReady: false,
|
||||
},
|
||||
{
|
||||
name: "one ready, one not ready",
|
||||
readyAgents: 1,
|
||||
notReadyAgents: 1,
|
||||
expectReady: false,
|
||||
},
|
||||
{
|
||||
name: "both ready",
|
||||
readyAgents: 2,
|
||||
notReadyAgents: 0,
|
||||
expectReady: true,
|
||||
},
|
||||
{
|
||||
name: "five ready, one not ready",
|
||||
readyAgents: 5,
|
||||
notReadyAgents: 1,
|
||||
expectReady: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
sqlDB := testSQLDB(t)
|
||||
err := migrations.Up(sqlDB)
|
||||
require.NoError(t, err)
|
||||
db := database.New(sqlDB)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl := createTemplate(t, db, orgID, userID)
|
||||
tmplV1 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
readyAgents: tc.readyAgents,
|
||||
notReadyAgents: tc.notReadyAgents,
|
||||
})
|
||||
|
||||
workspacePrebuilds := getWorkspacePrebuilds(sqlDB)
|
||||
require.Len(t, workspacePrebuilds, 1)
|
||||
require.Equal(t, tc.expectReady, workspacePrebuilds[0].Ready)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPresetsBackoff(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
now := dbtime.Now()
|
||||
orgID := uuid.New()
|
||||
userID := uuid.New()
|
||||
|
||||
findBackoffByTmplVersionID := func(backoffs []database.GetPresetsBackoffRow, tmplVersionID uuid.UUID) *database.GetPresetsBackoffRow {
|
||||
for _, backoff := range backoffs {
|
||||
if backoff.TemplateVersionID == tmplVersionID {
|
||||
return &backoff
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("Single Workspace Build", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl := createTemplate(t, db, orgID, userID)
|
||||
tmplV1 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmplV1.preset.ID)
|
||||
require.Equal(t, int32(1), backoff.NumFailed)
|
||||
})
|
||||
|
||||
t.Run("Multiple Workspace Builds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl := createTemplate(t, db, orgID, userID)
|
||||
tmplV1 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmplV1.preset.ID)
|
||||
require.Equal(t, int32(3), backoff.NumFailed)
|
||||
})
|
||||
|
||||
t.Run("Ignore Inactive Version", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl := createTemplate(t, db, orgID, userID)
|
||||
tmplV1 := createTmplVersionAndPreset(t, db, tmpl, uuid.New(), now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
// Active Version
|
||||
tmplV2 := createTmplVersionAndPreset(t, db, tmpl, tmpl.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV2, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl, tmplV2, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmplV2.preset.ID)
|
||||
require.Equal(t, int32(2), backoff.NumFailed)
|
||||
})
|
||||
|
||||
t.Run("Multiple Templates", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
tmpl2 := createTemplate(t, db, orgID, userID)
|
||||
tmpl2V1 := createTmplVersionAndPreset(t, db, tmpl2, tmpl2.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 2)
|
||||
{
|
||||
backoff := findBackoffByTmplVersionID(backoffs, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl1V1.preset.ID)
|
||||
require.Equal(t, int32(1), backoff.NumFailed)
|
||||
}
|
||||
{
|
||||
backoff := findBackoffByTmplVersionID(backoffs, tmpl2.ActiveVersionID)
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl2.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl2V1.preset.ID)
|
||||
require.Equal(t, int32(1), backoff.NumFailed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Multiple Templates, Versions and Workspace Builds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
tmpl2 := createTemplate(t, db, orgID, userID)
|
||||
tmpl2V1 := createTmplVersionAndPreset(t, db, tmpl2, tmpl2.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl2, tmpl2V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
tmpl3 := createTemplate(t, db, orgID, userID)
|
||||
tmpl3V1 := createTmplVersionAndPreset(t, db, tmpl3, uuid.New(), now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
tmpl3V2 := createTmplVersionAndPreset(t, db, tmpl3, tmpl3.ActiveVersionID, now, nil)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V2, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V2, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl3, tmpl3V2, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 3)
|
||||
{
|
||||
backoff := findBackoffByTmplVersionID(backoffs, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl1V1.preset.ID)
|
||||
require.Equal(t, int32(1), backoff.NumFailed)
|
||||
}
|
||||
{
|
||||
backoff := findBackoffByTmplVersionID(backoffs, tmpl2.ActiveVersionID)
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl2.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl2V1.preset.ID)
|
||||
require.Equal(t, int32(2), backoff.NumFailed)
|
||||
}
|
||||
{
|
||||
backoff := findBackoffByTmplVersionID(backoffs, tmpl3.ActiveVersionID)
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl3.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl3V2.preset.ID)
|
||||
require.Equal(t, int32(3), backoff.NumFailed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("No Workspace Builds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil)
|
||||
_ = tmpl1V1
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, backoffs)
|
||||
})
|
||||
|
||||
t.Run("No Failed Workspace Builds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, nil)
|
||||
successfulJobOpts := createPrebuiltWorkspaceOpts{}
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts)
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, backoffs)
|
||||
})
|
||||
|
||||
t.Run("Last job is successful - no backoff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 1,
|
||||
})
|
||||
failedJobOpts := createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-2 * time.Minute),
|
||||
}
|
||||
successfulJobOpts := createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-1 * time.Minute),
|
||||
}
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &failedJobOpts)
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &successfulJobOpts)
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, backoffs)
|
||||
})
|
||||
|
||||
t.Run("Last 3 jobs are successful - no backoff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 3,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-4 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-3 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-2 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-1 * time.Minute),
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, backoffs)
|
||||
})
|
||||
|
||||
t.Run("1 job failed out of 3 - backoff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 3,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-3 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-2 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-1 * time.Minute),
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
{
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl1V1.preset.ID)
|
||||
require.Equal(t, int32(1), backoff.NumFailed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("3 job failed out of 5 - backoff", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
lookbackPeriod := time.Hour
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 3,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-lookbackPeriod - time.Minute), // earlier than lookback period - skipped
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-4 * time.Minute), // within lookback period - counted as failed job
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-3 * time.Minute), // within lookback period - counted as failed job
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-2 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: false,
|
||||
createdAt: now.Add(-1 * time.Minute),
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-lookbackPeriod))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
{
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl1V1.preset.ID)
|
||||
require.Equal(t, int32(2), backoff.NumFailed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("check LastBuildAt timestamp", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
lookbackPeriod := time.Hour
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 6,
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-lookbackPeriod - time.Minute), // earlier than lookback period - skipped
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-4 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-0 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-3 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-1 * time.Minute),
|
||||
})
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-2 * time.Minute),
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-lookbackPeriod))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, backoffs, 1)
|
||||
{
|
||||
backoff := backoffs[0]
|
||||
require.Equal(t, backoff.TemplateVersionID, tmpl1.ActiveVersionID)
|
||||
require.Equal(t, backoff.PresetID, tmpl1V1.preset.ID)
|
||||
require.Equal(t, int32(5), backoff.NumFailed)
|
||||
// make sure LastBuildAt is equal to latest failed build timestamp
|
||||
require.Equal(t, 0, now.Compare(backoff.LastBuildAt))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failed job outside lookback period", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
dbgen.Organization(t, db, database.Organization{
|
||||
ID: orgID,
|
||||
})
|
||||
dbgen.User(t, db, database.User{
|
||||
ID: userID,
|
||||
})
|
||||
lookbackPeriod := time.Hour
|
||||
|
||||
tmpl1 := createTemplate(t, db, orgID, userID)
|
||||
tmpl1V1 := createTmplVersionAndPreset(t, db, tmpl1, tmpl1.ActiveVersionID, now, &tmplVersionOpts{
|
||||
DesiredInstances: 1,
|
||||
})
|
||||
|
||||
createPrebuiltWorkspace(ctx, t, db, tmpl1, tmpl1V1, orgID, now, &createPrebuiltWorkspaceOpts{
|
||||
failedJob: true,
|
||||
createdAt: now.Add(-lookbackPeriod - time.Minute), // earlier than lookback period - skipped
|
||||
})
|
||||
|
||||
backoffs, err := db.GetPresetsBackoff(ctx, now.Add(-lookbackPeriod))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, backoffs, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
|
||||
t.Helper()
|
||||
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)
|
||||
|
||||
+438
-10
@@ -5961,9 +5961,413 @@ func (q *sqlQuerier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const claimPrebuiltWorkspace = `-- name: ClaimPrebuiltWorkspace :one
|
||||
UPDATE workspaces w
|
||||
SET owner_id = $1::uuid,
|
||||
name = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE w.id IN (
|
||||
SELECT p.id
|
||||
FROM workspace_prebuilds p
|
||||
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
|
||||
INNER JOIN templates t ON p.template_id = t.id
|
||||
WHERE (b.transition = 'start'::workspace_transition
|
||||
AND b.job_status IN ('succeeded'::provisioner_job_status))
|
||||
-- The prebuilds system should never try to claim a prebuild for an inactive template version.
|
||||
-- Nevertheless, this filter is here as a defensive measure:
|
||||
AND b.template_version_id = t.active_version_id
|
||||
AND p.current_preset_id = $3::uuid
|
||||
AND p.ready
|
||||
LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
|
||||
)
|
||||
RETURNING w.id, w.name
|
||||
`
|
||||
|
||||
type ClaimPrebuiltWorkspaceParams struct {
|
||||
NewUserID uuid.UUID `db:"new_user_id" json:"new_user_id"`
|
||||
NewName string `db:"new_name" json:"new_name"`
|
||||
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
||||
}
|
||||
|
||||
type ClaimPrebuiltWorkspaceRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace, arg.NewUserID, arg.NewName, arg.PresetID)
|
||||
var i ClaimPrebuiltWorkspaceRow
|
||||
err := row.Scan(&i.ID, &i.Name)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const countInProgressPrebuilds = `-- name: CountInProgressPrebuilds :many
|
||||
SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count
|
||||
FROM workspace_latest_builds wlb
|
||||
INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id
|
||||
-- We only need these counts for active template versions.
|
||||
-- It doesn't influence whether we create or delete prebuilds
|
||||
-- for inactive template versions. This is because we never create
|
||||
-- prebuilds for inactive template versions, we always delete
|
||||
-- running prebuilds for inactive template versions, and we ignore
|
||||
-- prebuilds that are still building.
|
||||
INNER JOIN templates t ON t.active_version_id = wlb.template_version_id
|
||||
WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status)
|
||||
GROUP BY t.id, wpb.template_version_id, wpb.transition
|
||||
`
|
||||
|
||||
type CountInProgressPrebuildsRow struct {
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Transition WorkspaceTransition `db:"transition" json:"transition"`
|
||||
Count int32 `db:"count" json:"count"`
|
||||
}
|
||||
|
||||
// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition.
|
||||
// Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state.
|
||||
func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, countInProgressPrebuilds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []CountInProgressPrebuildsRow
|
||||
for rows.Next() {
|
||||
var i CountInProgressPrebuildsRow
|
||||
if err := rows.Scan(
|
||||
&i.TemplateID,
|
||||
&i.TemplateVersionID,
|
||||
&i.Transition,
|
||||
&i.Count,
|
||||
); 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 getPrebuildMetrics = `-- name: GetPrebuildMetrics :many
|
||||
SELECT
|
||||
t.name as template_name,
|
||||
tvp.name as preset_name,
|
||||
o.name as organization_name,
|
||||
COUNT(*) as created_count,
|
||||
COUNT(*) FILTER (WHERE pj.job_status = 'failed'::provisioner_job_status) as failed_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE w.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid -- The system user responsible for prebuilds.
|
||||
) as claimed_count
|
||||
FROM workspaces w
|
||||
INNER JOIN workspace_prebuild_builds wpb ON wpb.workspace_id = w.id
|
||||
INNER JOIN templates t ON t.id = w.template_id
|
||||
INNER JOIN template_version_presets tvp ON tvp.id = wpb.template_version_preset_id
|
||||
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
|
||||
INNER JOIN organizations o ON o.id = w.organization_id
|
||||
WHERE NOT t.deleted AND wpb.build_number = 1
|
||||
GROUP BY t.name, tvp.name, o.name
|
||||
ORDER BY t.name, tvp.name, o.name
|
||||
`
|
||||
|
||||
type GetPrebuildMetricsRow struct {
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
PresetName string `db:"preset_name" json:"preset_name"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
CreatedCount int64 `db:"created_count" json:"created_count"`
|
||||
FailedCount int64 `db:"failed_count" json:"failed_count"`
|
||||
ClaimedCount int64 `db:"claimed_count" json:"claimed_count"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getPrebuildMetrics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetPrebuildMetricsRow
|
||||
for rows.Next() {
|
||||
var i GetPrebuildMetricsRow
|
||||
if err := rows.Scan(
|
||||
&i.TemplateName,
|
||||
&i.PresetName,
|
||||
&i.OrganizationName,
|
||||
&i.CreatedCount,
|
||||
&i.FailedCount,
|
||||
&i.ClaimedCount,
|
||||
); 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 getPresetsBackoff = `-- name: GetPresetsBackoff :many
|
||||
WITH filtered_builds AS (
|
||||
-- Only select builds which are for prebuild creations
|
||||
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
|
||||
FROM template_version_presets tvp
|
||||
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
|
||||
INNER JOIN workspaces w ON wlb.workspace_id = w.id
|
||||
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
|
||||
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
|
||||
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
||||
AND wlb.transition = 'start'::workspace_transition
|
||||
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
|
||||
),
|
||||
time_sorted_builds AS (
|
||||
-- Group builds by preset, then sort each group by created_at.
|
||||
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
|
||||
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
|
||||
FROM filtered_builds fb
|
||||
),
|
||||
failed_count AS (
|
||||
-- Count failed builds per preset in the given period
|
||||
SELECT preset_id, COUNT(*) AS num_failed
|
||||
FROM filtered_builds
|
||||
WHERE job_status = 'failed'::provisioner_job_status
|
||||
AND created_at >= $1::timestamptz
|
||||
GROUP BY preset_id
|
||||
)
|
||||
SELECT
|
||||
tsb.template_version_id,
|
||||
tsb.preset_id,
|
||||
COALESCE(fc.num_failed, 0)::int AS num_failed,
|
||||
MAX(tsb.created_at)::timestamptz AS last_build_at
|
||||
FROM time_sorted_builds tsb
|
||||
LEFT JOIN failed_count fc ON fc.preset_id = tsb.preset_id
|
||||
WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the number of desired instances; if any fail, we backoff
|
||||
AND tsb.job_status = 'failed'::provisioner_job_status
|
||||
AND created_at >= $1::timestamptz
|
||||
GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed
|
||||
`
|
||||
|
||||
type GetPresetsBackoffRow struct {
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
|
||||
NumFailed int32 `db:"num_failed" json:"num_failed"`
|
||||
LastBuildAt time.Time `db:"last_build_at" json:"last_build_at"`
|
||||
}
|
||||
|
||||
// GetPresetsBackoff groups workspace builds by preset ID.
|
||||
// Each preset is associated with exactly one template version ID.
|
||||
// For each group, the query checks up to N of the most recent jobs that occurred within the
|
||||
// lookback period, where N equals the number of desired instances for the corresponding preset.
|
||||
// If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
|
||||
// Query returns a list of preset IDs for which we should backoff.
|
||||
// Only active template versions with configured presets are considered.
|
||||
// We also return the number of failed workspace builds that occurred during the lookback period.
|
||||
//
|
||||
// NOTE:
|
||||
// - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
|
||||
// - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
|
||||
//
|
||||
// The number of failed builds is used downstream to determine the backoff duration.
|
||||
func (q *sqlQuerier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]GetPresetsBackoffRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getPresetsBackoff, lookback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetPresetsBackoffRow
|
||||
for rows.Next() {
|
||||
var i GetPresetsBackoffRow
|
||||
if err := rows.Scan(
|
||||
&i.TemplateVersionID,
|
||||
&i.PresetID,
|
||||
&i.NumFailed,
|
||||
&i.LastBuildAt,
|
||||
); 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 getRunningPrebuiltWorkspaces = `-- name: GetRunningPrebuiltWorkspaces :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.template_id,
|
||||
b.template_version_id,
|
||||
p.current_preset_id AS current_preset_id,
|
||||
p.ready,
|
||||
p.created_at
|
||||
FROM workspace_prebuilds p
|
||||
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
|
||||
WHERE (b.transition = 'start'::workspace_transition
|
||||
AND b.job_status = 'succeeded'::provisioner_job_status)
|
||||
`
|
||||
|
||||
type GetRunningPrebuiltWorkspacesRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
CurrentPresetID uuid.NullUUID `db:"current_preset_id" json:"current_preset_id"`
|
||||
Ready bool `db:"ready" json:"ready"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetRunningPrebuiltWorkspaces(ctx context.Context) ([]GetRunningPrebuiltWorkspacesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getRunningPrebuiltWorkspaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetRunningPrebuiltWorkspacesRow
|
||||
for rows.Next() {
|
||||
var i GetRunningPrebuiltWorkspacesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.TemplateID,
|
||||
&i.TemplateVersionID,
|
||||
&i.CurrentPresetID,
|
||||
&i.Ready,
|
||||
&i.CreatedAt,
|
||||
); 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 getTemplatePresetsWithPrebuilds = `-- name: GetTemplatePresetsWithPrebuilds :many
|
||||
SELECT
|
||||
t.id AS template_id,
|
||||
t.name AS template_name,
|
||||
o.name AS organization_name,
|
||||
tv.id AS template_version_id,
|
||||
tv.name AS template_version_name,
|
||||
tv.id = t.active_version_id AS using_active_version,
|
||||
tvp.id,
|
||||
tvp.name,
|
||||
tvp.desired_instances AS desired_instances,
|
||||
t.deleted,
|
||||
t.deprecated != '' AS deprecated
|
||||
FROM templates t
|
||||
INNER JOIN template_versions tv ON tv.template_id = t.id
|
||||
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
|
||||
INNER JOIN organizations o ON o.id = t.organization_id
|
||||
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
||||
AND (t.id = $1::uuid OR $1 IS NULL)
|
||||
`
|
||||
|
||||
type GetTemplatePresetsWithPrebuildsRow struct {
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
TemplateName string `db:"template_name" json:"template_name"`
|
||||
OrganizationName string `db:"organization_name" json:"organization_name"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
TemplateVersionName string `db:"template_version_name" json:"template_version_name"`
|
||||
UsingActiveVersion bool `db:"using_active_version" json:"using_active_version"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
Deprecated bool `db:"deprecated" json:"deprecated"`
|
||||
}
|
||||
|
||||
// GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
|
||||
// It also returns the number of desired instances for each preset.
|
||||
// If template_id is specified, only template versions associated with that template will be returned.
|
||||
func (q *sqlQuerier) GetTemplatePresetsWithPrebuilds(ctx context.Context, templateID uuid.NullUUID) ([]GetTemplatePresetsWithPrebuildsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getTemplatePresetsWithPrebuilds, templateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetTemplatePresetsWithPrebuildsRow
|
||||
for rows.Next() {
|
||||
var i GetTemplatePresetsWithPrebuildsRow
|
||||
if err := rows.Scan(
|
||||
&i.TemplateID,
|
||||
&i.TemplateName,
|
||||
&i.OrganizationName,
|
||||
&i.TemplateVersionID,
|
||||
&i.TemplateVersionName,
|
||||
&i.UsingActiveVersion,
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.DesiredInstances,
|
||||
&i.Deleted,
|
||||
&i.Deprecated,
|
||||
); 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 getPresetByID = `-- name: GetPresetByID :one
|
||||
SELECT tvp.id, tvp.template_version_id, tvp.name, tvp.created_at, tvp.desired_instances, tvp.invalidate_after_secs, 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
|
||||
`
|
||||
|
||||
type GetPresetByIDRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
||||
TemplateID uuid.NullUUID `db:"template_id" json:"template_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getPresetByID, presetID)
|
||||
var i GetPresetByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TemplateVersionID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
&i.TemplateID,
|
||||
&i.OrganizationID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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.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
|
||||
FROM
|
||||
template_version_presets
|
||||
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
|
||||
@@ -5979,6 +6383,8 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
|
||||
&i.TemplateVersionID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -6023,7 +6429,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context,
|
||||
|
||||
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
|
||||
SELECT
|
||||
id, template_version_id, name, created_at
|
||||
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs
|
||||
FROM
|
||||
template_version_presets
|
||||
WHERE
|
||||
@@ -6044,6 +6450,8 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
||||
&i.TemplateVersionID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -6059,26 +6467,46 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
||||
}
|
||||
|
||||
const insertPreset = `-- name: InsertPreset :one
|
||||
INSERT INTO
|
||||
template_version_presets (template_version_id, name, created_at)
|
||||
VALUES
|
||||
($1, $2, $3) RETURNING id, template_version_id, name, created_at
|
||||
INSERT INTO template_version_presets (
|
||||
template_version_id,
|
||||
name,
|
||||
created_at,
|
||||
desired_instances,
|
||||
invalidate_after_secs
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5
|
||||
) RETURNING id, template_version_id, name, created_at, desired_instances, invalidate_after_secs
|
||||
`
|
||||
|
||||
type InsertPresetParams struct {
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
DesiredInstances sql.NullInt32 `db:"desired_instances" json:"desired_instances"`
|
||||
InvalidateAfterSecs sql.NullInt32 `db:"invalidate_after_secs" json:"invalidate_after_secs"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertPreset, arg.TemplateVersionID, arg.Name, arg.CreatedAt)
|
||||
row := q.db.QueryRowContext(ctx, insertPreset,
|
||||
arg.TemplateVersionID,
|
||||
arg.Name,
|
||||
arg.CreatedAt,
|
||||
arg.DesiredInstances,
|
||||
arg.InvalidateAfterSecs,
|
||||
)
|
||||
var i TemplateVersionPreset
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TemplateVersionID,
|
||||
&i.Name,
|
||||
&i.CreatedAt,
|
||||
&i.DesiredInstances,
|
||||
&i.InvalidateAfterSecs,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
-- name: ClaimPrebuiltWorkspace :one
|
||||
UPDATE workspaces w
|
||||
SET owner_id = @new_user_id::uuid,
|
||||
name = @new_name::text,
|
||||
updated_at = NOW()
|
||||
WHERE w.id IN (
|
||||
SELECT p.id
|
||||
FROM workspace_prebuilds p
|
||||
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
|
||||
INNER JOIN templates t ON p.template_id = t.id
|
||||
WHERE (b.transition = 'start'::workspace_transition
|
||||
AND b.job_status IN ('succeeded'::provisioner_job_status))
|
||||
-- The prebuilds system should never try to claim a prebuild for an inactive template version.
|
||||
-- Nevertheless, this filter is here as a defensive measure:
|
||||
AND b.template_version_id = t.active_version_id
|
||||
AND p.current_preset_id = @preset_id::uuid
|
||||
AND p.ready
|
||||
LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
|
||||
)
|
||||
RETURNING w.id, w.name;
|
||||
|
||||
-- name: GetTemplatePresetsWithPrebuilds :many
|
||||
-- GetTemplatePresetsWithPrebuilds retrieves template versions with configured presets and prebuilds.
|
||||
-- It also returns the number of desired instances for each preset.
|
||||
-- If template_id is specified, only template versions associated with that template will be returned.
|
||||
SELECT
|
||||
t.id AS template_id,
|
||||
t.name AS template_name,
|
||||
o.name AS organization_name,
|
||||
tv.id AS template_version_id,
|
||||
tv.name AS template_version_name,
|
||||
tv.id = t.active_version_id AS using_active_version,
|
||||
tvp.id,
|
||||
tvp.name,
|
||||
tvp.desired_instances AS desired_instances,
|
||||
t.deleted,
|
||||
t.deprecated != '' AS deprecated
|
||||
FROM templates t
|
||||
INNER JOIN template_versions tv ON tv.template_id = t.id
|
||||
INNER JOIN template_version_presets tvp ON tvp.template_version_id = tv.id
|
||||
INNER JOIN organizations o ON o.id = t.organization_id
|
||||
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
||||
AND (t.id = sqlc.narg('template_id')::uuid OR sqlc.narg('template_id') IS NULL);
|
||||
|
||||
-- name: GetRunningPrebuiltWorkspaces :many
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
p.template_id,
|
||||
b.template_version_id,
|
||||
p.current_preset_id AS current_preset_id,
|
||||
p.ready,
|
||||
p.created_at
|
||||
FROM workspace_prebuilds p
|
||||
INNER JOIN workspace_latest_builds b ON b.workspace_id = p.id
|
||||
WHERE (b.transition = 'start'::workspace_transition
|
||||
AND b.job_status = 'succeeded'::provisioner_job_status);
|
||||
|
||||
-- name: CountInProgressPrebuilds :many
|
||||
-- CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition.
|
||||
-- Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state.
|
||||
SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count
|
||||
FROM workspace_latest_builds wlb
|
||||
INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id
|
||||
-- We only need these counts for active template versions.
|
||||
-- It doesn't influence whether we create or delete prebuilds
|
||||
-- for inactive template versions. This is because we never create
|
||||
-- prebuilds for inactive template versions, we always delete
|
||||
-- running prebuilds for inactive template versions, and we ignore
|
||||
-- prebuilds that are still building.
|
||||
INNER JOIN templates t ON t.active_version_id = wlb.template_version_id
|
||||
WHERE wlb.job_status IN ('pending'::provisioner_job_status, 'running'::provisioner_job_status)
|
||||
GROUP BY t.id, wpb.template_version_id, wpb.transition;
|
||||
|
||||
-- GetPresetsBackoff groups workspace builds by preset ID.
|
||||
-- Each preset is associated with exactly one template version ID.
|
||||
-- For each group, the query checks up to N of the most recent jobs that occurred within the
|
||||
-- lookback period, where N equals the number of desired instances for the corresponding preset.
|
||||
-- If at least one of the job within a group has failed, we should backoff on the corresponding preset ID.
|
||||
-- Query returns a list of preset IDs for which we should backoff.
|
||||
-- Only active template versions with configured presets are considered.
|
||||
-- We also return the number of failed workspace builds that occurred during the lookback period.
|
||||
--
|
||||
-- NOTE:
|
||||
-- - To **decide whether to back off**, we look at up to the N most recent builds (within the defined lookback period).
|
||||
-- - To **calculate the number of failed builds**, we consider all builds within the defined lookback period.
|
||||
--
|
||||
-- The number of failed builds is used downstream to determine the backoff duration.
|
||||
-- name: GetPresetsBackoff :many
|
||||
WITH filtered_builds AS (
|
||||
-- Only select builds which are for prebuild creations
|
||||
SELECT wlb.template_version_id, wlb.created_at, tvp.id AS preset_id, wlb.job_status, tvp.desired_instances
|
||||
FROM template_version_presets tvp
|
||||
INNER JOIN workspace_latest_builds wlb ON wlb.template_version_preset_id = tvp.id
|
||||
INNER JOIN workspaces w ON wlb.workspace_id = w.id
|
||||
INNER JOIN template_versions tv ON wlb.template_version_id = tv.id
|
||||
INNER JOIN templates t ON tv.template_id = t.id AND t.active_version_id = tv.id
|
||||
WHERE tvp.desired_instances IS NOT NULL -- Consider only presets that have a prebuild configuration.
|
||||
AND wlb.transition = 'start'::workspace_transition
|
||||
AND w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'
|
||||
),
|
||||
time_sorted_builds AS (
|
||||
-- Group builds by preset, then sort each group by created_at.
|
||||
SELECT fb.template_version_id, fb.created_at, fb.preset_id, fb.job_status, fb.desired_instances,
|
||||
ROW_NUMBER() OVER (PARTITION BY fb.preset_id ORDER BY fb.created_at DESC) as rn
|
||||
FROM filtered_builds fb
|
||||
),
|
||||
failed_count AS (
|
||||
-- Count failed builds per preset in the given period
|
||||
SELECT preset_id, COUNT(*) AS num_failed
|
||||
FROM filtered_builds
|
||||
WHERE job_status = 'failed'::provisioner_job_status
|
||||
AND created_at >= @lookback::timestamptz
|
||||
GROUP BY preset_id
|
||||
)
|
||||
SELECT
|
||||
tsb.template_version_id,
|
||||
tsb.preset_id,
|
||||
COALESCE(fc.num_failed, 0)::int AS num_failed,
|
||||
MAX(tsb.created_at)::timestamptz AS last_build_at
|
||||
FROM time_sorted_builds tsb
|
||||
LEFT JOIN failed_count fc ON fc.preset_id = tsb.preset_id
|
||||
WHERE tsb.rn <= tsb.desired_instances -- Fetch the last N builds, where N is the number of desired instances; if any fail, we backoff
|
||||
AND tsb.job_status = 'failed'::provisioner_job_status
|
||||
AND created_at >= @lookback::timestamptz
|
||||
GROUP BY tsb.template_version_id, tsb.preset_id, fc.num_failed;
|
||||
|
||||
-- name: GetPrebuildMetrics :many
|
||||
SELECT
|
||||
t.name as template_name,
|
||||
tvp.name as preset_name,
|
||||
o.name as organization_name,
|
||||
COUNT(*) as created_count,
|
||||
COUNT(*) FILTER (WHERE pj.job_status = 'failed'::provisioner_job_status) as failed_count,
|
||||
COUNT(*) FILTER (
|
||||
WHERE w.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid -- The system user responsible for prebuilds.
|
||||
) as claimed_count
|
||||
FROM workspaces w
|
||||
INNER JOIN workspace_prebuild_builds wpb ON wpb.workspace_id = w.id
|
||||
INNER JOIN templates t ON t.id = w.template_id
|
||||
INNER JOIN template_version_presets tvp ON tvp.id = wpb.template_version_preset_id
|
||||
INNER JOIN provisioner_jobs pj ON pj.id = wpb.job_id
|
||||
INNER JOIN organizations o ON o.id = w.organization_id
|
||||
WHERE NOT t.deleted AND wpb.build_number = 1
|
||||
GROUP BY t.name, tvp.name, o.name
|
||||
ORDER BY t.name, tvp.name, o.name;
|
||||
@@ -1,8 +1,18 @@
|
||||
-- name: InsertPreset :one
|
||||
INSERT INTO
|
||||
template_version_presets (template_version_id, name, created_at)
|
||||
VALUES
|
||||
(@template_version_id, @name, @created_at) RETURNING *;
|
||||
INSERT INTO template_version_presets (
|
||||
template_version_id,
|
||||
name,
|
||||
created_at,
|
||||
desired_instances,
|
||||
invalidate_after_secs
|
||||
)
|
||||
VALUES (
|
||||
@template_version_id,
|
||||
@name,
|
||||
@created_at,
|
||||
@desired_instances,
|
||||
@invalidate_after_secs
|
||||
) RETURNING *;
|
||||
|
||||
-- name: InsertPresetParameters :many
|
||||
INSERT INTO
|
||||
@@ -38,3 +48,9 @@ FROM
|
||||
INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id
|
||||
WHERE
|
||||
template_version_presets.template_version_id = @template_version_id;
|
||||
|
||||
-- name: GetPresetByID :one
|
||||
SELECT tvp.*, 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 = @preset_id;
|
||||
|
||||
@@ -103,6 +103,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)));
|
||||
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);
|
||||
UniqueNotificationMessagesDedupeHashIndex UniqueConstraint = "notification_messages_dedupe_hash_idx" // CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash);
|
||||
|
||||
@@ -1856,9 +1856,11 @@ func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger
|
||||
func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, templateVersionID uuid.UUID, protoPreset *sdkproto.Preset, t time.Time) error {
|
||||
err := db.InTx(func(tx database.Store) error {
|
||||
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
|
||||
TemplateVersionID: templateVersionID,
|
||||
Name: protoPreset.Name,
|
||||
CreatedAt: t,
|
||||
TemplateVersionID: templateVersionID,
|
||||
Name: protoPreset.Name,
|
||||
CreatedAt: t,
|
||||
DesiredInstances: sql.NullInt32{},
|
||||
InvalidateAfterSecs: sql.NullInt32{},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert preset: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user