Skip to content

feat: add prebuilds configuration & bootstrapping #17527

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 9 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions cli/testdata/server-config.yaml.golden
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,16 @@ notifications:
# How often to query the database for queued notifications.
# (default: 15s, type: duration)
fetchInterval: 15s
# Configure how workspace prebuilds behave.
workspace_prebuilds:
# How often to reconcile workspace prebuilds state.
# (default: 15s, type: duration)
reconciliation_interval: 15s
# Interval to increase reconciliation backoff by when prebuilds fail, after which
# a retry attempt is made.
# (default: 15s, type: duration)
reconciliation_backoff_interval: 15s
# Interval to look back to determine number of failed prebuilds, which influences
# backoff.
# (default: 1h0m0s, type: duration)
reconciliation_backoff_lookback_period: 1h0m0s
27 changes: 25 additions & 2 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 25 additions & 2 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ func New(options *Options) *API {
api.AppearanceFetcher.Store(&f)
api.PortSharer.Store(&portsharing.DefaultPortSharer)
api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer)
api.PrebuildsReconciler.Store(&prebuilds.DefaultReconciler)
buildInfo := codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
Version: buildinfo.Version(),
Expand Down Expand Up @@ -1568,10 +1569,11 @@ type API struct {
DERPMapper atomic.Pointer[func(derpMap *tailcfg.DERPMap) *tailcfg.DERPMap]
// AccessControlStore is a pointer to an atomic pointer since it is
// passed to dbauthz.
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
PortSharer atomic.Pointer[portsharing.PortSharer]
FileCache files.Cache
PrebuildsClaimer atomic.Pointer[prebuilds.Claimer]
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
PortSharer atomic.Pointer[portsharing.PortSharer]
FileCache files.Cache
PrebuildsClaimer atomic.Pointer[prebuilds.Claimer]
PrebuildsReconciler atomic.Pointer[prebuilds.ReconciliationOrchestrator]

UpdatesProvider tailnet.WorkspaceUpdatesProvider

Expand Down Expand Up @@ -1659,6 +1661,13 @@ func (api *API) Close() error {
_ = api.AppSigningKeyCache.Close()
_ = api.AppEncryptionKeyCache.Close()
_ = api.UpdatesProvider.Close()

if current := api.PrebuildsReconciler.Load(); current != nil {
ctx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop before shutdown"))
defer giveUp()
(*current).Stop(ctx, nil)
}

return nil
}

Expand Down
4 changes: 2 additions & 2 deletions coderd/prebuilds/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ var ErrNoClaimablePrebuiltWorkspaces = xerrors.New("no claimable prebuilt worksp
type ReconciliationOrchestrator interface {
Reconciler

// RunLoop starts a continuous reconciliation loop that periodically calls ReconcileAll
// Run starts a continuous reconciliation loop that periodically calls ReconcileAll
// to ensure all prebuilds are in their desired states. The loop runs until the context
// is canceled or Stop is called.
RunLoop(ctx context.Context)
Run(ctx context.Context)

// Stop gracefully shuts down the orchestrator with the given cause.
// The cause is used for logging and error reporting.
Expand Down
31 changes: 9 additions & 22 deletions coderd/prebuilds/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,28 @@ import (

type NoopReconciler struct{}

func NewNoopReconciler() *NoopReconciler {
return &NoopReconciler{}
}

func (NoopReconciler) RunLoop(context.Context) {}

func (NoopReconciler) Stop(context.Context, error) {}

func (NoopReconciler) ReconcileAll(context.Context) error {
return nil
}

func (NoopReconciler) Run(context.Context) {}
func (NoopReconciler) Stop(context.Context, error) {}
func (NoopReconciler) ReconcileAll(context.Context) error { return nil }
func (NoopReconciler) SnapshotState(context.Context, database.Store) (*GlobalSnapshot, error) {
return &GlobalSnapshot{}, nil
}

func (NoopReconciler) ReconcilePreset(context.Context, PresetSnapshot) error {
return nil
}

func (NoopReconciler) ReconcilePreset(context.Context, PresetSnapshot) error { return nil }
func (NoopReconciler) CalculateActions(context.Context, PresetSnapshot) (*ReconciliationActions, error) {
return &ReconciliationActions{}, nil
}

var _ ReconciliationOrchestrator = NoopReconciler{}
var DefaultReconciler ReconciliationOrchestrator = NoopReconciler{}

type AGPLPrebuildClaimer struct{}
type NoopClaimer struct{}

func (AGPLPrebuildClaimer) Claim(context.Context, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
func (NoopClaimer) Claim(context.Context, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
// Not entitled to claim prebuilds in AGPL version.
return nil, ErrNoClaimablePrebuiltWorkspaces
}

func (AGPLPrebuildClaimer) Initiator() uuid.UUID {
func (NoopClaimer) Initiator() uuid.UUID {
return uuid.Nil
}

var DefaultClaimer Claimer = AGPLPrebuildClaimer{}
var DefaultClaimer Claimer = NoopClaimer{}
53 changes: 51 additions & 2 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const (
FeatureControlSharedPorts FeatureName = "control_shared_ports"
FeatureCustomRoles FeatureName = "custom_roles"
FeatureMultipleOrganizations FeatureName = "multiple_organizations"
FeatureWorkspacePrebuilds FeatureName = "workspace_prebuilds"
)

// FeatureNames must be kept in-sync with the Feature enum above.
Expand All @@ -103,6 +104,7 @@ var FeatureNames = []FeatureName{
FeatureControlSharedPorts,
FeatureCustomRoles,
FeatureMultipleOrganizations,
FeatureWorkspacePrebuilds,
}

// Humanize returns the feature name in a human-readable format.
Expand Down Expand Up @@ -132,6 +134,7 @@ func (n FeatureName) AlwaysEnable() bool {
FeatureHighAvailability: true,
FeatureCustomRoles: true,
FeatureMultipleOrganizations: true,
FeatureWorkspacePrebuilds: true,
}[n]
}

Expand Down Expand Up @@ -394,6 +397,7 @@ type DeploymentValues struct {
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"`
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`

Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
Expand Down Expand Up @@ -1034,6 +1038,11 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
Parent: &deploymentGroupNotifications,
YAML: "webhook",
}
deploymentGroupPrebuilds = serpent.Group{
Name: "Workspace Prebuilds",
YAML: "workspace_prebuilds",
Description: "Configure how workspace prebuilds behave.",
}
deploymentGroupInbox = serpent.Group{
Name: "Inbox",
Parent: &deploymentGroupNotifications,
Expand Down Expand Up @@ -3029,7 +3038,44 @@ Write out the current server config as YAML to stdout.`,
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
Hidden: true, // Hidden because most operators should not need to modify this.
},
// Push notifications.

// Workspace Prebuilds Options
{
Name: "Reconciliation Interval",
Description: "How often to reconcile workspace prebuilds state.",
Flag: "workspace-prebuilds-reconciliation-interval",
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_INTERVAL",
Value: &c.Prebuilds.ReconciliationInterval,
Default: (time.Second * 15).String(),
Group: &deploymentGroupPrebuilds,
YAML: "reconciliation_interval",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
Hidden: ExperimentsSafe.Enabled(ExperimentWorkspacePrebuilds), // Hide setting while this feature is experimental.
},
{
Name: "Reconciliation Backoff Interval",
Description: "Interval to increase reconciliation backoff by when prebuilds fail, after which a retry attempt is made.",
Flag: "workspace-prebuilds-reconciliation-backoff-interval",
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_INTERVAL",
Value: &c.Prebuilds.ReconciliationBackoffInterval,
Default: (time.Second * 15).String(),
Group: &deploymentGroupPrebuilds,
YAML: "reconciliation_backoff_interval",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
Hidden: true,
},
{
Name: "Reconciliation Backoff Lookback Period",
Description: "Interval to look back to determine number of failed prebuilds, which influences backoff.",
Flag: "workspace-prebuilds-reconciliation-backoff-lookback-period",
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_BACKOFF_LOOKBACK_PERIOD",
Value: &c.Prebuilds.ReconciliationBackoffLookback,
Default: (time.Hour).String(), // TODO: use https://pkg.go.dev/github.com/jackc/pgtype@v1.12.0#Interval
Group: &deploymentGroupPrebuilds,
YAML: "reconciliation_backoff_lookback_period",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
Hidden: true,
},
}

return opts
Expand Down Expand Up @@ -3256,13 +3302,16 @@ const (
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
ExperimentDynamicParameters Experiment = "dynamic-parameters" // Enables dynamic parameters when creating a workspace.
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
)

// ExperimentsSafe should include all experiments that are safe for
// users to opt-in to via --experimental='*'.
// Experiments that are not ready for consumption by all users should
// not be included here and will be essentially hidden.
var ExperimentsSafe = Experiments{}
var ExperimentsSafe = Experiments{
ExperimentWorkspacePrebuilds,
}

// Experiments is a list of experiments.
// Multiple experiments may be enabled at the same time.
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/api/general.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading