Skip to content

Commit 118f12a

Browse files
evgeniy-scherbinadannykoppingEdwardAngertjaaydenhethanndickson
authored
feat: implement claiming of prebuilt workspaces (coder#17458)
Signed-off-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: Danny Kopping <danny@coder.com> Co-authored-by: Edward Angert <EdwardAngert@users.noreply.github.com> Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com> Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com> Co-authored-by: M Atif Ali <atif@coder.com> Co-authored-by: Aericio <16523741+Aericio@users.noreply.github.com> Co-authored-by: M Atif Ali <me@matifali.dev> Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
1 parent 25dacd3 commit 118f12a

File tree

8 files changed

+731
-29
lines changed

8 files changed

+731
-29
lines changed

coderd/coderd.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
"github.com/coder/coder/v2/coderd/entitlements"
4646
"github.com/coder/coder/v2/coderd/files"
4747
"github.com/coder/coder/v2/coderd/idpsync"
48+
"github.com/coder/coder/v2/coderd/prebuilds"
4849
"github.com/coder/coder/v2/coderd/runtimeconfig"
4950
"github.com/coder/coder/v2/coderd/webpush"
5051

@@ -595,6 +596,7 @@ func New(options *Options) *API {
595596
f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String())
596597
api.AppearanceFetcher.Store(&f)
597598
api.PortSharer.Store(&portsharing.DefaultPortSharer)
599+
api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer)
598600
buildInfo := codersdk.BuildInfoResponse{
599601
ExternalURL: buildinfo.ExternalURL(),
600602
Version: buildinfo.Version(),
@@ -1569,6 +1571,7 @@ type API struct {
15691571
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
15701572
PortSharer atomic.Pointer[portsharing.PortSharer]
15711573
FileCache files.Cache
1574+
PrebuildsClaimer atomic.Pointer[prebuilds.Claimer]
15721575

15731576
UpdatesProvider tailnet.WorkspaceUpdatesProvider
15741577

coderd/prebuilds/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ package prebuilds
22

33
import (
44
"context"
5+
6+
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
58
)
69

10+
var ErrNoClaimablePrebuiltWorkspaces = xerrors.New("no claimable prebuilt workspaces found")
11+
712
// ReconciliationOrchestrator manages the lifecycle of prebuild reconciliation.
813
// It runs a continuous loop to check and reconcile prebuild states, and can be stopped gracefully.
914
type ReconciliationOrchestrator interface {
@@ -25,3 +30,8 @@ type Reconciler interface {
2530
// in parallel, creating or deleting prebuilds as needed to reach their desired states.
2631
ReconcileAll(ctx context.Context) error
2732
}
33+
34+
type Claimer interface {
35+
Claim(ctx context.Context, userID uuid.UUID, name string, presetID uuid.UUID) (*uuid.UUID, error)
36+
Initiator() uuid.UUID
37+
}

coderd/prebuilds/noop.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package prebuilds
33
import (
44
"context"
55

6+
"github.com/google/uuid"
7+
68
"github.com/coder/coder/v2/coderd/database"
79
)
810

@@ -33,3 +35,16 @@ func (NoopReconciler) CalculateActions(context.Context, PresetSnapshot) (*Reconc
3335
}
3436

3537
var _ ReconciliationOrchestrator = NoopReconciler{}
38+
39+
type AGPLPrebuildClaimer struct{}
40+
41+
func (AGPLPrebuildClaimer) Claim(context.Context, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
42+
// Not entitled to claim prebuilds in AGPL version.
43+
return nil, ErrNoClaimablePrebuiltWorkspaces
44+
}
45+
46+
func (AGPLPrebuildClaimer) Initiator() uuid.UUID {
47+
return uuid.Nil
48+
}
49+
50+
var DefaultClaimer Claimer = AGPLPrebuildClaimer{}

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2471,10 +2471,11 @@ type TemplateVersionImportJob struct {
24712471

24722472
// WorkspaceProvisionJob is the payload for the "workspace_provision" job type.
24732473
type WorkspaceProvisionJob struct {
2474-
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
2475-
DryRun bool `json:"dry_run"`
2476-
IsPrebuild bool `json:"is_prebuild,omitempty"`
2477-
LogLevel string `json:"log_level,omitempty"`
2474+
WorkspaceBuildID uuid.UUID `json:"workspace_build_id"`
2475+
DryRun bool `json:"dry_run"`
2476+
IsPrebuild bool `json:"is_prebuild,omitempty"`
2477+
PrebuildClaimedByUser uuid.UUID `json:"prebuild_claimed_by,omitempty"`
2478+
LogLevel string `json:"log_level,omitempty"`
24782479
}
24792480

24802481
// TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type.

coderd/workspaces.go

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"golang.org/x/xerrors"
1919

2020
"cdr.dev/slog"
21+
2122
"github.com/coder/coder/v2/agent/proto"
2223
"github.com/coder/coder/v2/coderd/audit"
2324
"github.com/coder/coder/v2/coderd/database"
@@ -28,6 +29,7 @@ import (
2829
"github.com/coder/coder/v2/coderd/httpapi"
2930
"github.com/coder/coder/v2/coderd/httpmw"
3031
"github.com/coder/coder/v2/coderd/notifications"
32+
"github.com/coder/coder/v2/coderd/prebuilds"
3133
"github.com/coder/coder/v2/coderd/rbac"
3234
"github.com/coder/coder/v2/coderd/rbac/policy"
3335
"github.com/coder/coder/v2/coderd/schedule"
@@ -636,33 +638,57 @@ func createWorkspace(
636638
workspaceBuild *database.WorkspaceBuild
637639
provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow
638640
)
641+
639642
err = api.Database.InTx(func(db database.Store) error {
640-
now := dbtime.Now()
641-
// Workspaces are created without any versions.
642-
minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
643-
ID: uuid.New(),
644-
CreatedAt: now,
645-
UpdatedAt: now,
646-
OwnerID: owner.ID,
647-
OrganizationID: template.OrganizationID,
648-
TemplateID: template.ID,
649-
Name: req.Name,
650-
AutostartSchedule: dbAutostartSchedule,
651-
NextStartAt: nextStartAt,
652-
Ttl: dbTTL,
653-
// The workspaces page will sort by last used at, and it's useful to
654-
// have the newly created workspace at the top of the list!
655-
LastUsedAt: dbtime.Now(),
656-
AutomaticUpdates: dbAU,
657-
})
658-
if err != nil {
659-
return xerrors.Errorf("insert workspace: %w", err)
643+
var (
644+
workspaceID uuid.UUID
645+
claimedWorkspace *database.Workspace
646+
prebuildsClaimer = *api.PrebuildsClaimer.Load()
647+
)
648+
649+
// If a template preset was chosen, try claim a prebuilt workspace.
650+
if req.TemplateVersionPresetID != uuid.Nil {
651+
// Try and claim an eligible prebuild, if available.
652+
claimedWorkspace, err = claimPrebuild(ctx, prebuildsClaimer, db, api.Logger, req, owner)
653+
if err != nil && !errors.Is(err, prebuilds.ErrNoClaimablePrebuiltWorkspaces) {
654+
return xerrors.Errorf("claim prebuild: %w", err)
655+
}
656+
}
657+
658+
// No prebuild found; regular flow.
659+
if claimedWorkspace == nil {
660+
now := dbtime.Now()
661+
// Workspaces are created without any versions.
662+
minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
663+
ID: uuid.New(),
664+
CreatedAt: now,
665+
UpdatedAt: now,
666+
OwnerID: owner.ID,
667+
OrganizationID: template.OrganizationID,
668+
TemplateID: template.ID,
669+
Name: req.Name,
670+
AutostartSchedule: dbAutostartSchedule,
671+
NextStartAt: nextStartAt,
672+
Ttl: dbTTL,
673+
// The workspaces page will sort by last used at, and it's useful to
674+
// have the newly created workspace at the top of the list!
675+
LastUsedAt: dbtime.Now(),
676+
AutomaticUpdates: dbAU,
677+
})
678+
if err != nil {
679+
return xerrors.Errorf("insert workspace: %w", err)
680+
}
681+
workspaceID = minimumWorkspace.ID
682+
} else {
683+
// Prebuild found!
684+
workspaceID = claimedWorkspace.ID
685+
initiatorID = prebuildsClaimer.Initiator()
660686
}
661687

662688
// We have to refetch the workspace for the joined in fields.
663689
// TODO: We can use WorkspaceTable for the builder to not require
664690
// this extra fetch.
665-
workspace, err = db.GetWorkspaceByID(ctx, minimumWorkspace.ID)
691+
workspace, err = db.GetWorkspaceByID(ctx, workspaceID)
666692
if err != nil {
667693
return xerrors.Errorf("get workspace by ID: %w", err)
668694
}
@@ -676,6 +702,13 @@ func createWorkspace(
676702
if req.TemplateVersionID != uuid.Nil {
677703
builder = builder.VersionID(req.TemplateVersionID)
678704
}
705+
if req.TemplateVersionPresetID != uuid.Nil {
706+
builder = builder.TemplateVersionPresetID(req.TemplateVersionPresetID)
707+
}
708+
if claimedWorkspace != nil {
709+
builder = builder.MarkPrebuildClaimedBy(owner.ID)
710+
}
711+
679712
if req.EnableDynamicParameters && api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) {
680713
builder = builder.UsingDynamicParameters()
681714
}
@@ -842,6 +875,21 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C
842875
return template, true
843876
}
844877

878+
func claimPrebuild(ctx context.Context, claimer prebuilds.Claimer, db database.Store, logger slog.Logger, req codersdk.CreateWorkspaceRequest, owner workspaceOwner) (*database.Workspace, error) {
879+
claimedID, err := claimer.Claim(ctx, owner.ID, req.Name, req.TemplateVersionPresetID)
880+
if err != nil {
881+
// TODO: enhance this by clarifying whether this *specific* prebuild failed or whether there are none to claim.
882+
return nil, xerrors.Errorf("claim prebuild: %w", err)
883+
}
884+
885+
lookup, err := db.GetWorkspaceByID(ctx, *claimedID)
886+
if err != nil {
887+
logger.Error(ctx, "unable to find claimed workspace by ID", slog.Error(err), slog.F("claimed_prebuild_id", claimedID.String()))
888+
return nil, xerrors.Errorf("find claimed workspace by ID %q: %w", claimedID.String(), err)
889+
}
890+
return &lookup, nil
891+
}
892+
845893
func (api *API) notifyWorkspaceCreated(
846894
ctx context.Context,
847895
receiverID uuid.UUID,

coderd/wsbuilder/wsbuilder.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ type Builder struct {
7676
parameterValues *[]string
7777
templateVersionPresetParameterValues []database.TemplateVersionPresetParameter
7878

79-
prebuild bool
79+
prebuild bool
80+
prebuildClaimedBy uuid.UUID
8081

8182
verifyNoLegacyParametersOnce bool
8283
}
@@ -179,6 +180,12 @@ func (b Builder) MarkPrebuild() Builder {
179180
return b
180181
}
181182

183+
func (b Builder) MarkPrebuildClaimedBy(userID uuid.UUID) Builder {
184+
// nolint: revive
185+
b.prebuildClaimedBy = userID
186+
return b
187+
}
188+
182189
func (b Builder) UsingDynamicParameters() Builder {
183190
b.dynamicParametersEnabled = true
184191
return b
@@ -315,9 +322,10 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
315322

316323
workspaceBuildID := uuid.New()
317324
input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{
318-
WorkspaceBuildID: workspaceBuildID,
319-
LogLevel: b.logLevel,
320-
IsPrebuild: b.prebuild,
325+
WorkspaceBuildID: workspaceBuildID,
326+
LogLevel: b.logLevel,
327+
IsPrebuild: b.prebuild,
328+
PrebuildClaimedByUser: b.prebuildClaimedBy,
321329
})
322330
if err != nil {
323331
return nil, nil, nil, BuildError{

enterprise/coderd/prebuilds/claim.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package prebuilds
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/prebuilds"
13+
)
14+
15+
type EnterpriseClaimer struct {
16+
store database.Store
17+
}
18+
19+
func NewEnterpriseClaimer(store database.Store) *EnterpriseClaimer {
20+
return &EnterpriseClaimer{
21+
store: store,
22+
}
23+
}
24+
25+
func (c EnterpriseClaimer) Claim(
26+
ctx context.Context,
27+
userID uuid.UUID,
28+
name string,
29+
presetID uuid.UUID,
30+
) (*uuid.UUID, error) {
31+
result, err := c.store.ClaimPrebuiltWorkspace(ctx, database.ClaimPrebuiltWorkspaceParams{
32+
NewUserID: userID,
33+
NewName: name,
34+
PresetID: presetID,
35+
})
36+
if err != nil {
37+
switch {
38+
// No eligible prebuilds found
39+
case errors.Is(err, sql.ErrNoRows):
40+
return nil, prebuilds.ErrNoClaimablePrebuiltWorkspaces
41+
default:
42+
return nil, xerrors.Errorf("claim prebuild for user %q: %w", userID.String(), err)
43+
}
44+
}
45+
46+
return &result.ID, nil
47+
}
48+
49+
func (EnterpriseClaimer) Initiator() uuid.UUID {
50+
return prebuilds.SystemUserID
51+
}
52+
53+
var _ prebuilds.Claimer = &EnterpriseClaimer{}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy