Skip to content

Commit 2aed738

Browse files
committed
Improvements & tests
Signed-off-by: Danny Kopping <dannykopping@gmail.com>
1 parent 72b6c42 commit 2aed738

File tree

9 files changed

+135
-59
lines changed

9 files changed

+135
-59
lines changed

cli/testdata/server-config.yaml.golden

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -693,10 +693,11 @@ workspace_prebuilds:
693693
# How often to reconcile workspace prebuilds state.
694694
# (default: 15s, type: duration)
695695
reconciliation_interval: 15s
696-
# Interval to increase reconciliation backoff by when unrecoverable errors occur.
696+
# Interval to increase reconciliation backoff by when prebuilds fail, after which
697+
# a retry attempt is made.
697698
# (default: 15s, type: duration)
698699
reconciliation_backoff_interval: 15s
699-
# Interval to look back to determine number of failed builds, which influences
700+
# Interval to look back to determine number of failed prebuilds, which influences
700701
# backoff.
701702
# (default: 1h0m0s, type: duration)
702703
reconciliation_backoff_lookback_period: 1h0m0s

coderd/coderd.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ func New(options *Options) *API {
597597
api.AppearanceFetcher.Store(&f)
598598
api.PortSharer.Store(&portsharing.DefaultPortSharer)
599599
api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer)
600+
api.PrebuildsReconciler.Store(&prebuilds.DefaultReconciler)
600601
buildInfo := codersdk.BuildInfoResponse{
601602
ExternalURL: buildinfo.ExternalURL(),
602603
Version: buildinfo.Version(),
@@ -1568,10 +1569,11 @@ type API struct {
15681569
DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
15691570
// AccessControlStore is a pointer to an atomic pointer since it is
15701571
// passed to dbauthz.
1571-
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
1572-
PortSharer atomic.Pointer[portsharing.PortSharer]
1573-
FileCache files.Cache
1574-
PrebuildsClaimer atomic.Pointer[prebuilds.Claimer]
1572+
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
1573+
PortSharer atomic.Pointer[portsharing.PortSharer]
1574+
FileCache files.Cache
1575+
PrebuildsClaimer atomic.Pointer[prebuilds.Claimer]
1576+
PrebuildsReconciler atomic.Pointer[prebuilds.ReconciliationOrchestrator]
15751577

15761578
UpdatesProvider tailnet.WorkspaceUpdatesProvider
15771579

@@ -1659,6 +1661,13 @@ func (api *API) Close() error {
16591661
_ = api.AppSigningKeyCache.Close()
16601662
_ = api.AppEncryptionKeyCache.Close()
16611663
_ = api.UpdatesProvider.Close()
1664+
1665+
if current := api.PrebuildsReconciler.Load(); current != nil {
1666+
ctx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop"))
1667+
defer giveUp()
1668+
(*current).Stop(ctx, nil)
1669+
}
1670+
16621671
return nil
16631672
}
16641673

coderd/prebuilds/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package prebuilds
22

33
import (
44
"context"
5-
"errors"
65

76
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
88

99
"github.com/coder/coder/v2/coderd/database"
1010
)
1111

12-
var ErrNoClaimablePrebuiltWorkspaces = errors.New("no claimable prebuilt workspaces found")
12+
var ErrNoClaimablePrebuiltWorkspaces = xerrors.New("no claimable prebuilt workspaces found")
1313

1414
// ReconciliationOrchestrator manages the lifecycle of prebuild reconciliation.
1515
// It runs a continuous loop to check and reconcile prebuild states, and can be stopped gracefully.

coderd/prebuilds/noop.go

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,25 @@ import (
1010

1111
type NoopReconciler struct{}
1212

13-
func NewNoopReconciler() *NoopReconciler {
14-
return &NoopReconciler{}
15-
}
16-
17-
func (NoopReconciler) RunLoop(context.Context) {}
18-
19-
func (NoopReconciler) Stop(context.Context, error) {}
20-
21-
func (NoopReconciler) ReconcileAll(context.Context) error {
22-
return nil
23-
}
24-
13+
func (NoopReconciler) RunLoop(context.Context) {}
14+
func (NoopReconciler) Stop(context.Context, error) {}
15+
func (NoopReconciler) ReconcileAll(context.Context) error { return nil }
2516
func (NoopReconciler) SnapshotState(context.Context, database.Store) (*GlobalSnapshot, error) {
2617
return &GlobalSnapshot{}, nil
2718
}
28-
29-
func (NoopReconciler) ReconcilePreset(context.Context, PresetSnapshot) error {
30-
return nil
31-
}
32-
19+
func (NoopReconciler) ReconcilePreset(context.Context, PresetSnapshot) error { return nil }
3320
func (NoopReconciler) CalculateActions(context.Context, PresetSnapshot) (*ReconciliationActions, error) {
3421
return &ReconciliationActions{}, nil
3522
}
3623

37-
var _ ReconciliationOrchestrator = NoopReconciler{}
24+
var DefaultReconciler ReconciliationOrchestrator = NoopReconciler{}
3825

39-
type AGPLPrebuildClaimer struct{}
26+
type NoopClaimer struct{}
4027

41-
func (c AGPLPrebuildClaimer) Claim(context.Context, database.Store, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
28+
func (NoopClaimer) Claim(context.Context, database.Store, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
4229
// Not entitled to claim prebuilds in AGPL version.
4330
return nil, ErrNoClaimablePrebuiltWorkspaces
4431
}
32+
func (NoopClaimer) Initiator() uuid.UUID { return uuid.Nil }
4533

46-
func (c AGPLPrebuildClaimer) Initiator() uuid.UUID {
47-
return uuid.Nil
48-
}
49-
50-
var DefaultClaimer Claimer = AGPLPrebuildClaimer{}
34+
var DefaultClaimer Claimer = NoopClaimer{}

codersdk/deployment.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,8 @@ type DeploymentValues struct {
396396
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
397397
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
398398
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
399-
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`
400399
WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"`
400+
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`
401401

402402
Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
403403
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
@@ -3038,6 +3038,9 @@ Write out the current server config as YAML to stdout.`,
30383038
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
30393039
Hidden: true, // Hidden because most operators should not need to modify this.
30403040
},
3041+
// Push notifications.
3042+
3043+
// Workspace Prebuilds Options
30413044
{
30423045
Name: "Reconciliation Interval",
30433046
Description: "How often to reconcile workspace prebuilds state.",
@@ -3051,7 +3054,7 @@ Write out the current server config as YAML to stdout.`,
30513054
},
30523055
{
30533056
Name: "Reconciliation Backoff Interval",
3054-
Description: "Interval to increase reconciliation backoff by when unrecoverable errors occur.",
3057+
Description: "Interval to increase reconciliation backoff by when prebuilds fail, after which a retry attempt is made.",
30553058
Flag: "workspace-prebuilds-reconciliation-backoff-interval",
30563059
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_INTERVAL",
30573060
Value: &c.Prebuilds.ReconciliationBackoffInterval,
@@ -3063,7 +3066,7 @@ Write out the current server config as YAML to stdout.`,
30633066
},
30643067
{
30653068
Name: "Reconciliation Backoff Lookback Period",
3066-
Description: "Interval to look back to determine number of failed builds, which influences backoff.",
3069+
Description: "Interval to look back to determine number of failed prebuilds, which influences backoff.",
30673070
Flag: "workspace-prebuilds-reconciliation-backoff-lookback-period",
30683071
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_LOOKBACK_PERIOD",
30693072
Value: &c.Prebuilds.ReconciliationBackoffLookback,
@@ -3073,7 +3076,6 @@ Write out the current server config as YAML to stdout.`,
30733076
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
30743077
Hidden: true,
30753078
},
3076-
// Push notifications.
30773079
}
30783080

30793081
return opts
@@ -3298,9 +3300,9 @@ const (
32983300
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
32993301
ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events.
33003302
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
3301-
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
33023303
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
33033304
ExperimentDynamicParameters Experiment = "dynamic-parameters" // Enables dynamic parameters when creating a workspace.
3305+
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
33043306
)
33053307

33063308
// ExperimentsSafe should include all experiments that are safe for

enterprise/coderd/coderd.go

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,6 @@ type API struct {
632632

633633
licenseMetricsCollector *license.MetricsCollector
634634
tailnetService *tailnet.ClientService
635-
636-
PrebuildsReconciler agplprebuilds.ReconciliationOrchestrator
637635
}
638636

639637
// writeEntitlementWarningsHeader writes the entitlement warnings to the response header
@@ -665,12 +663,6 @@ func (api *API) Close() error {
665663
api.Options.CheckInactiveUsersCancelFunc()
666664
}
667665

668-
if api.PrebuildsReconciler != nil {
669-
ctx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop"))
670-
defer giveUp()
671-
api.PrebuildsReconciler.Stop(ctx, xerrors.New("api closed")) // TODO: determine root cause (requires changes up the stack, though).
672-
}
673-
674666
return api.AGPL.Close()
675667
}
676668

@@ -873,15 +865,15 @@ func (api *API) updateEntitlements(ctx context.Context) error {
873865
api.AGPL.PortSharer.Store(&ps)
874866
}
875867

876-
if initial, changed, enabled := featureChanged(codersdk.FeatureWorkspacePrebuilds); shouldUpdate(initial, changed, enabled) || api.PrebuildsReconciler == nil {
868+
if initial, changed, enabled := featureChanged(codersdk.FeatureWorkspacePrebuilds); shouldUpdate(initial, changed, enabled) {
877869
reconciler, claimer := api.setupPrebuilds(enabled)
878-
if api.PrebuildsReconciler != nil {
870+
if current := api.AGPL.PrebuildsReconciler.Load(); current != nil {
879871
stopCtx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop"))
880872
defer giveUp()
881-
api.PrebuildsReconciler.Stop(stopCtx, xerrors.New("entitlements change"))
873+
(*current).Stop(stopCtx, xerrors.New("entitlements change"))
882874
}
883875

884-
api.PrebuildsReconciler = reconciler
876+
api.AGPL.PrebuildsReconciler.Store(&reconciler)
885877
go reconciler.RunLoop(context.Background())
886878

887879
api.AGPL.PrebuildsClaimer.Store(&claimer)
@@ -1156,17 +1148,17 @@ func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Obj
11561148
return api.AGPL.HTTPAuth.Authorize(r, action, object)
11571149
}
11581150

1159-
func (api *API) setupPrebuilds(entitled bool) (agplprebuilds.ReconciliationOrchestrator, agplprebuilds.Claimer) {
1160-
enabled := api.AGPL.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds)
1161-
if !enabled || !entitled {
1151+
// nolint:revive // featureEnabled is a legit control flag.
1152+
func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.ReconciliationOrchestrator, agplprebuilds.Claimer) {
1153+
experimentEnabled := api.AGPL.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds)
1154+
if !experimentEnabled || !featureEnabled {
11621155
api.Logger.Debug(context.Background(), "prebuilds not enabled",
1163-
slog.F("experiment_enabled", enabled), slog.F("entitled", entitled))
1156+
slog.F("experiment_enabled", experimentEnabled), slog.F("feature_enabled", featureEnabled))
11641157

1165-
return agplprebuilds.NewNoopReconciler(), agplprebuilds.DefaultClaimer
1158+
return agplprebuilds.DefaultReconciler, agplprebuilds.DefaultClaimer
11661159
}
11671160

11681161
reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.DeploymentValues.Prebuilds,
11691162
api.Logger.Named("prebuilds"), quartz.NewReal())
1170-
11711163
return reconciler, prebuilds.EnterpriseClaimer{}
11721164
}

enterprise/coderd/coderd_test.go

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,15 @@ import (
2828
"github.com/coder/coder/v2/agent"
2929
"github.com/coder/coder/v2/agent/agenttest"
3030
"github.com/coder/coder/v2/coderd/httpapi"
31+
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
3132
"github.com/coder/coder/v2/coderd/rbac/policy"
3233
"github.com/coder/coder/v2/coderd/util/ptr"
34+
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
3335
"github.com/coder/coder/v2/tailnet/tailnettest"
3436

37+
"github.com/coder/retry"
38+
"github.com/coder/serpent"
39+
3540
agplaudit "github.com/coder/coder/v2/coderd/audit"
3641
"github.com/coder/coder/v2/coderd/coderdtest"
3742
"github.com/coder/coder/v2/coderd/database"
@@ -50,8 +55,6 @@ import (
5055
"github.com/coder/coder/v2/enterprise/dbcrypt"
5156
"github.com/coder/coder/v2/enterprise/replicasync"
5257
"github.com/coder/coder/v2/testutil"
53-
"github.com/coder/retry"
54-
"github.com/coder/serpent"
5558
)
5659

5760
func TestMain(m *testing.M) {
@@ -253,6 +256,90 @@ func TestEntitlements_HeaderWarnings(t *testing.T) {
253256
})
254257
}
255258

259+
func TestEntitlements_Prebuilds(t *testing.T) {
260+
t.Parallel()
261+
262+
cases := []struct {
263+
name string
264+
experimentEnabled bool
265+
featureEnabled bool
266+
expectedEnabled bool
267+
}{
268+
{
269+
name: "Fully enabled",
270+
featureEnabled: true,
271+
experimentEnabled: true,
272+
expectedEnabled: true,
273+
},
274+
{
275+
name: "Feature disabled",
276+
featureEnabled: false,
277+
experimentEnabled: true,
278+
expectedEnabled: false,
279+
},
280+
{
281+
name: "Experiment disabled",
282+
featureEnabled: true,
283+
experimentEnabled: false,
284+
expectedEnabled: false,
285+
},
286+
{
287+
name: "Fully disabled",
288+
featureEnabled: false,
289+
experimentEnabled: false,
290+
expectedEnabled: false,
291+
},
292+
}
293+
294+
for _, tc := range cases {
295+
tc := tc
296+
297+
t.Run(tc.name, func(t *testing.T) {
298+
t.Parallel()
299+
300+
var prebuildsEntitled int64
301+
if tc.featureEnabled {
302+
prebuildsEntitled = 1
303+
}
304+
305+
_, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
306+
Options: &coderdtest.Options{
307+
DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) {
308+
if tc.experimentEnabled {
309+
values.Experiments = serpent.StringArray{string(codersdk.ExperimentWorkspacePrebuilds)}
310+
}
311+
}),
312+
},
313+
314+
EntitlementsUpdateInterval: time.Second,
315+
LicenseOptions: &coderdenttest.LicenseOptions{
316+
Features: license.Features{
317+
codersdk.FeatureWorkspacePrebuilds: prebuildsEntitled,
318+
},
319+
},
320+
})
321+
322+
// The entitlements will need to refresh before the reconciler is set.
323+
require.Eventually(t, func() bool {
324+
return api.AGPL.PrebuildsReconciler.Load() != nil
325+
}, testutil.WaitSuperLong, testutil.IntervalFast)
326+
327+
reconciler := api.AGPL.PrebuildsReconciler.Load()
328+
claimer := api.AGPL.PrebuildsClaimer.Load()
329+
require.NotNil(t, reconciler)
330+
require.NotNil(t, claimer)
331+
332+
if tc.expectedEnabled {
333+
require.IsType(t, &prebuilds.StoreReconciler{}, *reconciler)
334+
require.IsType(t, prebuilds.EnterpriseClaimer{}, *claimer)
335+
} else {
336+
require.Equal(t, &agplprebuilds.DefaultReconciler, reconciler)
337+
require.Equal(t, &agplprebuilds.DefaultClaimer, claimer)
338+
}
339+
})
340+
}
341+
}
342+
256343
func TestAuditLogging(t *testing.T) {
257344
t.Parallel()
258345
t.Run("Enabled", func(t *testing.T) {

enterprise/coderd/prebuilds/claim.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"database/sql"
66
"errors"
7+
78
"github.com/google/uuid"
89
"golang.org/x/xerrors"
910

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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