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"
|
"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/policy"
|
||||||
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
"github.com/coder/coder/v2/coderd/rbac/rolestore"
|
||||||
|
|
||||||
@@ -361,6 +362,27 @@ var (
|
|||||||
}),
|
}),
|
||||||
Scope: rbac.ScopeAll,
|
Scope: rbac.ScopeAll,
|
||||||
}.WithCachedASTValue()
|
}.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
|
// 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)
|
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{
|
var AsRemoveActor = rbac.Subject{
|
||||||
ID: "remove-actor",
|
ID: "remove-actor",
|
||||||
}
|
}
|
||||||
@@ -1109,6 +1137,31 @@ func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg data
|
|||||||
return q.db.BulkMarkNotificationMessagesSent(ctx, arg)
|
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 {
|
func (q *querier) CleanTailnetCoordinators(ctx context.Context) error {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceTailnetCoordinator); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1130,6 +1183,13 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
|
|||||||
return q.db.CleanTailnetTunnels(ctx)
|
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) {
|
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 {
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceInboxNotification.WithOwner(userID.String())); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -2096,6 +2156,30 @@ func (q *querier) GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUI
|
|||||||
return q.db.GetParameterSchemasByJobID(ctx, jobID)
|
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) {
|
func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate); err != nil {
|
||||||
return database.TemplateVersionPreset{}, err
|
return database.TemplateVersionPreset{}, err
|
||||||
@@ -2113,6 +2197,14 @@ func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, te
|
|||||||
return q.db.GetPresetParametersByTemplateVersionID(ctx, templateVersionID)
|
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) {
|
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.
|
// An actor can read template version presets if they can read the related template version.
|
||||||
_, err := q.GetTemplateVersionByID(ctx, templateVersionID)
|
_, err := q.GetTemplateVersionByID(ctx, templateVersionID)
|
||||||
@@ -2164,13 +2256,13 @@ func (q *querier) GetProvisionerJobByID(ctx context.Context, id uuid.UUID) (data
|
|||||||
// can read the job.
|
// can read the job.
|
||||||
_, err := q.GetWorkspaceBuildByJobID(ctx, id)
|
_, err := q.GetWorkspaceBuildByJobID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.ProvisionerJob{}, err
|
return database.ProvisionerJob{}, xerrors.Errorf("fetch related workspace build: %w", err)
|
||||||
}
|
}
|
||||||
case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport:
|
case database.ProvisionerJobTypeTemplateVersionDryRun, database.ProvisionerJobTypeTemplateVersionImport:
|
||||||
// Authorized call to get template version.
|
// Authorized call to get template version.
|
||||||
_, err := authorizedTemplateVersionFromJob(ctx, q, job)
|
_, err := authorizedTemplateVersionFromJob(ctx, q, job)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return database.ProvisionerJob{}, err
|
return database.ProvisionerJob{}, xerrors.Errorf("fetch related template version: %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return database.ProvisionerJob{}, xerrors.Errorf("unknown job type: %q", job.Type)
|
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)
|
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) {
|
func (q *querier) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -2387,6 +2487,15 @@ func (q *querier) GetTemplateParameterInsights(ctx context.Context, arg database
|
|||||||
return q.db.GetTemplateParameterInsights(ctx, arg)
|
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) {
|
func (q *querier) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||||
if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil {
|
if err := q.authorizeTemplateInsights(ctx, arg.TemplateIDs); err != nil {
|
||||||
return nil, err
|
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() {
|
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
|
||||||
s.Run("GetOAuth2ProviderApps", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("GetOAuth2ProviderApps", s.Subtest(func(db database.Store, check *expects) {
|
||||||
apps := []database.OAuth2ProviderApp{
|
apps := []database.OAuth2ProviderApp{
|
||||||
|
|||||||
@@ -1196,6 +1196,29 @@ func TelemetryItem(t testing.TB, db database.Store, seed database.TelemetryItem)
|
|||||||
return item
|
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 {
|
func provisionerJobTiming(t testing.TB, db database.Store, seed database.ProvisionerJobTiming) database.ProvisionerJobTiming {
|
||||||
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
|
timing, err := db.InsertProvisionerJobTimings(genCtx, database.InsertProvisionerJobTimingsParams{
|
||||||
JobID: takeFirst(seed.JobID, uuid.New()),
|
JobID: takeFirst(seed.JobID, uuid.New()),
|
||||||
|
|||||||
@@ -1741,6 +1741,10 @@ func (*FakeQuerier) BulkMarkNotificationMessagesSent(_ context.Context, arg data
|
|||||||
return int64(len(arg.IDs)), nil
|
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 {
|
func (*FakeQuerier) CleanTailnetCoordinators(_ context.Context) error {
|
||||||
return ErrUnimplemented
|
return ErrUnimplemented
|
||||||
}
|
}
|
||||||
@@ -1753,6 +1757,10 @@ func (*FakeQuerier) CleanTailnetTunnels(context.Context) error {
|
|||||||
return ErrUnimplemented
|
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) {
|
func (q *FakeQuerier) CountUnreadInboxNotificationsByUserID(_ context.Context, userID uuid.UUID) (int64, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
@@ -4212,6 +4220,44 @@ func (q *FakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
|
|||||||
return parameters, nil
|
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) {
|
func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
@@ -4254,6 +4300,10 @@ func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context,
|
|||||||
return parameters, nil
|
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) {
|
func (q *FakeQuerier) GetPresetsByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||||
q.mutex.RLock()
|
q.mutex.RLock()
|
||||||
defer q.mutex.RUnlock()
|
defer q.mutex.RUnlock()
|
||||||
@@ -4917,6 +4967,10 @@ func (q *FakeQuerier) GetReplicasUpdatedAfter(_ context.Context, updatedAt time.
|
|||||||
return replicas, nil
|
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) {
|
func (q *FakeQuerier) GetRuntimeConfig(_ context.Context, key string) (string, error) {
|
||||||
q.mutex.Lock()
|
q.mutex.Lock()
|
||||||
defer q.mutex.Unlock()
|
defer q.mutex.Unlock()
|
||||||
@@ -5956,6 +6010,10 @@ func (q *FakeQuerier) GetTemplateParameterInsights(ctx context.Context, arg data
|
|||||||
return rows, nil
|
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) {
|
func (q *FakeQuerier) GetTemplateUsageStats(_ context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||||
err := validateDatabaseType(arg)
|
err := validateDatabaseType(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -6426,6 +6484,10 @@ func (q *FakeQuerier) GetUserCount(_ context.Context, includeSystem bool) (int64
|
|||||||
if !u.Deleted {
|
if !u.Deleted {
|
||||||
existing++
|
existing++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !includeSystem && u.IsSystem {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return existing, nil
|
return existing, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,6 +158,13 @@ func (m queryMetricsStore) BulkMarkNotificationMessagesSent(ctx context.Context,
|
|||||||
return r0, r1
|
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 {
|
func (m queryMetricsStore) CleanTailnetCoordinators(ctx context.Context) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := m.s.CleanTailnetCoordinators(ctx)
|
err := m.s.CleanTailnetCoordinators(ctx)
|
||||||
@@ -179,6 +186,13 @@ func (m queryMetricsStore) CleanTailnetTunnels(ctx context.Context) error {
|
|||||||
return r0
|
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) {
|
func (m queryMetricsStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.CountUnreadInboxNotificationsByUserID(ctx, userID)
|
r0, r1 := m.s.CountUnreadInboxNotificationsByUserID(ctx, userID)
|
||||||
@@ -1075,6 +1089,20 @@ func (m queryMetricsStore) GetParameterSchemasByJobID(ctx context.Context, jobID
|
|||||||
return schemas, err
|
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) {
|
func (m queryMetricsStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetPresetByWorkspaceBuildID(ctx, workspaceBuildID)
|
r0, r1 := m.s.GetPresetByWorkspaceBuildID(ctx, workspaceBuildID)
|
||||||
@@ -1089,6 +1117,13 @@ func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Co
|
|||||||
return r0, r1
|
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) {
|
func (m queryMetricsStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetPresetsByTemplateVersionID(ctx, templateVersionID)
|
r0, r1 := m.s.GetPresetsByTemplateVersionID(ctx, templateVersionID)
|
||||||
@@ -1222,6 +1257,13 @@ func (m queryMetricsStore) GetReplicasUpdatedAfter(ctx context.Context, updatedA
|
|||||||
return replicas, err
|
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) {
|
func (m queryMetricsStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetRuntimeConfig(ctx, key)
|
r0, r1 := m.s.GetRuntimeConfig(ctx, key)
|
||||||
@@ -1348,6 +1390,13 @@ func (m queryMetricsStore) GetTemplateParameterInsights(ctx context.Context, arg
|
|||||||
return r0, r1
|
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) {
|
func (m queryMetricsStore) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetTemplateUsageStats(ctx, arg)
|
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)
|
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.
|
// CleanTailnetCoordinators mocks base method.
|
||||||
func (m *MockStore) CleanTailnetCoordinators(ctx context.Context) error {
|
func (m *MockStore) CleanTailnetCoordinators(ctx context.Context) error {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// CountUnreadInboxNotificationsByUserID mocks base method.
|
||||||
func (m *MockStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
func (m *MockStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// GetPresetByWorkspaceBuildID mocks base method.
|
||||||
func (m *MockStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
func (m *MockStore) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// GetPresetsByTemplateVersionID mocks base method.
|
||||||
func (m *MockStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
func (m *MockStore) GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPreset, error) {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// GetRuntimeConfig mocks base method.
|
||||||
func (m *MockStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
func (m *MockStore) GetRuntimeConfig(ctx context.Context, key string) (string, error) {
|
||||||
m.ctrl.T.Helper()
|
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)
|
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.
|
// GetTemplateUsageStats mocks base method.
|
||||||
func (m *MockStore) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
func (m *MockStore) GetTemplateUsageStats(ctx context.Context, arg database.GetTemplateUsageStatsParams) ([]database.TemplateUsageStat, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
Generated
+104
-36
@@ -1401,7 +1401,9 @@ CREATE TABLE template_version_presets (
|
|||||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
template_version_id uuid NOT NULL,
|
template_version_id uuid NOT NULL,
|
||||||
name text 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 (
|
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.';
|
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 (
|
CREATE TABLE workspace_modules (
|
||||||
id uuid NOT NULL,
|
id uuid NOT NULL,
|
||||||
job_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
|
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 (
|
CREATE TABLE workspace_proxies (
|
||||||
id uuid NOT NULL,
|
id uuid NOT NULL,
|
||||||
name text 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;
|
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
|
CREATE VIEW workspaces_expanded AS
|
||||||
SELECT workspaces.id,
|
SELECT workspaces.id,
|
||||||
workspaces.created_at,
|
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 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_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);
|
CREATE INDEX idx_user_status_changes_changed_at ON user_status_changes USING btree (changed_at);
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const (
|
|||||||
LockIDDBPurge
|
LockIDDBPurge
|
||||||
LockIDNotificationsReportGenerator
|
LockIDNotificationsReportGenerator
|
||||||
LockIDCryptoKeyRotation
|
LockIDCryptoKeyRotation
|
||||||
|
LockIDReconcileTemplatePrebuilds
|
||||||
|
LockIDDeterminePrebuildsState
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenLockID generates a unique and consistent lock ID from a given string.
|
// 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');
|
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');
|
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 {
|
type TemplateVersionPreset struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
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 {
|
type TemplateVersionPresetParameter struct {
|
||||||
@@ -3636,6 +3638,17 @@ type WorkspaceBuildTable struct {
|
|||||||
TemplateVersionPresetID uuid.NullUUID `db:"template_version_preset_id" json:"template_version_preset_id"`
|
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 {
|
type WorkspaceModule struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
JobID uuid.UUID `db:"job_id" json:"job_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"`
|
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 {
|
type WorkspaceProxy struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
|
|||||||
@@ -60,9 +60,13 @@ type sqlcQuerier interface {
|
|||||||
BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error
|
BatchUpdateWorkspaceNextStartAt(ctx context.Context, arg BatchUpdateWorkspaceNextStartAtParams) error
|
||||||
BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error)
|
BulkMarkNotificationMessagesFailed(ctx context.Context, arg BulkMarkNotificationMessagesFailedParams) (int64, error)
|
||||||
BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error)
|
BulkMarkNotificationMessagesSent(ctx context.Context, arg BulkMarkNotificationMessagesSentParams) (int64, error)
|
||||||
|
ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error)
|
||||||
CleanTailnetCoordinators(ctx context.Context) error
|
CleanTailnetCoordinators(ctx context.Context) error
|
||||||
CleanTailnetLostPeers(ctx context.Context) error
|
CleanTailnetLostPeers(ctx context.Context) error
|
||||||
CleanTailnetTunnels(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)
|
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||||
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
|
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
|
||||||
DeleteAPIKeyByID(ctx context.Context, id string) error
|
DeleteAPIKeyByID(ctx context.Context, id string) error
|
||||||
@@ -230,8 +234,25 @@ type sqlcQuerier interface {
|
|||||||
GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error)
|
GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error)
|
||||||
GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error)
|
GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error)
|
||||||
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, 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)
|
GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error)
|
||||||
GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, 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)
|
GetPresetsByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPreset, error)
|
||||||
GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error)
|
GetPreviousTemplateVersion(ctx context.Context, arg GetPreviousTemplateVersionParams) (TemplateVersion, error)
|
||||||
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
|
GetProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error)
|
||||||
@@ -253,6 +274,7 @@ type sqlcQuerier interface {
|
|||||||
GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error)
|
GetQuotaConsumedForUser(ctx context.Context, arg GetQuotaConsumedForUserParams) (int64, error)
|
||||||
GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error)
|
GetReplicaByID(ctx context.Context, id uuid.UUID) (Replica, error)
|
||||||
GetReplicasUpdatedAfter(ctx context.Context, updatedAt time.Time) ([]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)
|
GetRuntimeConfig(ctx context.Context, key string) (string, error)
|
||||||
GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
|
GetTailnetAgents(ctx context.Context, id uuid.UUID) ([]TailnetAgent, error)
|
||||||
GetTailnetClientsForAgent(ctx context.Context, agentID uuid.UUID) ([]TailnetClient, 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
|
// created in the timeframe and return the aggregate usage counts of parameter
|
||||||
// values.
|
// values.
|
||||||
GetTemplateParameterInsights(ctx context.Context, arg GetTemplateParameterInsightsParams) ([]GetTemplateParameterInsightsRow, error)
|
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)
|
GetTemplateUsageStats(ctx context.Context, arg GetTemplateUsageStatsParams) ([]TemplateUsageStat, error)
|
||||||
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
|
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
|
||||||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"cdr.dev/slog/sloggers/slogtest"
|
"cdr.dev/slog/sloggers/slogtest"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
"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) {
|
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)
|
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
|
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
|
const getPresetByWorkspaceBuildID = `-- name: GetPresetByWorkspaceBuildID :one
|
||||||
SELECT
|
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
|
FROM
|
||||||
template_version_presets
|
template_version_presets
|
||||||
INNER JOIN workspace_builds ON workspace_builds.template_version_preset_id = template_version_presets.id
|
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.TemplateVersionID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.DesiredInstances,
|
||||||
|
&i.InvalidateAfterSecs,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -6023,7 +6429,7 @@ func (q *sqlQuerier) GetPresetParametersByTemplateVersionID(ctx context.Context,
|
|||||||
|
|
||||||
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
|
const getPresetsByTemplateVersionID = `-- name: GetPresetsByTemplateVersionID :many
|
||||||
SELECT
|
SELECT
|
||||||
id, template_version_id, name, created_at
|
id, template_version_id, name, created_at, desired_instances, invalidate_after_secs
|
||||||
FROM
|
FROM
|
||||||
template_version_presets
|
template_version_presets
|
||||||
WHERE
|
WHERE
|
||||||
@@ -6044,6 +6450,8 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
|||||||
&i.TemplateVersionID,
|
&i.TemplateVersionID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.DesiredInstances,
|
||||||
|
&i.InvalidateAfterSecs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -6059,26 +6467,46 @@ func (q *sqlQuerier) GetPresetsByTemplateVersionID(ctx context.Context, template
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertPreset = `-- name: InsertPreset :one
|
const insertPreset = `-- name: InsertPreset :one
|
||||||
INSERT INTO
|
INSERT INTO template_version_presets (
|
||||||
template_version_presets (template_version_id, name, created_at)
|
template_version_id,
|
||||||
VALUES
|
name,
|
||||||
($1, $2, $3) RETURNING id, template_version_id, name, created_at
|
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 {
|
type InsertPresetParams struct {
|
||||||
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
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) {
|
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
|
var i TemplateVersionPreset
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.TemplateVersionID,
|
&i.TemplateVersionID,
|
||||||
&i.Name,
|
&i.Name,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
|
&i.DesiredInstances,
|
||||||
|
&i.InvalidateAfterSecs,
|
||||||
)
|
)
|
||||||
return i, err
|
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
|
-- name: InsertPreset :one
|
||||||
INSERT INTO
|
INSERT INTO template_version_presets (
|
||||||
template_version_presets (template_version_id, name, created_at)
|
template_version_id,
|
||||||
VALUES
|
name,
|
||||||
(@template_version_id, @name, @created_at) RETURNING *;
|
created_at,
|
||||||
|
desired_instances,
|
||||||
|
invalidate_after_secs
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
@template_version_id,
|
||||||
|
@name,
|
||||||
|
@created_at,
|
||||||
|
@desired_instances,
|
||||||
|
@invalidate_after_secs
|
||||||
|
) RETURNING *;
|
||||||
|
|
||||||
-- name: InsertPresetParameters :many
|
-- name: InsertPresetParameters :many
|
||||||
INSERT INTO
|
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
|
INNER JOIN template_version_presets ON template_version_preset_parameters.template_version_preset_id = template_version_presets.id
|
||||||
WHERE
|
WHERE
|
||||||
template_version_presets.template_version_id = @template_version_id;
|
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));
|
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);
|
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)));
|
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);
|
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);
|
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);
|
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 {
|
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 {
|
err := db.InTx(func(tx database.Store) error {
|
||||||
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
|
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
|
||||||
TemplateVersionID: templateVersionID,
|
TemplateVersionID: templateVersionID,
|
||||||
Name: protoPreset.Name,
|
Name: protoPreset.Name,
|
||||||
CreatedAt: t,
|
CreatedAt: t,
|
||||||
|
DesiredInstances: sql.NullInt32{},
|
||||||
|
InvalidateAfterSecs: sql.NullInt32{},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("insert preset: %w", err)
|
return xerrors.Errorf("insert preset: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user