Skip to content

feat: implement claiming of prebuilt workspaces #17458

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Apr 24, 2025
Merged
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8b9e30d
feat: implement claiming of prebuilt workspaces
evgeniy-scherbina Apr 17, 2025
d9497df
Merge branch 'main' of github.com:/coder/coder into yevhenii/512-clai…
dannykopping Apr 22, 2025
217e46f
Reverting unncessary changes
dannykopping Apr 21, 2025
c459533
Refactoring
dannykopping Apr 22, 2025
f905219
Removing unnecessary field
dannykopping Apr 22, 2025
8266338
make fmt
dannykopping Apr 22, 2025
4098ed7
CR's fixes
evgeniy-scherbina Apr 22, 2025
50d23e4
CR's fixes
evgeniy-scherbina Apr 22, 2025
546b7ae
Update coderd/workspaces.go
evgeniy-scherbina Apr 22, 2025
85f7926
CR's fixes
evgeniy-scherbina Apr 22, 2025
ee9908c
CR's fixes
evgeniy-scherbina Apr 22, 2025
70bf179
CR's fixes
evgeniy-scherbina Apr 22, 2025
7a72d03
CR's fixes
evgeniy-scherbina Apr 22, 2025
b246589
fix tests by updating noop.go
evgeniy-scherbina Apr 22, 2025
ff8d3de
Merge branch 'main' of github.com:/coder/coder into yevhenii/512-clai…
dannykopping Apr 23, 2025
fcdbba8
test: add failure testcase scenario
evgeniy-scherbina Apr 23, 2025
e7b7f80
refactor: revert interface changes
evgeniy-scherbina Apr 23, 2025
e7455db
refactor: simplify signature of claim method
evgeniy-scherbina Apr 23, 2025
077d7f0
refactor: CR's fixes
evgeniy-scherbina Apr 23, 2025
0d426c7
Merge remote-tracking branch 'origin/main' into yevhenii/512-claim-pr…
evgeniy-scherbina Apr 23, 2025
663a8c0
refactor: CR's fixes
evgeniy-scherbina Apr 23, 2025
bc31fac
refactor: make lint
evgeniy-scherbina Apr 23, 2025
087bd20
refactor: fix linter
evgeniy-scherbina Apr 23, 2025
9f66169
refactor: remove PrebuildsOrchestrator context from claim tests
evgeniy-scherbina Apr 23, 2025
7f25f24
refactor: don't fail test in a goroutine
evgeniy-scherbina Apr 23, 2025
ff9eeb4
docs: fix ssh coder example in testing-templates doc (#17550)
EdwardAngert Apr 23, 2025
31b873f
fix: add websocket close handling (#17548)
jaaydenh Apr 23, 2025
4fe770d
ci: move go install tools to separate action (#17552)
ethanndickson Apr 24, 2025
8d8fc99
chore(dogfood): allow provider minor version updates (#17554)
matifali Apr 24, 2025
af32325
fix(examples/templates/docker-devcontainer): update folder path and p…
Aericio Apr 24, 2025
0826e8a
feat: enable masking password inputs instead of blocking echo (#17469)
ibetitsmike Apr 24, 2025
cbb6c12
fix(examples/templates/kubernetes-devcontainer): update coder provide…
matifali Apr 24, 2025
e466844
docs: add automatic release calendar updates in docs (#17531)
matifali Apr 24, 2025
f24557b
chore(dogfood): add windsurf module (#17558)
matifali Apr 24, 2025
fa65564
Merge branch 'main' of github.com:/coder/coder into yevhenii/512-clai…
dannykopping Apr 24, 2025
53b38d7
fix: use userCtx instead of prebuildCtx for claiming prebuild
evgeniy-scherbina Apr 24, 2025
6c41e03
refactor: CR's fixes
evgeniy-scherbina Apr 24, 2025
9173e9a
CR's fixes
evgeniy-scherbina Apr 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: add failure testcase scenario
  • Loading branch information
evgeniy-scherbina committed Apr 23, 2025
commit fcdbba8ecc1892af0d94951444023b4dfcd491ac
197 changes: 197 additions & 0 deletions enterprise/coderd/prebuilds/claim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"context"
"database/sql"
"errors"
"strings"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -66,6 +67,32 @@
return result, err
}

type errorStore struct {
claimingErr error

database.Store
}

func newErrorStore(db database.Store, claimingErr error) *errorStore {
return &errorStore{
Store: db,
claimingErr: claimingErr,
}
}

func (es *errorStore) InTx(fn func(store database.Store) error, opts *database.TxOptions) error {
// Pass failure store down into transaction store.
return es.Store.InTx(func(store database.Store) error {
newES := newErrorStore(store, es.claimingErr)

return fn(newES)
}, opts)
}

func (es *errorStore) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
return database.ClaimPrebuiltWorkspaceRow{}, es.claimingErr
}

func TestClaimPrebuild(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -151,7 +178,7 @@
runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuiltWorkspacesRow, desiredInstances*presetCount)
require.Eventually(t, func() bool {
rows, err := spy.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)

Check failure on line 181 in enterprise/coderd/prebuilds/claim_test.go

View workflow job for this annotation

GitHub Actions / lint

ruleguard: Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834) (gocritic)

for _, row := range rows {
runningPrebuilds[row.CurrentPresetID.UUID] = row
Expand Down Expand Up @@ -246,7 +273,7 @@

require.Eventually(t, func() bool {
rows, err := spy.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)

Check failure on line 276 in enterprise/coderd/prebuilds/claim_test.go

View workflow job for this annotation

GitHub Actions / lint

ruleguard: Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834) (gocritic)

t.Logf("found %d running prebuilds so far, want %d", len(rows), expectedPrebuildsCount)

Expand Down Expand Up @@ -284,6 +311,176 @@
}
}

func TestClaimPrebuild_CheckDifferentErrors(t *testing.T) {
t.Parallel()

if !dbtestutil.WillUsePostgres() {
t.Skip("This test requires postgres")
}

const (
desiredInstances = 1
presetCount = 2

expectedPrebuildsCount = desiredInstances * presetCount
)

cases := map[string]struct {
claimingErr error
checkFn func(
t *testing.T,
ctx context.Context,
store database.Store,
userClient *codersdk.Client,
user codersdk.User,
templateVersionID uuid.UUID,
presetID uuid.UUID,
)
}{
"ErrNoClaimablePrebuiltWorkspaces is returned": {
claimingErr: agplprebuilds.ErrNoClaimablePrebuiltWorkspaces,
checkFn: func(
t *testing.T,
ctx context.Context,
store database.Store,
userClient *codersdk.Client,
user codersdk.User,
templateVersionID uuid.UUID,
presetID uuid.UUID,
) {
// When: a user creates a new workspace with a preset for which prebuilds are configured.
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{
TemplateVersionID: templateVersionID,
Name: workspaceName,
TemplateVersionPresetID: presetID,
})

require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID)

// Then: the number of running prebuilds hasn't changed because claiming prebuild is failed and we fallback to creating new workspace.
currentPrebuilds, err := store.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)
require.Equal(t, expectedPrebuildsCount, len(currentPrebuilds))
},
},
"unexpected error during claim is returned": {
claimingErr: errors.New("unexpected error during claim"),
checkFn: func(
t *testing.T,
ctx context.Context,
store database.Store,
userClient *codersdk.Client,
user codersdk.User,
templateVersionID uuid.UUID,
presetID uuid.UUID,
) {
// When: a user creates a new workspace with a preset for which prebuilds are configured.
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
_, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{
TemplateVersionID: templateVersionID,
Name: workspaceName,
TemplateVersionPresetID: presetID,
})

// Then: unexpected error happened and was propagated all the way to the caller
require.Error(t, err)
require.ErrorContains(t, err, "unexpected error during claim")

// Then: the number of running prebuilds hasn't changed because claiming prebuild is failed.
currentPrebuilds, err := store.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)
require.Equal(t, expectedPrebuildsCount, len(currentPrebuilds))
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
t.Parallel()

// Setup.
ctx := testutil.Context(t, testutil.WaitMedium)
db, pubsub := dbtestutil.NewDB(t)
errorStore := newErrorStore(db, tc.claimingErr)

logger := testutil.Logger(t)
client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
IncludeProvisionerDaemon: true,
Database: errorStore,
Pubsub: pubsub,
},

EntitlementsUpdateInterval: time.Second,
})

