Skip to content

Commit 6a14a8b

Browse files
test: improve test coverage for hard-limited presets
1 parent 2667684 commit 6a14a8b

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed

enterprise/coderd/prebuilds/reconcile_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,213 @@ func TestSkippingHardLimitedPresets(t *testing.T) {
815815
}
816816
}
817817

818+
func TestHardLimitedPresetShouldNotBlockDeletion(t *testing.T) {
819+
t.Parallel()
820+
821+
if !dbtestutil.WillUsePostgres() {
822+
t.Skip("This test requires postgres")
823+
}
824+
825+
// Test cases verify the behavior of prebuild creation depending on configured failure limits.
826+
testCases := []struct {
827+
name string
828+
hardLimit int64
829+
isHardLimitHit bool
830+
}{
831+
{
832+
name: "hard limit is hit - skip creation of prebuilt workspace",
833+
hardLimit: 1,
834+
isHardLimitHit: true,
835+
},
836+
}
837+
838+
for _, tc := range testCases {
839+
t.Run(tc.name, func(t *testing.T) {
840+
t.Parallel()
841+
842+
clock := quartz.NewMock(t)
843+
ctx := testutil.Context(t, testutil.WaitShort)
844+
cfg := codersdk.PrebuildsConfig{
845+
FailureHardLimit: serpent.Int64(tc.hardLimit),
846+
ReconciliationBackoffInterval: 0,
847+
}
848+
logger := slogtest.Make(
849+
t, &slogtest.Options{IgnoreErrors: true},
850+
).Leveled(slog.LevelDebug)
851+
db, pubSub := dbtestutil.NewDB(t)
852+
fakeEnqueuer := newFakeEnqueuer()
853+
registry := prometheus.NewRegistry()
854+
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, registry, fakeEnqueuer)
855+
856+
// Template admin to receive a notification.
857+
templateAdmin := dbgen.User(t, db, database.User{
858+
RBACRoles: []string{codersdk.RoleTemplateAdmin},
859+
})
860+
861+
// Set up test environment with a template, version, and preset.
862+
ownerID := uuid.New()
863+
dbgen.User(t, db, database.User{
864+
ID: ownerID,
865+
})
866+
org, template := setupTestDBTemplate(t, db, ownerID, false)
867+
templateVersionID := setupTestDBTemplateVersion(ctx, t, clock, db, pubSub, org.ID, ownerID, template.ID)
868+
preset := setupTestDBPreset(t, db, templateVersionID, 2, uuid.New().String())
869+
870+
// Create a successful prebuilt workspace.
871+
successfulWorkspace, _ := setupTestDBPrebuild(
872+
t,
873+
clock,
874+
db,
875+
pubSub,
876+
database.WorkspaceTransitionStart,
877+
database.ProvisionerJobStatusSucceeded,
878+
org.ID,
879+
preset,
880+
template.ID,
881+
templateVersionID,
882+
)
883+
884+
// Make sure that prebuilt workspaces created in such order: [successful, failed].
885+
clock.Advance(time.Second).MustWait(ctx)
886+
887+
// Create a failed prebuilt workspace that counts toward the hard failure limit.
888+
setupTestDBPrebuild(
889+
t,
890+
clock,
891+
db,
892+
pubSub,
893+
database.WorkspaceTransitionStart,
894+
database.ProvisionerJobStatusFailed,
895+
org.ID,
896+
preset,
897+
template.ID,
898+
templateVersionID,
899+
)
900+
901+
getJobStatusMap := func(workspaces []database.WorkspaceTable) map[database.ProvisionerJobStatus]int {
902+
jobStatusMap := make(map[database.ProvisionerJobStatus]int)
903+
for _, workspace := range workspaces {
904+
workspaceBuilds, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
905+
WorkspaceID: workspace.ID,
906+
})
907+
require.NoError(t, err)
908+
909+
for _, workspaceBuild := range workspaceBuilds {
910+
job, err := db.GetProvisionerJobByID(ctx, workspaceBuild.JobID)
911+
require.NoError(t, err)
912+
jobStatusMap[job.JobStatus]++
913+
}
914+
}
915+
return jobStatusMap
916+
}
917+
918+
// Verify initial state: two workspaces exist, one successful, one failed.
919+
workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID)
920+
require.NoError(t, err)
921+
require.Equal(t, 2, len(workspaces))
922+
jobStatusMap := getJobStatusMap(workspaces)
923+
require.Len(t, jobStatusMap, 2)
924+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusSucceeded])
925+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusFailed])
926+
927+
//Verify initial state: metric is not set - meaning preset is not hard limited.
928+
require.NoError(t, controller.ForceMetricsUpdate(ctx))
929+
mf, err := registry.Gather()
930+
require.NoError(t, err)
931+
metric := findMetric(mf, prebuilds.MetricPresetHardLimitedGauge, map[string]string{
932+
"template_name": template.Name,
933+
"preset_name": preset.Name,
934+
"org_name": org.Name,
935+
})
936+
require.Nil(t, metric)
937+
938+
// We simulate a failed prebuild in the test; Consequently, the backoff mechanism is triggered when ReconcileAll is called.
939+
// Even though ReconciliationBackoffInterval is set to zero, we still need to advance the clock by at least one nanosecond.
940+
clock.Advance(time.Nanosecond).MustWait(ctx)
941+
942+
// Trigger reconciliation to attempt creating a new prebuild.
943+
// The outcome depends on whether the hard limit has been reached.
944+
require.NoError(t, controller.ReconcileAll(ctx))
945+
946+
// These two additional calls to ReconcileAll should not trigger any notifications.
947+
// A notification is only sent once.
948+
require.NoError(t, controller.ReconcileAll(ctx))
949+
require.NoError(t, controller.ReconcileAll(ctx))
950+
951+
// Verify the final state after reconciliation.
952+
// When hard limit is reached, no new workspace should be created.
953+
workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID)
954+
require.NoError(t, err)
955+
require.Equal(t, 2, len(workspaces))
956+
jobStatusMap = getJobStatusMap(workspaces)
957+
require.Len(t, jobStatusMap, 2)
958+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusSucceeded])
959+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusFailed])
960+
961+
updatedPreset, err := db.GetPresetByID(ctx, preset.ID)
962+
require.NoError(t, err)
963+
require.Equal(t, database.PrebuildStatusHardLimited, updatedPreset.PrebuildStatus)
964+
965+
// When hard limit is reached, a notification should be sent.
966+
matching := fakeEnqueuer.Sent(func(notification *notificationstest.FakeNotification) bool {
967+
if !assert.Equal(t, notifications.PrebuildFailureLimitReached, notification.TemplateID, "unexpected template") {
968+
return false
969+
}
970+
971+
if !assert.Equal(t, templateAdmin.ID, notification.UserID, "unexpected receiver") {
972+
return false
973+
}
974+
975+
return true
976+
})
977+
require.Len(t, matching, 1)
978+
979+
// When hard limit is reached, metric is set to 1.
980+
mf, err = registry.Gather()
981+
require.NoError(t, err)
982+
metric = findMetric(mf, prebuilds.MetricPresetHardLimitedGauge, map[string]string{
983+
"template_name": template.Name,
984+
"preset_name": preset.Name,
985+
"org_name": org.Name,
986+
})
987+
require.NotNil(t, metric)
988+
require.NotNil(t, metric.GetGauge())
989+
require.EqualValues(t, 1, metric.GetGauge().GetValue())
990+
991+
// When: the template is deleted.
992+
require.NoError(t, db.UpdateTemplateDeletedByID(ctx, database.UpdateTemplateDeletedByIDParams{
993+
ID: template.ID,
994+
Deleted: true,
995+
UpdatedAt: dbtime.Now(),
996+
}))
997+
998+
// Trigger reconciliation to make sure that successful, but outdated prebuilt workspace will be deleted.
999+
require.NoError(t, controller.ReconcileAll(ctx))
1000+
1001+
workspaces, err = db.GetWorkspacesByTemplateID(ctx, template.ID)
1002+
require.NoError(t, err)
1003+
require.Equal(t, 2, len(workspaces))
1004+
1005+
jobStatusMap = getJobStatusMap(workspaces)
1006+
require.Len(t, jobStatusMap, 3)
1007+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusSucceeded])
1008+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusFailed])
1009+
// Pending job should be the job that deletes successful, but outdated prebuilt workspace.
1010+
// Prebuilt workspace MUST be deleted, despite the fact that preset is marked as hard limited.
1011+
require.Equal(t, 1, jobStatusMap[database.ProvisionerJobStatusPending])
1012+
1013+
workspaceBuilds, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{
1014+
WorkspaceID: successfulWorkspace.ID,
1015+
})
1016+
require.NoError(t, err)
1017+
require.Equal(t, 2, len(workspaceBuilds))
1018+
// Make sure that successfully created, but outdated prebuilt workspace was scheduled for deletion.
1019+
require.Equal(t, database.WorkspaceTransitionDelete, workspaceBuilds[0].Transition)
1020+
require.Equal(t, database.WorkspaceTransitionStart, workspaceBuilds[1].Transition)
1021+
})
1022+
}
1023+
}
1024+
8181025
func TestRunLoop(t *testing.T) {
8191026
t.Parallel()
8201027

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