feat: graduate prebuilds to general availability (#18607)

This PR removes the prebuilds experiment and allows the use of prebuilds
without opting into an experiment.
This commit is contained in:
Sas Swart
2025-06-26 15:54:52 +02:00
committed by GitHub
parent 872aef3af9
commit c6e0ba12d3
17 changed files with 46 additions and 88 deletions

View File

@ -677,6 +677,12 @@ workspaces stopping during the day due to template scheduling.
must be *. Only one hour and minute can be specified (ranges or comma must be *. Only one hour and minute can be specified (ranges or comma
separated values are not supported). separated values are not supported).
WORKSPACE PREBUILDS OPTIONS:
Configure how workspace prebuilds behave.
--workspace-prebuilds-reconciliation-interval duration, $CODER_WORKSPACE_PREBUILDS_RECONCILIATION_INTERVAL (default: 15s)
How often to reconcile workspace prebuilds state.
⚠️ DANGEROUS OPTIONS: ⚠️ DANGEROUS OPTIONS:
--dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING --dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING
Allow workspace apps that are not served from subdomains to be shared. Allow workspace apps that are not served from subdomains to be shared.

7
coderd/apidoc/docs.go generated
View File

@ -12194,15 +12194,13 @@ const docTemplate = `{
"auto-fill-parameters", "auto-fill-parameters",
"notifications", "notifications",
"workspace-usage", "workspace-usage",
"web-push", "web-push"
"workspace-prebuilds"
], ],
"x-enum-comments": { "x-enum-comments": {
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentExample": "This isn't used for anything.", "ExperimentExample": "This isn't used for anything.",
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
"ExperimentWebPush": "Enables web push notifications through the browser.", "ExperimentWebPush": "Enables web push notifications through the browser.",
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking." "ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
}, },
"x-enum-varnames": [ "x-enum-varnames": [
@ -12210,8 +12208,7 @@ const docTemplate = `{
"ExperimentAutoFillParameters", "ExperimentAutoFillParameters",
"ExperimentNotifications", "ExperimentNotifications",
"ExperimentWorkspaceUsage", "ExperimentWorkspaceUsage",
"ExperimentWebPush", "ExperimentWebPush"
"ExperimentWorkspacePrebuilds"
] ]
}, },
"codersdk.ExternalAuth": { "codersdk.ExternalAuth": {

View File

@ -10927,15 +10927,13 @@
"auto-fill-parameters", "auto-fill-parameters",
"notifications", "notifications",
"workspace-usage", "workspace-usage",
"web-push", "web-push"
"workspace-prebuilds"
], ],
"x-enum-comments": { "x-enum-comments": {
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
"ExperimentExample": "This isn't used for anything.", "ExperimentExample": "This isn't used for anything.",
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.", "ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
"ExperimentWebPush": "Enables web push notifications through the browser.", "ExperimentWebPush": "Enables web push notifications through the browser.",
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking." "ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
}, },
"x-enum-varnames": [ "x-enum-varnames": [
@ -10943,8 +10941,7 @@
"ExperimentAutoFillParameters", "ExperimentAutoFillParameters",
"ExperimentNotifications", "ExperimentNotifications",
"ExperimentWorkspaceUsage", "ExperimentWorkspaceUsage",
"ExperimentWebPush", "ExperimentWebPush"
"ExperimentWorkspacePrebuilds"
] ]
}, },
"codersdk.ExternalAuth": { "codersdk.ExternalAuth": {

View File

@ -5059,8 +5059,7 @@ func (s *MethodTestSuite) TestPrebuilds() {
})) }))
s.Run("GetPrebuildMetrics", s.Subtest(func(_ database.Store, check *expects) { s.Run("GetPrebuildMetrics", s.Subtest(func(_ database.Store, check *expects) {
check.Args(). check.Args().
Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead). Asserts(rbac.ResourceWorkspace.All(), policy.ActionRead)
ErrorsWithInMemDB(dbmem.ErrUnimplemented)
})) }))
s.Run("CountInProgressPrebuilds", s.Subtest(func(_ database.Store, check *expects) { s.Run("CountInProgressPrebuilds", s.Subtest(func(_ database.Store, check *expects) {
check.Args(). check.Args().

View File

@ -4270,7 +4270,7 @@ func (q *FakeQuerier) GetParameterSchemasByJobID(_ context.Context, jobID uuid.U
} }
func (*FakeQuerier) GetPrebuildMetrics(_ context.Context) ([]database.GetPrebuildMetricsRow, error) { func (*FakeQuerier) GetPrebuildMetrics(_ context.Context) ([]database.GetPrebuildMetricsRow, error) {
return nil, ErrUnimplemented return make([]database.GetPrebuildMetricsRow, 0), nil
} }
func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) { func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (database.GetPresetByIDRow, error) {

View File

@ -687,10 +687,6 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
return nil return nil
}) })
eg.Go(func() error { eg.Go(func() error {
if !r.options.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds) {
return nil
}
metrics, err := r.options.Database.GetPrebuildMetrics(ctx) metrics, err := r.options.Database.GetPrebuildMetrics(ctx)
if err != nil { if err != nil {
return xerrors.Errorf("get prebuild metrics: %w", err) return xerrors.Errorf("get prebuild metrics: %w", err)

View File

@ -408,7 +408,6 @@ func TestPrebuiltWorkspacesTelemetry(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
experimentEnabled bool
storeFn func(store database.Store) database.Store storeFn func(store database.Store) database.Store
expectedSnapshotEntries int expectedSnapshotEntries int
expectedCreated int expectedCreated int
@ -416,8 +415,7 @@ func TestPrebuiltWorkspacesTelemetry(t *testing.T) {
expectedClaimed int expectedClaimed int
}{ }{
{ {
name: "experiment enabled", name: "prebuilds enabled",
experimentEnabled: true,
storeFn: func(store database.Store) database.Store { storeFn: func(store database.Store) database.Store {
return &mockDB{Store: store} return &mockDB{Store: store}
}, },
@ -427,19 +425,11 @@ func TestPrebuiltWorkspacesTelemetry(t *testing.T) {
expectedClaimed: 3, expectedClaimed: 3,
}, },
{ {
name: "experiment enabled, prebuilds not used", name: "prebuilds not used",
experimentEnabled: true,
storeFn: func(store database.Store) database.Store { storeFn: func(store database.Store) database.Store {
return &emptyMockDB{Store: store} return &emptyMockDB{Store: store}
}, },
}, },
{
name: "experiment disabled",
experimentEnabled: false,
storeFn: func(store database.Store) database.Store {
return &mockDB{Store: store}
},
},
} }
for _, tc := range cases { for _, tc := range cases {
@ -448,11 +438,6 @@ func TestPrebuiltWorkspacesTelemetry(t *testing.T) {
deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options { deployment, snapshot := collectSnapshot(ctx, t, db, func(opts telemetry.Options) telemetry.Options {
opts.Database = tc.storeFn(db) opts.Database = tc.storeFn(db)
if tc.experimentEnabled {
opts.Experiments = codersdk.Experiments{
codersdk.ExperimentWorkspacePrebuilds,
}
}
return opts return opts
}) })

View File

@ -3070,7 +3070,6 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupPrebuilds, Group: &deploymentGroupPrebuilds,
YAML: "reconciliation_interval", YAML: "reconciliation_interval",
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"), Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
Hidden: ExperimentsSafe.Enabled(ExperimentWorkspacePrebuilds), // Hide setting while this feature is experimental.
}, },
{ {
Name: "Reconciliation Backoff Interval", Name: "Reconciliation Backoff Interval",
@ -3342,7 +3341,6 @@ const (
ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events.
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser. ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
) )
// ExperimentsKnown should include all experiments defined above. // ExperimentsKnown should include all experiments defined above.
@ -3352,16 +3350,13 @@ var ExperimentsKnown = Experiments{
ExperimentNotifications, ExperimentNotifications,
ExperimentWorkspaceUsage, ExperimentWorkspaceUsage,
ExperimentWebPush, ExperimentWebPush,
ExperimentWorkspacePrebuilds,
} }
// ExperimentsSafe should include all experiments that are safe for // ExperimentsSafe should include all experiments that are safe for
// users to opt-in to via --experimental='*'. // users to opt-in to via --experimental='*'.
// Experiments that are not ready for consumption by all users should // Experiments that are not ready for consumption by all users should
// not be included here and will be essentially hidden. // not be included here and will be essentially hidden.
var ExperimentsSafe = Experiments{ var ExperimentsSafe = Experiments{}
ExperimentWorkspacePrebuilds,
}
// Experiments is a list of experiments. // Experiments is a list of experiments.
// Multiple experiments may be enabled at the same time. // Multiple experiments may be enabled at the same time.

View File

@ -27,7 +27,6 @@ Prebuilt workspaces are tightly integrated with [workspace presets](./parameters
- [**Premium license**](../../licensing/index.md) - [**Premium license**](../../licensing/index.md)
- **Compatible Terraform provider**: Use `coder/coder` Terraform provider `>= 2.4.1`. - **Compatible Terraform provider**: Use `coder/coder` Terraform provider `>= 2.4.1`.
- **Feature flag**: Enable the `workspace-prebuilds` [experiment](../../../reference/cli/server.md#--experiments).
## Enable prebuilt workspaces for template presets ## Enable prebuilt workspaces for template presets

View File

@ -2996,7 +2996,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `notifications` | | `notifications` |
| `workspace-usage` | | `workspace-usage` |
| `web-push` | | `web-push` |
| `workspace-prebuilds` |
## codersdk.ExternalAuth ## codersdk.ExternalAuth

View File

@ -1615,6 +1615,17 @@ Enable Coder Inbox.
The upper limit of attempts to send a notification. The upper limit of attempts to send a notification.
### --workspace-prebuilds-reconciliation-interval
| | |
|-------------|-----------------------------------------------------------------|
| Type | <code>duration</code> |
| Environment | <code>$CODER_WORKSPACE_PREBUILDS_RECONCILIATION_INTERVAL</code> |
| YAML | <code>workspace_prebuilds.reconciliation_interval</code> |
| Default | <code>15s</code> |
How often to reconcile workspace prebuilds state.
### --hide-ai-tasks ### --hide-ai-tasks
| | | | | |

View File

@ -678,6 +678,12 @@ workspaces stopping during the day due to template scheduling.
must be *. Only one hour and minute can be specified (ranges or comma must be *. Only one hour and minute can be specified (ranges or comma
separated values are not supported). separated values are not supported).
WORKSPACE PREBUILDS OPTIONS:
Configure how workspace prebuilds behave.
--workspace-prebuilds-reconciliation-interval duration, $CODER_WORKSPACE_PREBUILDS_RECONCILIATION_INTERVAL (default: 15s)
How often to reconcile workspace prebuilds state.
⚠️ DANGEROUS OPTIONS: ⚠️ DANGEROUS OPTIONS:
--dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING --dangerous-allow-path-app-sharing bool, $CODER_DANGEROUS_ALLOW_PATH_APP_SHARING
Allow workspace apps that are not served from subdomains to be shared. Allow workspace apps that are not served from subdomains to be shared.

View File

@ -1150,16 +1150,9 @@ func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Obj
// nolint:revive // featureEnabled is a legit control flag. // nolint:revive // featureEnabled is a legit control flag.
func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.ReconciliationOrchestrator, agplprebuilds.Claimer) { func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.ReconciliationOrchestrator, agplprebuilds.Claimer) {
experimentEnabled := api.AGPL.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds)
if !experimentEnabled || !featureEnabled {
levelFn := api.Logger.Debug
// If the experiment is enabled but the license does not entitle the feature, operators should be warned.
if !featureEnabled { if !featureEnabled {
levelFn = api.Logger.Warn api.Logger.Warn(context.Background(), "prebuilds not enabled; ensure you have a premium license",
} slog.F("feature_enabled", featureEnabled))
levelFn(context.Background(), "prebuilds not enabled; ensure you have a premium license and the 'workspace-prebuilds' experiment set",
slog.F("experiment_enabled", experimentEnabled), slog.F("feature_enabled", featureEnabled))
return agplprebuilds.DefaultReconciler, agplprebuilds.DefaultClaimer return agplprebuilds.DefaultReconciler, agplprebuilds.DefaultClaimer
} }

View File

@ -261,32 +261,17 @@ func TestEntitlements_Prebuilds(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
experimentEnabled bool
featureEnabled bool featureEnabled bool
expectedEnabled bool expectedEnabled bool
}{ }{
{ {
name: "Fully enabled", name: "Feature enabled",
featureEnabled: true, featureEnabled: true,
experimentEnabled: true,
expectedEnabled: true, expectedEnabled: true,
}, },
{ {
name: "Feature disabled", name: "Feature disabled",
featureEnabled: false, featureEnabled: false,
experimentEnabled: true,
expectedEnabled: false,
},
{
name: "Experiment disabled",
featureEnabled: true,
experimentEnabled: false,
expectedEnabled: false,
},
{
name: "Fully disabled",
featureEnabled: false,
experimentEnabled: false,
expectedEnabled: false, expectedEnabled: false,
}, },
} }
@ -302,11 +287,7 @@ func TestEntitlements_Prebuilds(t *testing.T) {
_, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ _, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{ Options: &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { DeploymentValues: coderdtest.DeploymentValues(t),
if tc.experimentEnabled {
values.Experiments = serpent.StringArray{string(codersdk.ExperimentWorkspacePrebuilds)}
}
}),
}, },
EntitlementsUpdateInterval: time.Second, EntitlementsUpdateInterval: time.Second,

View File

@ -112,7 +112,6 @@ func TestReinitializeAgent(t *testing.T) {
Pubsub: ps, Pubsub: ps,
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) {
dv.Prebuilds.ReconciliationInterval = serpent.Duration(time.Second) dv.Prebuilds.ReconciliationInterval = serpent.Duration(time.Second)
dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds))
}), }),
}, },
LicenseOptions: &coderdenttest.LicenseOptions{ LicenseOptions: &coderdenttest.LicenseOptions{

View File

@ -531,10 +531,7 @@ func TestCreateUserWorkspace(t *testing.T) {
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{ Options: &coderdtest.Options{
DeploymentValues: coderdtest.DeploymentValues(t, func(dv *codersdk.DeploymentValues) { DeploymentValues: coderdtest.DeploymentValues(t),
err := dv.Experiments.Append(string(codersdk.ExperimentWorkspacePrebuilds))
require.NoError(t, err)
}),
}, },
LicenseOptions: &coderdenttest.LicenseOptions{ LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{ Features: license.Features{

View File

@ -795,7 +795,6 @@ export type Experiment =
| "example" | "example"
| "notifications" | "notifications"
| "web-push" | "web-push"
| "workspace-prebuilds"
| "workspace-usage"; | "workspace-usage";
export const Experiments: Experiment[] = [ export const Experiments: Experiment[] = [
@ -803,7 +802,6 @@ export const Experiments: Experiment[] = [
"example", "example",
"notifications", "notifications",
"web-push", "web-push",
"workspace-prebuilds",
"workspace-usage", "workspace-usage",
]; ];