mirror of
https://github.com/coder/coder.git
synced 2025-07-23 21:32:07 +00:00
feat: add prebuilds configuration & bootstrapping (#17527)
Closes https://github.com/coder/internal/issues/508 --------- Signed-off-by: Danny Kopping <dannykopping@gmail.com> Co-authored-by: Cian Johnston <cian@coder.com>
This commit is contained in:
13
cli/testdata/server-config.yaml.golden
vendored
13
cli/testdata/server-config.yaml.golden
vendored
@@ -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
coderd/apidoc/docs.go
generated
27
coderd/apidoc/docs.go
generated
@@ -11926,6 +11926,9 @@ const docTemplate = `{
|
||||
"workspace_hostname_suffix": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_prebuilds": {
|
||||
"$ref": "#/definitions/codersdk.PrebuildsConfig"
|
||||
},
|
||||
"write_config": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -12005,7 +12008,8 @@ const docTemplate = `{
|
||||
"notifications",
|
||||
"workspace-usage",
|
||||
"web-push",
|
||||
"dynamic-parameters"
|
||||
"dynamic-parameters",
|
||||
"workspace-prebuilds"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
@@ -12013,6 +12017,7 @@ const docTemplate = `{
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
|
||||
"ExperimentWebPush": "Enables web push notifications through the browser.",
|
||||
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
|
||||
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
@@ -12021,7 +12026,8 @@ const docTemplate = `{
|
||||
"ExperimentNotifications",
|
||||
"ExperimentWorkspaceUsage",
|
||||
"ExperimentWebPush",
|
||||
"ExperimentDynamicParameters"
|
||||
"ExperimentDynamicParameters",
|
||||
"ExperimentWorkspacePrebuilds"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@@ -13654,6 +13660,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PrebuildsConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reconciliation_backoff_interval": {
|
||||
"description": "ReconciliationBackoffInterval specifies the amount of time to increase the backoff interval\nwhen errors occur during reconciliation.",
|
||||
"type": "integer"
|
||||
},
|
||||
"reconciliation_backoff_lookback": {
|
||||
"description": "ReconciliationBackoffLookback determines the time window to look back when calculating\nthe number of failed prebuilds, which influences the backoff strategy.",
|
||||
"type": "integer"
|
||||
},
|
||||
"reconciliation_interval": {
|
||||
"description": "ReconciliationInterval defines how often the workspace prebuilds state should be reconciled.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Preset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
27
coderd/apidoc/swagger.json
generated
27
coderd/apidoc/swagger.json
generated
@@ -10684,6 +10684,9 @@
|
||||
"workspace_hostname_suffix": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_prebuilds": {
|
||||
"$ref": "#/definitions/codersdk.PrebuildsConfig"
|
||||
},
|
||||
"write_config": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -10759,7 +10762,8 @@
|
||||
"notifications",
|
||||
"workspace-usage",
|
||||
"web-push",
|
||||
"dynamic-parameters"
|
||||
"dynamic-parameters",
|
||||
"workspace-prebuilds"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
@@ -10767,6 +10771,7 @@
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
|
||||
"ExperimentWebPush": "Enables web push notifications through the browser.",
|
||||
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
|
||||
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
@@ -10775,7 +10780,8 @@
|
||||
"ExperimentNotifications",
|
||||
"ExperimentWorkspaceUsage",
|
||||
"ExperimentWebPush",
|
||||
"ExperimentDynamicParameters"
|
||||
"ExperimentDynamicParameters",
|
||||
"ExperimentWorkspacePrebuilds"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
@@ -12346,6 +12352,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.PrebuildsConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reconciliation_backoff_interval": {
|
||||
"description": "ReconciliationBackoffInterval specifies the amount of time to increase the backoff interval\nwhen errors occur during reconciliation.",
|
||||
"type": "integer"
|
||||
},
|
||||
"reconciliation_backoff_lookback": {
|
||||
"description": "ReconciliationBackoffLookback determines the time window to look back when calculating\nthe number of failed prebuilds, which influences the backoff strategy.",
|
||||
"type": "integer"
|
||||
},
|
||||
"reconciliation_interval": {
|
||||
"description": "ReconciliationInterval defines how often the workspace prebuilds state should be reconciled.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Preset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@@ -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(),
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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{}
|
||||
|
@@ -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.
|
||||
@@ -103,6 +104,7 @@ var FeatureNames = []FeatureName{
|
||||
FeatureControlSharedPorts,
|
||||
FeatureCustomRoles,
|
||||
FeatureMultipleOrganizations,
|
||||
FeatureWorkspacePrebuilds,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
@@ -132,6 +134,7 @@ func (n FeatureName) AlwaysEnable() bool {
|
||||
FeatureHighAvailability: true,
|
||||
FeatureCustomRoles: true,
|
||||
FeatureMultipleOrganizations: true,
|
||||
FeatureWorkspacePrebuilds: true,
|
||||
}[n]
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
5
docs/reference/api/general.md
generated
5
docs/reference/api/general.md
generated
@@ -519,6 +519,11 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \
|
||||
"wgtunnel_host": "string",
|
||||
"wildcard_access_url": "string",
|
||||
"workspace_hostname_suffix": "string",
|
||||
"workspace_prebuilds": {
|
||||
"reconciliation_backoff_interval": 0,
|
||||
"reconciliation_backoff_lookback": 0,
|
||||
"reconciliation_interval": 0
|
||||
},
|
||||
"write_config": true
|
||||
},
|
||||
"options": [
|
||||
|
30
docs/reference/api/schemas.md
generated
30
docs/reference/api/schemas.md
generated
@@ -2170,6 +2170,11 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"wgtunnel_host": "string",
|
||||
"wildcard_access_url": "string",
|
||||
"workspace_hostname_suffix": "string",
|
||||
"workspace_prebuilds": {
|
||||
"reconciliation_backoff_interval": 0,
|
||||
"reconciliation_backoff_lookback": 0,
|
||||
"reconciliation_interval": 0
|
||||
},
|
||||
"write_config": true
|
||||
},
|
||||
"options": [
|
||||
@@ -2650,6 +2655,11 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"wgtunnel_host": "string",
|
||||
"wildcard_access_url": "string",
|
||||
"workspace_hostname_suffix": "string",
|
||||
"workspace_prebuilds": {
|
||||
"reconciliation_backoff_interval": 0,
|
||||
"reconciliation_backoff_lookback": 0,
|
||||
"reconciliation_interval": 0
|
||||
},
|
||||
"write_config": true
|
||||
}
|
||||
```
|
||||
@@ -2719,6 +2729,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `wgtunnel_host` | string | false | | |
|
||||
| `wildcard_access_url` | string | false | | |
|
||||
| `workspace_hostname_suffix` | string | false | | |
|
||||
| `workspace_prebuilds` | [codersdk.PrebuildsConfig](#codersdkprebuildsconfig) | false | | |
|
||||
| `write_config` | boolean | false | | |
|
||||
|
||||
## codersdk.DisplayApp
|
||||
@@ -2817,6 +2828,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `workspace-usage` |
|
||||
| `web-push` |
|
||||
| `dynamic-parameters` |
|
||||
| `workspace-prebuilds` |
|
||||
|
||||
## codersdk.ExternalAuth
|
||||
|
||||
@@ -4659,6 +4671,24 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith
|
||||
| `address` | [serpent.HostPort](#serpenthostport) | false | | |
|
||||
| `enable` | boolean | false | | |
|
||||
|
||||
## codersdk.PrebuildsConfig
|
||||
|
||||
```json
|
||||
{
|
||||
"reconciliation_backoff_interval": 0,
|
||||
"reconciliation_backoff_lookback": 0,
|
||||
"reconciliation_interval": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-----------------------------------|---------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `reconciliation_backoff_interval` | integer | false | | Reconciliation backoff interval specifies the amount of time to increase the backoff interval when errors occur during reconciliation. |
|
||||
| `reconciliation_backoff_lookback` | integer | false | | Reconciliation backoff lookback determines the time window to look back when calculating the number of failed prebuilds, which influences the backoff strategy. |
|
||||
| `reconciliation_interval` | integer | false | | Reconciliation interval defines how often the workspace prebuilds state should be reconciled. |
|
||||
|
||||
## codersdk.Preset
|
||||
|
||||
```json
|
||||
|
@@ -12,12 +12,15 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/quartz"
|
||||
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/coderd/appearance"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/entitlements"
|
||||
"github.com/coder/coder/v2/coderd/idpsync"
|
||||
agplportsharing "github.com/coder/coder/v2/coderd/portsharing"
|
||||
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/enidpsync"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/portsharing"
|
||||
@@ -43,6 +46,7 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/dbauthz"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/proxyhealth"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/enterprise/dbcrypt"
|
||||
@@ -658,6 +662,7 @@ func (api *API) Close() error {
|
||||
if api.Options.CheckInactiveUsersCancelFunc != nil {
|
||||
api.Options.CheckInactiveUsersCancelFunc()
|
||||
}
|
||||
|
||||
return api.AGPL.Close()
|
||||
}
|
||||
|
||||
@@ -860,6 +865,20 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
api.AGPL.PortSharer.Store(&ps)
|
||||
}
|
||||
|
||||
if initial, changed, enabled := featureChanged(codersdk.FeatureWorkspacePrebuilds); shouldUpdate(initial, changed, enabled) {
|
||||
reconciler, claimer := api.setupPrebuilds(enabled)
|
||||
if current := api.AGPL.PrebuildsReconciler.Load(); current != nil {
|
||||
stopCtx, giveUp := context.WithTimeoutCause(context.Background(), time.Second*30, xerrors.New("gave up waiting for reconciler to stop"))
|
||||
defer giveUp()
|
||||
(*current).Stop(stopCtx, xerrors.New("entitlements change"))
|
||||
}
|
||||
|
||||
api.AGPL.PrebuildsReconciler.Store(&reconciler)
|
||||
go reconciler.Run(context.Background())
|
||||
|
||||
api.AGPL.PrebuildsClaimer.Store(&claimer)
|
||||
}
|
||||
|
||||
// External token encryption is soft-enforced
|
||||
featureExternalTokenEncryption := reloadedEntitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
||||
@@ -1128,3 +1147,24 @@ func (api *API) runEntitlementsLoop(ctx context.Context) {
|
||||
func (api *API) Authorize(r *http.Request, action policy.Action, object rbac.Objecter) bool {
|
||||
return api.AGPL.HTTPAuth.Authorize(r, action, object)
|
||||
}
|
||||
|
||||
// nolint:revive // featureEnabled is a legit control flag.
|
||||
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 {
|
||||
levelFn = api.Logger.Warn
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.DeploymentValues.Prebuilds,
|
||||
api.Logger.Named("prebuilds"), quartz.NewReal())
|
||||
return reconciler, prebuilds.EnterpriseClaimer{}
|
||||
}
|
||||
|
@@ -28,10 +28,15 @@ import (
|
||||
"github.com/coder/coder/v2/agent"
|
||||
"github.com/coder/coder/v2/agent/agenttest"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
|
||||
"github.com/coder/coder/v2/tailnet/tailnettest"
|
||||
|
||||
"github.com/coder/retry"
|
||||
"github.com/coder/serpent"
|
||||
|
||||
agplaudit "github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
@@ -50,8 +55,6 @@ import (
|
||||
"github.com/coder/coder/v2/enterprise/dbcrypt"
|
||||
"github.com/coder/coder/v2/enterprise/replicasync"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/retry"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -253,6 +256,90 @@ func TestEntitlements_HeaderWarnings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEntitlements_Prebuilds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
experimentEnabled bool
|
||||
featureEnabled bool
|
||||
expectedEnabled bool
|
||||
}{
|
||||
{
|
||||
name: "Fully enabled",
|
||||
featureEnabled: true,
|
||||
experimentEnabled: true,
|
||||
expectedEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "Feature disabled",
|
||||
featureEnabled: false,
|
||||
experimentEnabled: true,
|
||||
expectedEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "Experiment disabled",
|
||||
featureEnabled: true,
|
||||
experimentEnabled: false,
|
||||
expectedEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "Fully disabled",
|
||||
featureEnabled: false,
|
||||
experimentEnabled: false,
|
||||
expectedEnabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var prebuildsEntitled int64
|
||||
if tc.featureEnabled {
|
||||
prebuildsEntitled = 1
|
||||
}
|
||||
|
||||
_, _, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) {
|
||||
if tc.experimentEnabled {
|
||||
values.Experiments = serpent.StringArray{string(codersdk.ExperimentWorkspacePrebuilds)}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
EntitlementsUpdateInterval: time.Second,
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspacePrebuilds: prebuildsEntitled,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// The entitlements will need to refresh before the reconciler is set.
|
||||
require.Eventually(t, func() bool {
|
||||
return api.AGPL.PrebuildsReconciler.Load() != nil
|
||||
}, testutil.WaitSuperLong, testutil.IntervalFast)
|
||||
|
||||
reconciler := api.AGPL.PrebuildsReconciler.Load()
|
||||
claimer := api.AGPL.PrebuildsClaimer.Load()
|
||||
require.NotNil(t, reconciler)
|
||||
require.NotNil(t, claimer)
|
||||
|
||||
if tc.expectedEnabled {
|
||||
require.IsType(t, &prebuilds.StoreReconciler{}, *reconciler)
|
||||
require.IsType(t, prebuilds.EnterpriseClaimer{}, *claimer)
|
||||
} else {
|
||||
require.Equal(t, &agplprebuilds.DefaultReconciler, reconciler)
|
||||
require.Equal(t, &agplprebuilds.DefaultClaimer, claimer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditLogging(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Enabled", func(t *testing.T) {
|
||||
|
@@ -38,6 +38,7 @@ type StoreReconciler struct {
|
||||
clock quartz.Clock
|
||||
|
||||
cancelFn context.CancelCauseFunc
|
||||
running atomic.Bool
|
||||
stopped atomic.Bool
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ func NewStoreReconciler(
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StoreReconciler) RunLoop(ctx context.Context) {
|
||||
func (c *StoreReconciler) Run(ctx context.Context) {
|
||||
reconciliationInterval := c.cfg.ReconciliationInterval.Value()
|
||||
if reconciliationInterval <= 0 { // avoids a panic
|
||||
reconciliationInterval = 5 * time.Minute
|
||||
@@ -82,6 +83,11 @@ func (c *StoreReconciler) RunLoop(ctx context.Context) {
|
||||
ctx, cancel := context.WithCancelCause(dbauthz.AsPrebuildsOrchestrator(ctx))
|
||||
c.cancelFn = cancel
|
||||
|
||||
// Everything is in place, reconciler can now be considered as running.
|
||||
//
|
||||
// NOTE: without this atomic bool, Stop might race with Run for the c.cancelFn above.
|
||||
c.running.Store(true)
|
||||
|
||||
for {
|
||||
select {
|
||||
// TODO: implement pubsub listener to allow reconciling a specific template imperatively once it has been changed,
|
||||
@@ -107,16 +113,26 @@ func (c *StoreReconciler) RunLoop(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (c *StoreReconciler) Stop(ctx context.Context, cause error) {
|
||||
defer c.running.Store(false)
|
||||
|
||||
if cause != nil {
|
||||
c.logger.Error(context.Background(), "stopping reconciler due to an error", slog.Error(cause))
|
||||
} else {
|
||||
c.logger.Info(context.Background(), "gracefully stopping reconciler")
|
||||
}
|
||||
|
||||
if c.isStopped() {
|
||||
// If previously stopped (Swap returns previous value), then short-circuit.
|
||||
//
|
||||
// NOTE: we need to *prospectively* mark this as stopped to prevent Stop being called multiple times and causing problems.
|
||||
if c.stopped.Swap(true) {
|
||||
return
|
||||
}
|
||||
c.stopped.Store(true)
|
||||
|
||||
// If the reconciler is not running, there's nothing else to do.
|
||||
if !c.running.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
if c.cancelFn != nil {
|
||||
c.cancelFn(cause)
|
||||
}
|
||||
@@ -138,10 +154,6 @@ func (c *StoreReconciler) Stop(ctx context.Context, cause error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *StoreReconciler) isStopped() bool {
|
||||
return c.stopped.Load()
|
||||
}
|
||||
|
||||
// ReconcileAll will attempt to resolve the desired vs actual state of all templates which have presets with prebuilds configured.
|
||||
//
|
||||
// NOTE:
|
||||
|
@@ -575,7 +575,7 @@ func TestRunLoop(t *testing.T) {
|
||||
t, &slogtest.Options{IgnoreErrors: true},
|
||||
).Leveled(slog.LevelDebug)
|
||||
db, pubSub := dbtestutil.NewDB(t)
|
||||
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock)
|
||||
reconciler := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock)
|
||||
|
||||
ownerID := uuid.New()
|
||||
dbgen.User(t, db, database.User{
|
||||
@@ -639,7 +639,7 @@ func TestRunLoop(t *testing.T) {
|
||||
// we need to wait until ticker is initialized, and only then use clock.Advance()
|
||||
// otherwise clock.Advance() will be ignored
|
||||
trap := clock.Trap().NewTicker()
|
||||
go controller.RunLoop(ctx)
|
||||
go reconciler.Run(ctx)
|
||||
// wait until ticker is initialized
|
||||
trap.MustWait(ctx).Release()
|
||||
// start 1st iteration of ReconciliationLoop
|
||||
@@ -681,7 +681,7 @@ func TestRunLoop(t *testing.T) {
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
// gracefully stop the reconciliation loop
|
||||
controller.Stop(ctx, nil)
|
||||
reconciler.Stop(ctx, nil)
|
||||
}
|
||||
|
||||
func TestFailedBuildBackoff(t *testing.T) {
|
||||
|
4
site/src/api/typesGenerated.ts
generated
4
site/src/api/typesGenerated.ts
generated
@@ -691,6 +691,7 @@ export interface DeploymentValues {
|
||||
readonly notifications?: NotificationsConfig;
|
||||
readonly additional_csp_policy?: string;
|
||||
readonly workspace_hostname_suffix?: string;
|
||||
readonly workspace_prebuilds?: PrebuildsConfig;
|
||||
readonly config?: string;
|
||||
readonly write_config?: boolean;
|
||||
readonly address?: string;
|
||||
@@ -773,6 +774,7 @@ export type Experiment =
|
||||
| "example"
|
||||
| "notifications"
|
||||
| "web-push"
|
||||
| "workspace-prebuilds"
|
||||
| "workspace-usage";
|
||||
|
||||
// From codersdk/deployment.go
|
||||
@@ -887,6 +889,7 @@ export type FeatureName =
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
| "workspace_batch_actions"
|
||||
| "workspace_prebuilds"
|
||||
| "workspace_proxy";
|
||||
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
@@ -907,6 +910,7 @@ export const FeatureNames: FeatureName[] = [
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
"workspace_batch_actions",
|
||||
"workspace_prebuilds",
|
||||
"workspace_proxy",
|
||||
];
|
||||
|
||||
|
Reference in New Issue
Block a user