reconciler := prebuilds.NewStoreReconciler(errorStore, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t))
var claimer agplprebuilds.Claimer = &prebuilds.EnterpriseClaimer{}
api.AGPL.PrebuildsClaimer.Store(&claimer)

version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(desiredInstances))
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
presets, err := client.TemplateVersionPresets(ctx, version.ID)
require.NoError(t, err)
require.Len(t, presets, presetCount)

userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())

ctx = dbauthz.AsPrebuildsOrchestrator(ctx)

// Given: the reconciliation state is snapshot.
state, err := reconciler.SnapshotState(ctx, errorStore)
require.NoError(t, err)
require.Len(t, state.Presets, presetCount)

// When: a reconciliation is setup for each preset.
for _, preset := range presets {
ps, err := state.FilterByPreset(preset.ID)
require.NoError(t, err)
require.NotNil(t, ps)
actions, err := reconciler.CalculateActions(ctx, *ps)
require.NoError(t, err)
require.NotNil(t, actions)

require.NoError(t, reconciler.ReconcilePreset(ctx, *ps))
}

// Given: a set of running, eligible prebuilds eventually starts up.
runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuiltWorkspacesRow, desiredInstances*presetCount)
require.Eventually(t, func() bool {
rows, err := errorStore.GetRunningPrebuiltWorkspaces(ctx)
require.NoError(t, err)

Check failure on line 455 in enterprise/coderd/prebuilds/claim_test.go

View workflow job for this annotation

GitHub Actions / lint

ruleguard: Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834) (gocritic)

for _, row := range rows {
runningPrebuilds[row.CurrentPresetID.UUID] = row

agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, row.ID)
require.NoError(t, err)

// Workspaces are eligible once its agent is marked "ready".
for _, agent := range agents {
require.NoError(t, db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
ID: agent.ID,
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true},
ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
}))
}
}

t.Logf("found %d running prebuilds so far, want %d", len(runningPrebuilds), expectedPrebuildsCount)

return len(runningPrebuilds) == expectedPrebuildsCount
}, testutil.WaitSuperLong, testutil.IntervalSlow)

tc.checkFn(t, ctx, errorStore, userClient, user, version.ID, presets[0].ID)
})
}
}

func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
Expand Down
Loading