mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
Add prebuild claim test
Some checks failed
Deploy PR / check_pr (push) Has been cancelled
Deploy PR / get_info (push) Has been cancelled
Deploy PR / comment-pr (push) Has been cancelled
Deploy PR / build (push) Has been cancelled
Deploy PR / deploy (push) Has been cancelled
Some checks failed
Deploy PR / check_pr (push) Has been cancelled
Deploy PR / get_info (push) Has been cancelled
Deploy PR / comment-pr (push) Has been cancelled
Deploy PR / build (push) Has been cancelled
Deploy PR / deploy (push) Has been cancelled
Signed-off-by: Danny Kopping <dannykopping@gmail.com>
This commit is contained in:
@ -1104,7 +1104,7 @@ func (q *querier) BulkMarkNotificationMessagesSent(ctx context.Context, arg data
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *querier) ClaimPrebuild(ctx context.Context, newOwnerID database.ClaimPrebuildParams) (database.ClaimPrebuildRow, error) {
|
func (q *querier) ClaimPrebuild(ctx context.Context, newOwnerID database.ClaimPrebuildParams) (database.ClaimPrebuildRow, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWorkspace); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspace); err != nil {
|
||||||
return database.ClaimPrebuildRow{
|
return database.ClaimPrebuildRow{
|
||||||
ID: uuid.Nil,
|
ID: uuid.Nil,
|
||||||
}, err
|
}, err
|
||||||
|
260
enterprise/coderd/prebuilds/claim_test.go
Normal file
260
enterprise/coderd/prebuilds/claim_test.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package prebuilds_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||||
|
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
|
||||||
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeSpy struct {
|
||||||
|
database.Store
|
||||||
|
|
||||||
|
claims *atomic.Int32
|
||||||
|
claimParams *atomic.Pointer[database.ClaimPrebuildParams]
|
||||||
|
claimedWorkspace *atomic.Pointer[database.ClaimPrebuildRow]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoreSpy(db database.Store) *storeSpy {
|
||||||
|
return &storeSpy{
|
||||||
|
Store: db,
|
||||||
|
claims: &atomic.Int32{},
|
||||||
|
claimParams: &atomic.Pointer[database.ClaimPrebuildParams]{},
|
||||||
|
claimedWorkspace: &atomic.Pointer[database.ClaimPrebuildRow]{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *storeSpy) InTx(fn func(store database.Store) error, opts *database.TxOptions) error {
|
||||||
|
// Pass spy down into transaction store.
|
||||||
|
return m.Store.InTx(func(store database.Store) error {
|
||||||
|
spy := newStoreSpy(store)
|
||||||
|
spy.claims = m.claims
|
||||||
|
spy.claimParams = m.claimParams
|
||||||
|
spy.claimedWorkspace = m.claimedWorkspace
|
||||||
|
|
||||||
|
return fn(spy)
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *storeSpy) ClaimPrebuild(ctx context.Context, arg database.ClaimPrebuildParams) (database.ClaimPrebuildRow, error) {
|
||||||
|
m.claims.Add(1)
|
||||||
|
m.claimParams.Store(&arg)
|
||||||
|
result, err := m.Store.ClaimPrebuild(ctx, arg)
|
||||||
|
m.claimedWorkspace.Store(&result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClaimPrebuild(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Setup. // TODO: abstract?
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||||
|
db, pubsub := dbtestutil.NewDB(t)
|
||||||
|
spy := newStoreSpy(db)
|
||||||
|
|
||||||
|
client, _, _, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
IncludeProvisionerDaemon: true,
|
||||||
|
Database: spy,
|
||||||
|
Pubsub: pubsub,
|
||||||
|
},
|
||||||
|
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureWorkspacePrebuilds: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
controller := prebuilds.NewController(spy, pubsub, codersdk.PrebuildsConfig{}, testutil.Logger(t))
|
||||||
|
|
||||||
|
const (
|
||||||
|
desiredInstances = 1
|
||||||
|
presetCount = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
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.AsSystemRestricted(ctx)
|
||||||
|
|
||||||
|
// Given: a reconciliation completes.
|
||||||
|
controller.Reconcile(ctx, nil)
|
||||||
|
|
||||||
|
// Given: a set of running, eligible prebuilds eventually starts up.
|
||||||
|
runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuildsRow, desiredInstances*presetCount)
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
rows, err := spy.GetRunningPrebuilds(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
runningPrebuilds[row.CurrentPresetID.UUID] = row
|
||||||
|
|
||||||
|
agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, row.WorkspaceID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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), desiredInstances*presetCount)
|
||||||
|
|
||||||
|
return len(runningPrebuilds) == (desiredInstances * presetCount)
|
||||||
|
}, testutil.WaitSuperLong, testutil.IntervalSlow)
|
||||||
|
|
||||||
|
// When: a user creates a new workspace with a preset for which prebuilds are configured.
|
||||||
|
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
|
||||||
|
params := database.ClaimPrebuildParams{
|
||||||
|
NewUserID: user.ID,
|
||||||
|
NewName: workspaceName,
|
||||||
|
PresetID: presets[0].ID,
|
||||||
|
}
|
||||||
|
userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{
|
||||||
|
TemplateVersionID: version.ID,
|
||||||
|
Name: workspaceName,
|
||||||
|
TemplateVersionPresetID: presets[0].ID,
|
||||||
|
ClaimPrebuildIfAvailable: true, // TODO: doesn't do anything yet; it probably should though.
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, userWorkspace.LatestBuild.ID)
|
||||||
|
|
||||||
|
// TODO: this feels... wrong; we should probably be injecting an implementation of prebuilds.Claimer.
|
||||||
|
// Then: a prebuild should have been claimed.
|
||||||
|
require.EqualValues(t, spy.claims.Load(), 1)
|
||||||
|
require.NotNil(t, spy.claims.Load())
|
||||||
|
require.EqualValues(t, *spy.claimParams.Load(), params)
|
||||||
|
require.NotNil(t, spy.claimedWorkspace.Load())
|
||||||
|
claimed := *spy.claimedWorkspace.Load()
|
||||||
|
require.NotEqual(t, claimed, uuid.Nil)
|
||||||
|
|
||||||
|
// Then: the claimed prebuild must now be owned by the requester.
|
||||||
|
workspace, err := spy.GetWorkspaceByID(ctx, claimed.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, user.ID, workspace.OwnerID)
|
||||||
|
|
||||||
|
// Then: the number of running prebuilds has changed since one was claimed.
|
||||||
|
currentPrebuilds, err := spy.GetRunningPrebuilds(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, len(currentPrebuilds), len(runningPrebuilds))
|
||||||
|
|
||||||
|
// Then: the claimed prebuild is now missing from the running prebuilds set.
|
||||||
|
current, err := spy.GetRunningPrebuilds(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, prebuild := range current {
|
||||||
|
if prebuild.WorkspaceID == claimed.ID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.False(t, found, "claimed prebuild should not still be considered a running prebuild")
|
||||||
|
}
|
||||||
|
|
||||||
|
func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
|
||||||
|
return &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionPlan: []*proto.Response{
|
||||||
|
{
|
||||||
|
Type: &proto.Response_Plan{
|
||||||
|
Plan: &proto.PlanComplete{
|
||||||
|
Resources: []*proto.Resource{
|
||||||
|
{
|
||||||
|
Type: "compute",
|
||||||
|
Name: "main",
|
||||||
|
Agents: []*proto.Agent{
|
||||||
|
{
|
||||||
|
Name: "smith",
|
||||||
|
OperatingSystem: "linux",
|
||||||
|
Architecture: "i386",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Presets: []*proto.Preset{
|
||||||
|
{
|
||||||
|
Name: "preset-a",
|
||||||
|
Parameters: []*proto.PresetParameter{
|
||||||
|
{
|
||||||
|
Name: "k1",
|
||||||
|
Value: "v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Prebuild: &proto.Prebuild{
|
||||||
|
Instances: desiredInstances,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "preset-b",
|
||||||
|
Parameters: []*proto.PresetParameter{
|
||||||
|
{
|
||||||
|
Name: "k1",
|
||||||
|
Value: "v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Prebuild: &proto.Prebuild{
|
||||||
|
Instances: desiredInstances,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ProvisionApply: []*proto.Response{
|
||||||
|
{
|
||||||
|
Type: &proto.Response_Apply{
|
||||||
|
Apply: &proto.ApplyComplete{
|
||||||
|
Resources: []*proto.Resource{
|
||||||
|
{
|
||||||
|
Type: "compute",
|
||||||
|
Name: "main",
|
||||||
|
Agents: []*proto.Agent{
|
||||||
|
{
|
||||||
|
Name: "smith",
|
||||||
|
OperatingSystem: "linux",
|
||||||
|
Architecture: "i386",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(dannyk): test claiming a prebuild causes a replacement to be provisioned.
|
||||||
|
// TODO(dannyk): test that prebuilds are only attempted to be claimed for net-new workspace builds
|
@ -67,10 +67,10 @@ func (c *Controller) Loop(ctx context.Context) error {
|
|||||||
select {
|
select {
|
||||||
// Accept nudges from outside the control loop to trigger a new iteration.
|
// Accept nudges from outside the control loop to trigger a new iteration.
|
||||||
case template := <-c.nudgeCh:
|
case template := <-c.nudgeCh:
|
||||||
c.reconcile(ctx, template)
|
c.Reconcile(ctx, template)
|
||||||
// Trigger a new iteration on each tick.
|
// Trigger a new iteration on each tick.
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
c.reconcile(ctx, nil)
|
c.Reconcile(ctx, nil)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
c.logger.Error(context.Background(), "prebuilds reconciliation loop exited", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx)))
|
c.logger.Error(context.Background(), "prebuilds reconciliation loop exited", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx)))
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
@ -97,7 +97,7 @@ func (c *Controller) ReconcileTemplate(templateID *uuid.UUID) {
|
|||||||
c.nudgeCh <- templateID
|
c.nudgeCh <- templateID
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconcile will attempt to resolve the desired vs actual state of all templates which have presets with prebuilds configured.
|
// Reconcile will attempt to resolve the desired vs actual state of all templates which have presets with prebuilds configured.
|
||||||
//
|
//
|
||||||
// NOTE:
|
// NOTE:
|
||||||
//
|
//
|
||||||
@ -113,7 +113,7 @@ func (c *Controller) ReconcileTemplate(templateID *uuid.UUID) {
|
|||||||
// be reconciled again, leading to another workspace being provisioned. Two workspace builds will be occurring
|
// be reconciled again, leading to another workspace being provisioned. Two workspace builds will be occurring
|
||||||
// simultaneously for the same preset, but once both jobs have completed the reconciliation loop will notice the
|
// simultaneously for the same preset, but once both jobs have completed the reconciliation loop will notice the
|
||||||
// extraneous instance and delete it.
|
// extraneous instance and delete it.
|
||||||
func (c *Controller) reconcile(ctx context.Context, templateID *uuid.UUID) {
|
func (c *Controller) Reconcile(ctx context.Context, templateID *uuid.UUID) {
|
||||||
var logger slog.Logger
|
var logger slog.Logger
|
||||||
if templateID == nil {
|
if templateID == nil {
|
||||||
logger = c.logger.With(slog.F("reconcile_context", "all"))
|
logger = c.logger.With(slog.F("reconcile_context", "all"))
|
||||||
|
@ -1,27 +1,21 @@
|
|||||||
package prebuilds
|
package prebuilds_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"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/dbauthz"
|
|
||||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/provisioner/echo"
|
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
|
||||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +33,7 @@ func TestNoReconciliationActionsIfNoPresets(t *testing.T) {
|
|||||||
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
|
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
|
||||||
}
|
}
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
controller := NewController(db, pubsub, cfg, logger)
|
controller := prebuilds.NewController(db, pubsub, cfg, logger)
|
||||||
|
|
||||||
// given a template version with no presets
|
// given a template version with no presets
|
||||||
org := dbgen.Organization(t, db, database.Organization{})
|
org := dbgen.Organization(t, db, database.Organization{})
|
||||||
@ -59,7 +53,7 @@ func TestNoReconciliationActionsIfNoPresets(t *testing.T) {
|
|||||||
require.Equal(t, templateVersion, gotTemplateVersion)
|
require.Equal(t, templateVersion, gotTemplateVersion)
|
||||||
|
|
||||||
// when we trigger the reconciliation loop for all templates
|
// when we trigger the reconciliation loop for all templates
|
||||||
controller.reconcile(ctx, nil)
|
controller.Reconcile(ctx, nil)
|
||||||
|
|
||||||
// then no reconciliation actions are taken
|
// then no reconciliation actions are taken
|
||||||
// because without presets, there are no prebuilds
|
// because without presets, there are no prebuilds
|
||||||
@ -83,7 +77,7 @@ func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) {
|
|||||||
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
|
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
|
||||||
}
|
}
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
controller := NewController(db, pubsub, cfg, logger)
|
controller := prebuilds.NewController(db, pubsub, cfg, logger)
|
||||||
|
|
||||||
// given there are presets, but no prebuilds
|
// given there are presets, but no prebuilds
|
||||||
org := dbgen.Organization(t, db, database.Organization{})
|
org := dbgen.Organization(t, db, database.Organization{})
|
||||||
@ -115,7 +109,7 @@ func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) {
|
|||||||
require.NotEmpty(t, presetParameters)
|
require.NotEmpty(t, presetParameters)
|
||||||
|
|
||||||
// when we trigger the reconciliation loop for all templates
|
// when we trigger the reconciliation loop for all templates
|
||||||
controller.reconcile(ctx, nil)
|
controller.Reconcile(ctx, nil)
|
||||||
|
|
||||||
// then no reconciliation actions are taken
|
// then no reconciliation actions are taken
|
||||||
// because without prebuilds, there is nothing to reconcile
|
// because without prebuilds, there is nothing to reconcile
|
||||||
@ -223,11 +217,11 @@ func setupTestDBPrebuild(
|
|||||||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||||
TemplateID: templateID,
|
TemplateID: templateID,
|
||||||
OrganizationID: orgID,
|
OrganizationID: orgID,
|
||||||
OwnerID: OwnerID,
|
OwnerID: prebuilds.OwnerID,
|
||||||
Deleted: deleted,
|
Deleted: deleted,
|
||||||
})
|
})
|
||||||
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
||||||
InitiatorID: OwnerID,
|
InitiatorID: prebuilds.OwnerID,
|
||||||
CreatedAt: time.Now().Add(-2 * time.Hour),
|
CreatedAt: time.Now().Add(-2 * time.Hour),
|
||||||
CompletedAt: completedAt,
|
CompletedAt: completedAt,
|
||||||
CanceledAt: cancelledAt,
|
CanceledAt: cancelledAt,
|
||||||
@ -236,7 +230,7 @@ func setupTestDBPrebuild(
|
|||||||
})
|
})
|
||||||
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||||
WorkspaceID: workspace.ID,
|
WorkspaceID: workspace.ID,
|
||||||
InitiatorID: OwnerID,
|
InitiatorID: prebuilds.OwnerID,
|
||||||
TemplateVersionID: templateVersion.ID,
|
TemplateVersionID: templateVersion.ID,
|
||||||
JobID: job.ID,
|
JobID: job.ID,
|
||||||
TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true},
|
TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true},
|
||||||
@ -308,7 +302,7 @@ func TestActiveTemplateVersionPrebuilds(t *testing.T) {
|
|||||||
db, pubsub := dbtestutil.NewDB(t)
|
db, pubsub := dbtestutil.NewDB(t)
|
||||||
cfg := codersdk.PrebuildsConfig{}
|
cfg := codersdk.PrebuildsConfig{}
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
controller := NewController(db, pubsub, cfg, logger)
|
controller := prebuilds.NewController(db, pubsub, cfg, logger)
|
||||||
|
|
||||||
orgID, userID, templateID := setupTestDBTemplate(t, db)
|
orgID, userID, templateID := setupTestDBTemplate(t, db)
|
||||||
_, _, prebuildID := setupTestDBPrebuild(
|
_, _, prebuildID := setupTestDBPrebuild(
|
||||||
@ -322,7 +316,7 @@ func TestActiveTemplateVersionPrebuilds(t *testing.T) {
|
|||||||
templateID,
|
templateID,
|
||||||
)
|
)
|
||||||
|
|
||||||
controller.reconcile(ctx, nil)
|
controller.Reconcile(ctx, nil)
|
||||||
|
|
||||||
createdNewPrebuild := false
|
createdNewPrebuild := false
|
||||||
deletedOldPrebuild := true
|
deletedOldPrebuild := true
|
||||||
@ -347,314 +341,35 @@ func TestInactiveTemplateVersionPrebuilds(t *testing.T) {
|
|||||||
// Scenario: Prebuilds are never created and always deleted if the template version is inactive
|
// Scenario: Prebuilds are never created and always deleted if the template version is inactive
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
t.Skip("todo")
|
t.Skip("todo")
|
||||||
}
|
|
||||||
|
|
||||||
type partiallyMockedDB struct {
|
|
||||||
mock.Mock
|
|
||||||
database.Store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *partiallyMockedDB) ClaimPrebuild(ctx context.Context, arg database.ClaimPrebuildParams) (database.ClaimPrebuildRow, error) {
|
|
||||||
args := m.Mock.Called(ctx, arg)
|
|
||||||
return args.Get(0).(database.ClaimPrebuildRow), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClaimPrebuild(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
db, pubsub := dbtestutil.NewDB(t)
|
db, pubsub := dbtestutil.NewDB(t)
|
||||||
mockedDB := &partiallyMockedDB{
|
|
||||||
Store: db,
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
|
||||||
IncludeProvisionerDaemon: true,
|
|
||||||
Database: mockedDB,
|
|
||||||
Pubsub: pubsub,
|
|
||||||
})
|
|
||||||
|
|
||||||
cfg := codersdk.PrebuildsConfig{}
|
cfg := codersdk.PrebuildsConfig{}
|
||||||
logger := testutil.Logger(t)
|
logger := testutil.Logger(t)
|
||||||
controller := NewController(mockedDB, pubsub, cfg, logger)
|
controller := prebuilds.NewController(db, pubsub, cfg, logger)
|
||||||
|
|
||||||
const (
|
// when does a prebuild get deleted?
|
||||||
desiredInstances = 1
|
// * when it is in some way permanently ineligible to be claimed
|
||||||
presetCount = 2
|
// * this could be because the build failed or was canceled
|
||||||
)
|
// * or it belongs to a template version that is no longer active
|
||||||
|
// * or it belongs to a template version that is deprecated
|
||||||
|
// * when there are more prebuilds than the preset desires
|
||||||
|
// * someone could have manually created a workspace for the prebuild user
|
||||||
|
// * any workspaces that were created for the prebuilds user and don't match a preset should be deleted - deferred
|
||||||
|
|
||||||
// Setup. // TODO: abstract?
|
// given a preset that desires 2 prebuilds
|
||||||
owner := coderdtest.CreateFirstUser(t, client)
|
// and there are 3 running prebuilds for the preset
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(desiredInstances))
|
// and there are 4 non-running prebuilds for the preset
|
||||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
// * one is not running because its latest build was a stop transition
|
||||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
// * another is not running because its latest build was a delete transition
|
||||||
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
// * a third is not running because its latest build was a start transition but the build failed
|
||||||
require.NoError(t, err)
|
// * a fourth is not running because its latest build was a start transition but the build was canceled
|
||||||
require.Len(t, presets, presetCount)
|
// when we trigger the reconciliation loop for all templates
|
||||||
|
controller.Reconcile(ctx, nil)
|
||||||
|
// then the four non running prebuilds are deleted
|
||||||
//
|
// and 1 of the running prebuilds is deleted
|
||||||
//
|
// because stopped, deleted and failed builds are not considered running in terms of the definition of "running" above.
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// TODO: for Monday: need to get this feature entitled so the EnterpriseClaimer is used, otherwise it's a noop.
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
|
|
||||||
api.Entitlements.Modify(func(entitlements *codersdk.Entitlements) {
|
|
||||||
entitlements.Features[codersdk.FeatureWorkspacePrebuilds] = codersdk.Feature{
|
|
||||||
Enabled: true,
|
|
||||||
Entitlement: codersdk.EntitlementEntitled,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// TODO: can't use coderd.PubsubEventLicenses const because of an import cycle.
|
|
||||||
require.NoError(t, api.Pubsub.Publish("licenses", []byte("add")))
|
|
||||||
|
|
||||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
|
||||||
|
|
||||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
|
||||||
|
|
||||||
//claimer := EnterpriseClaimer{}
|
|
||||||
//prebuildsUser, err := db.GetUserByID(ctx, claimer.Initiator())
|
|
||||||
//require.NoError(t, err)
|
|
||||||
|
|
||||||
controller.reconcile(ctx, nil)
|
|
||||||
|
|
||||||
runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuildsRow, desiredInstances*presetCount)
|
|
||||||
require.Eventually(t, func() bool {
|
|
||||||
rows, err := mockedDB.GetRunningPrebuilds(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("found %d running prebuilds so far", len(rows))
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
runningPrebuilds[row.CurrentPresetID.UUID] = row
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(runningPrebuilds) == (desiredInstances * presetCount)
|
|
||||||
}, testutil.WaitSuperLong, testutil.IntervalSlow)
|
|
||||||
|
|
||||||
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
|
|
||||||
|
|
||||||
params := database.ClaimPrebuildParams{
|
|
||||||
NewUserID: user.ID,
|
|
||||||
NewName: workspaceName,
|
|
||||||
PresetID: presets[0].ID,
|
|
||||||
}
|
|
||||||
mockedDB.On("ClaimPrebuild", mock.Anything, params).Return(db.ClaimPrebuild(ctx, params)).Once()
|
|
||||||
|
|
||||||
// When: a user creates a new workspace with a preset for which prebuilds are configured.
|
|
||||||
userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{
|
|
||||||
TemplateVersionID: version.ID,
|
|
||||||
Name: workspaceName,
|
|
||||||
TemplateVersionPresetID: presets[0].ID,
|
|
||||||
ClaimPrebuildIfAvailable: true, // TODO: doesn't do anything yet; it probably should though.
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, userWorkspace.LatestBuild.ID)
|
|
||||||
|
|
||||||
require.True(t, mockedDB.AssertCalled(t, "ClaimPrebuild", ctx, params))
|
|
||||||
|
|
||||||
for _, rp := range runningPrebuilds {
|
|
||||||
t.Logf("prev >>%s", rp.WorkspaceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
pb, err := mockedDB.GetRunningPrebuilds(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
for _, rp := range pb {
|
|
||||||
t.Logf("new >>%s", rp.WorkspaceName)
|
|
||||||
}
|
|
||||||
require.Len(t, pb, 4)
|
|
||||||
|
|
||||||
//var prebuildIDs []uuid.UUID
|
|
||||||
//// Given: two running prebuilds.
|
|
||||||
//for i := 0; i < 2; i++ {
|
|
||||||
// prebuiltWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
||||||
// TemplateID: template.ID,
|
|
||||||
// OrganizationID: owner.OrganizationID,
|
|
||||||
// OwnerID: prebuildsUser.ID,
|
|
||||||
// })
|
|
||||||
// prebuildIDs = append(prebuildIDs, prebuiltWorkspace.ID)
|
|
||||||
//
|
|
||||||
// job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
||||||
// InitiatorID: OwnerID,
|
|
||||||
// CreatedAt: time.Now().Add(-2 * time.Hour),
|
|
||||||
// CompletedAt: sql.NullTime{Time: time.Now().Add(-time.Hour), Valid: true},
|
|
||||||
// OrganizationID: owner.OrganizationID,
|
|
||||||
// Provisioner: database.ProvisionerTypeEcho,
|
|
||||||
// Type: database.ProvisionerJobTypeWorkspaceBuild,
|
|
||||||
// })
|
|
||||||
// dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
||||||
// WorkspaceID: prebuiltWorkspace.ID,
|
|
||||||
// InitiatorID: OwnerID,
|
|
||||||
// TemplateVersionID: version.ID,
|
|
||||||
// JobID: job.ID,
|
|
||||||
// TemplateVersionPresetID: uuid.NullUUID{UUID: presets[0].ID, Valid: true},
|
|
||||||
// Transition: database.WorkspaceTransitionStart,
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// // Setup workspace agent which is in a given state. // TODO: table test with unclaimable when !ready
|
|
||||||
// resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
||||||
// ID: uuid.New(),
|
|
||||||
// CreatedAt: time.Now().Add(-1 * time.Hour),
|
|
||||||
// JobID: job.ID,
|
|
||||||
// Transition: database.WorkspaceTransitionStart,
|
|
||||||
// Type: "some_compute_resource",
|
|
||||||
// Name: "beep_boop",
|
|
||||||
// })
|
|
||||||
// agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
||||||
// ID: uuid.New(),
|
|
||||||
// CreatedAt: time.Now().Add(-1 * time.Hour),
|
|
||||||
// Name: "main",
|
|
||||||
// FirstConnectedAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
|
||||||
// LastConnectedAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
|
||||||
// ResourceID: resource.ID,
|
|
||||||
// })
|
|
||||||
// require.NoError(t, db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
|
|
||||||
// ID: agent.ID,
|
|
||||||
// LifecycleState: database.WorkspaceAgentLifecycleStateReady,
|
|
||||||
// StartedAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
|
||||||
// ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
|
||||||
// }))
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
////Then: validate that these prebuilds are indeed considered running.
|
|
||||||
//running, err := db.GetRunningPrebuilds(ctx)
|
|
||||||
//require.NoError(t, err)
|
|
||||||
//var (
|
|
||||||
// found []uuid.UUID
|
|
||||||
// ready int
|
|
||||||
//)
|
|
||||||
//for _, w := range running {
|
|
||||||
// found = append(found, w.WorkspaceID)
|
|
||||||
// if w.Ready {
|
|
||||||
// ready++
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//require.ElementsMatch(t, prebuildIDs, found)
|
|
||||||
//require.EqualValues(t, len(prebuildIDs), ready)
|
|
||||||
|
|
||||||
//// When: a user creates a new workspace with a preset for which prebuilds are configured.
|
|
||||||
//userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{
|
|
||||||
// TemplateVersionID: templateVersion.ID,
|
|
||||||
// Name: strings.ReplaceAll(testutil.GetRandomName(t), "_", "-"),
|
|
||||||
// TemplateVersionPresetID: preset.ID,
|
|
||||||
// ClaimPrebuildIfAvailable: true, // TODO: doesn't do anything yet; it probably should though.
|
|
||||||
//})
|
|
||||||
//require.NoError(t, err)
|
|
||||||
//require.NoError(t, cliui.WorkspaceBuild(ctx, os.Stderr, userClient, userWorkspace.LatestBuild.ID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//func addPremiumLicense(t *testing.T) (*codersdk.Entitlements, error) {
|
|
||||||
// premiumLicense := (&coderdenttest.LicenseOptions{
|
|
||||||
// AccountType: "salesforce",
|
|
||||||
// AccountID: "Charlie",
|
|
||||||
// DeploymentIDs: nil,
|
|
||||||
// Trial: false,
|
|
||||||
// FeatureSet: codersdk.FeatureSetPremium,
|
|
||||||
// AllFeatures: true,
|
|
||||||
// }).Valid(time.Now())
|
|
||||||
// licenses := []*coderdenttest.LicenseOptions{premiumLicense}
|
|
||||||
//
|
|
||||||
// allEnablements := make(map[codersdk.FeatureName]bool, len(codersdk.FeatureNames))
|
|
||||||
// for _, e := range codersdk.FeatureNames {
|
|
||||||
// allEnablements[e] = true
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// generatedLicenses := make([]database.License, 0, len(licenses))
|
|
||||||
// for i, lo := range licenses {
|
|
||||||
// generatedLicenses = append(generatedLicenses, database.License{
|
|
||||||
// ID: int32(i),
|
|
||||||
// UploadedAt: time.Now().Add(time.Hour * -1),
|
|
||||||
// JWT: lo.Generate(t),
|
|
||||||
// Exp: lo.GraceAt,
|
|
||||||
// UUID: uuid.New(),
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// ents, err := license.LicensesEntitlements(time.Now(), generatedLicenses, allEnablements, coderdenttest.Keys, license.FeatureArguments{})
|
|
||||||
// return &ents, err
|
|
||||||
//}
|
|
||||||
|
|
||||||
func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
|
|
||||||
return &echo.Responses{
|
|
||||||
Parse: echo.ParseComplete,
|
|
||||||
ProvisionPlan: []*proto.Response{
|
|
||||||
{
|
|
||||||
Type: &proto.Response_Plan{
|
|
||||||
Plan: &proto.PlanComplete{
|
|
||||||
Resources: []*proto.Resource{
|
|
||||||
{
|
|
||||||
Type: "compute",
|
|
||||||
Name: "main",
|
|
||||||
Agents: []*proto.Agent{
|
|
||||||
{
|
|
||||||
Name: "smith",
|
|
||||||
OperatingSystem: "linux",
|
|
||||||
Architecture: "i386",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Presets: []*proto.Preset{
|
|
||||||
{
|
|
||||||
Name: "preset-a",
|
|
||||||
Parameters: []*proto.PresetParameter{
|
|
||||||
{
|
|
||||||
Name: "k1",
|
|
||||||
Value: "v1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Prebuild: &proto.Prebuild{
|
|
||||||
Instances: desiredInstances,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "preset-b",
|
|
||||||
Parameters: []*proto.PresetParameter{
|
|
||||||
{
|
|
||||||
Name: "k1",
|
|
||||||
Value: "v2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Prebuild: &proto.Prebuild{
|
|
||||||
Instances: desiredInstances,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ProvisionApply: []*proto.Response{
|
|
||||||
{
|
|
||||||
Type: &proto.Response_Apply{
|
|
||||||
Apply: &proto.ApplyComplete{
|
|
||||||
Resources: []*proto.Resource{
|
|
||||||
{
|
|
||||||
Type: "compute",
|
|
||||||
Name: "main",
|
|
||||||
Agents: []*proto.Agent{
|
|
||||||
{
|
|
||||||
Name: "smith",
|
|
||||||
OperatingSystem: "linux",
|
|
||||||
Architecture: "i386",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(dannyk): test that prebuilds are only attempted to be claimed for net-new workspace builds
|
|
||||||
// TODO (sasswart): test idempotency of reconciliation
|
// TODO (sasswart): test idempotency of reconciliation
|
||||||
// TODO (sasswart): test mutual exclusion
|
// TODO (sasswart): test mutual exclusion
|
||||||
|
Reference in New Issue
Block a user