Skip to content

Commit 7498980

Browse files
committed
Hide prebuilds behind premium license & experiment
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent f2bed85 commit 7498980

File tree

8 files changed

+100
-49
lines changed

8 files changed

+100
-49
lines changed

cli/server.go

-7
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ import (
3131
"sync/atomic"
3232
"time"
3333

34-
"github.com/coder/coder/v2/coderd/prebuilds"
35-
3634
"github.com/charmbracelet/lipgloss"
3735
"github.com/coreos/go-oidc/v3/oidc"
3836
"github.com/coreos/go-systemd/daemon"
@@ -943,11 +941,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
943941
cliui.Info(inv.Stdout, "Notifications are currently disabled as there are no configured delivery methods. See https://coder.com/docs/admin/monitoring/notifications#delivery-methods for more details.")
944942
}
945943

946-
// TODO: implement experiment and configs
947-
prebuildsCtrl := prebuilds.NewController(options.Database, options.Pubsub, logger.Named("prebuilds.controller"))
948-
go prebuildsCtrl.Loop(ctx)
949-
defer prebuildsCtrl.Stop()
950-
951944
// Since errCh only has one buffered slot, all routines
952945
// sending on it must be wrapped in a select/default to
953946
// avoid leaving dangling goroutines waiting for the

coderd/apidoc/docs.go

+5-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+5-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/prebuilds/controller.go

+36-37
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"math"
99
mrand "math/rand"
1010
"strings"
11+
"sync/atomic"
1112
"time"
1213

1314
"github.com/hashicorp/go-multierror"
@@ -34,66 +35,68 @@ import (
3435

3536
type Controller struct {
3637
store database.Store
38+
cfg codersdk.PrebuildsConfig
3739
pubsub pubsub.Pubsub
38-
logger slog.Logger
3940

40-
nudgeCh chan *uuid.UUID
41-
closeCh chan struct{}
41+
logger slog.Logger
42+
nudgeCh chan *uuid.UUID
43+
cancelFn context.CancelCauseFunc
44+
closed atomic.Bool
4245
}
4346

44-
func NewController(store database.Store, pubsub pubsub.Pubsub, logger slog.Logger) *Controller {
47+
func NewController(store database.Store, pubsub pubsub.Pubsub, cfg codersdk.PrebuildsConfig, logger slog.Logger) *Controller {
4548
return &Controller{
4649
store: store,
4750
pubsub: pubsub,
4851
logger: logger,
52+
cfg: cfg,
4953
nudgeCh: make(chan *uuid.UUID, 1),
50-
closeCh: make(chan struct{}, 1),
5154
}
5255
}
5356

54-
func (c Controller) Loop(ctx context.Context) {
55-
ticker := time.NewTicker(time.Second * 5) // TODO: configurable? 1m probably lowest valid value
57+
func (c *Controller) Loop(ctx context.Context) error {
58+
ticker := time.NewTicker(c.cfg.ReconciliationInterval.Value())
5659
defer ticker.Stop()
5760

5861
// TODO: create new authz role
59-
ctx = dbauthz.AsSystemRestricted(ctx)
62+
ctx, cancel := context.WithCancelCause(dbauthz.AsSystemRestricted(ctx))
63+
c.cancelFn = cancel
6064

61-
// TODO: bounded concurrency?
62-
var eg errgroup.Group
6365
for {
6466
select {
6567
// Accept nudges from outside the control loop to trigger a new iteration.
6668
case template := <-c.nudgeCh:
67-
eg.Go(func() error {
68-
c.reconcile(ctx, template)
69-
return nil
70-
})
69+
c.reconcile(ctx, template)
7170
// Trigger a new iteration on each tick.
7271
case <-ticker.C:
73-
eg.Go(func() error {
74-
c.reconcile(ctx, nil)
75-
return nil
76-
})
77-
case <-c.closeCh:
78-
c.logger.Info(ctx, "control loop stopped")
79-
goto wait
72+
c.reconcile(ctx, nil)
8073
case <-ctx.Done():
81-
c.logger.Error(context.Background(), "control loop exited: %w", ctx.Err())
82-
goto wait
74+
c.logger.Error(context.Background(), "prebuilds reconciliation loop exited", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx)))
75+
return ctx.Err()
8376
}
8477
}
78+
}
79+
80+
func (c *Controller) Close(cause error) {
81+
if c.isClosed() {
82+
return
83+
}
84+
c.closed.Store(true)
85+
if c.cancelFn != nil {
86+
c.cancelFn(cause)
87+
}
88+
}
8589

86-
// TODO: no gotos
87-
wait:
88-
_ = eg.Wait()
90+
func (c *Controller) isClosed() bool {
91+
return c.closed.Load()
8992
}
9093

91-
func (c Controller) ReconcileTemplate(templateID uuid.UUID) {
94+
func (c *Controller) ReconcileTemplate(templateID uuid.UUID) {
9295
// TODO: replace this with pubsub listening
9396
c.nudgeCh <- &templateID
9497
}
9598

96-
func (c Controller) reconcile(ctx context.Context, templateID *uuid.UUID) {
99+
func (c *Controller) reconcile(ctx context.Context, templateID *uuid.UUID) {
97100
var logger slog.Logger
98101
if templateID == nil {
99102
logger = c.logger.With(slog.F("reconcile_context", "all"))
@@ -167,7 +170,7 @@ type reconciliationActions struct {
167170

168171
// calculateActions MUST be called within the context of a transaction (TODO: isolation)
169172
// with an advisory lock to prevent TOCTOU races.
170-
func (c Controller) calculateActions(ctx context.Context, template database.Template, state database.GetTemplatePrebuildStateRow) (*reconciliationActions, error) {
173+
func (c *Controller) calculateActions(ctx context.Context, template database.Template, state database.GetTemplatePrebuildStateRow) (*reconciliationActions, error) {
171174
// TODO: align workspace states with how we represent them on the FE and the CLI
172175
// right now there's some slight differences which can lead to additional prebuilds being created
173176

@@ -279,7 +282,7 @@ func (c Controller) calculateActions(ctx context.Context, template database.Temp
279282
return actions, nil
280283
}
281284

282-
func (c Controller) reconcileTemplate(ctx context.Context, template database.Template) error {
285+
func (c *Controller) reconcileTemplate(ctx context.Context, template database.Template) error {
283286
logger := c.logger.With(slog.F("template_id", template.ID.String()))
284287

285288
// get number of desired vs actual prebuild instances
@@ -360,7 +363,7 @@ func (c Controller) reconcileTemplate(ctx context.Context, template database.Tem
360363
return nil
361364
}
362365

363-
func (c Controller) createPrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
366+
func (c *Controller) createPrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
364367
name, err := generateName()
365368
if err != nil {
366369
return xerrors.Errorf("failed to generate unique prebuild ID: %w", err)
@@ -394,7 +397,7 @@ func (c Controller) createPrebuild(ctx context.Context, db database.Store, prebu
394397

395398
return c.provision(ctx, db, prebuildID, template, presetID, database.WorkspaceTransitionStart, workspace)
396399
}
397-
func (c Controller) deletePrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
400+
func (c *Controller) deletePrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
398401
workspace, err := db.GetWorkspaceByID(ctx, prebuildID)
399402
if err != nil {
400403
return xerrors.Errorf("get workspace by ID: %w", err)
@@ -406,7 +409,7 @@ func (c Controller) deletePrebuild(ctx context.Context, db database.Store, prebu
406409
return c.provision(ctx, db, prebuildID, template, presetID, database.WorkspaceTransitionDelete, workspace)
407410
}
408411

409-
func (c Controller) provision(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID, transition database.WorkspaceTransition, workspace database.Workspace) error {
412+
func (c *Controller) provision(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID, transition database.WorkspaceTransition, workspace database.Workspace) error {
410413
tvp, err := db.GetPresetParametersByTemplateVersionID(ctx, template.ActiveVersionID)
411414
if err != nil {
412415
return xerrors.Errorf("fetch preset details: %w", err)
@@ -464,10 +467,6 @@ func (c Controller) provision(ctx context.Context, db database.Store, prebuildID
464467
return nil
465468
}
466469

467-
func (c Controller) Stop() {
468-
c.closeCh <- struct{}{}
469-
}
470-
471470
// generateName generates a 20-byte prebuild name which should safe to use without truncation in most situations.
472471
// UUIDs may be too long for a resource name in cloud providers (since this ID will be used in the prebuild's name).
473472
//

codersdk/deployment.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const (
8181
FeatureControlSharedPorts FeatureName = "control_shared_ports"
8282
FeatureCustomRoles FeatureName = "custom_roles"
8383
FeatureMultipleOrganizations FeatureName = "multiple_organizations"
84+
FeatureWorkspacePrebuilds FeatureName = "workspace_prebuilds"
8485
)
8586

8687
// FeatureNames must be kept in-sync with the Feature enum above.
@@ -103,6 +104,7 @@ var FeatureNames = []FeatureName{
103104
FeatureControlSharedPorts,
104105
FeatureCustomRoles,
105106
FeatureMultipleOrganizations,
107+
FeatureWorkspacePrebuilds,
106108
}
107109

108110
// Humanize returns the feature name in a human-readable format.
@@ -132,6 +134,7 @@ func (n FeatureName) AlwaysEnable() bool {
132134
FeatureHighAvailability: true,
133135
FeatureCustomRoles: true,
134136
FeatureMultipleOrganizations: true,
137+
FeatureWorkspacePrebuilds: true,
135138
}[n]
136139
}
137140

@@ -393,6 +396,7 @@ type DeploymentValues struct {
393396
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
394397
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
395398
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"`
399+
Prebuilds PrebuildsConfig `json:"workspace_prebuilds,omitempty" typescript:",notnull"`
396400

397401
Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
398402
WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"`
@@ -747,6 +751,10 @@ type NotificationsWebhookConfig struct {
747751
Endpoint serpent.URL `json:"endpoint" typescript:",notnull"`
748752
}
749753

754+
type PrebuildsConfig struct {
755+
ReconciliationInterval serpent.Duration `json:"reconciliation_interval" typescript:",notnull"`
756+
}
757+
750758
const (
751759
annotationFormatDuration = "format_duration"
752760
annotationEnterpriseKey = "enterprise"
@@ -977,6 +985,11 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
977985
Parent: &deploymentGroupNotifications,
978986
YAML: "webhook",
979987
}
988+
deploymentGroupPrebuilds = serpent.Group{
989+
Name: "Workspace Prebuilds",
990+
YAML: "workspace_prebuilds",
991+
Description: "Configure how workspace prebuilds behave.",
992+
}
980993
)
981994

982995
httpAddress := serpent.Option{
@@ -2897,6 +2910,16 @@ Write out the current server config as YAML to stdout.`,
28972910
Annotations: serpent.Annotations{}.Mark(annotationFormatDuration, "true"),
28982911
Hidden: true, // Hidden because most operators should not need to modify this.
28992912
},
2913+
{
2914+
Name: "Reconciliation Interval",
2915+
Description: "How often to reconcile workspace prebuilds state.",
2916+
Flag: "workspace-prebuilds-reconciliation-interval",
2917+
Env: "CODER_WORKSPACE_PREBUILDS_RECONCILIATION_INTERVAL",
2918+
Value: &c.Prebuilds.ReconciliationInterval,
2919+
Default: (time.Second * 15).String(),
2920+
Group: &deploymentGroupPrebuilds,
2921+
YAML: "reconciliation_interval",
2922+
},
29002923
}
29012924

29022925
return opts
@@ -3118,13 +3141,16 @@ const (
31183141
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
31193142
ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events.
31203143
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
3144+
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
31213145
)
31223146

31233147
// ExperimentsAll should include all experiments that are safe for
31243148
// users to opt-in to via --experimental='*'.
31253149
// Experiments that are not ready for consumption by all users should
31263150
// not be included here and will be essentially hidden.
3127-
var ExperimentsAll = Experiments{}
3151+
var ExperimentsAll = Experiments{
3152+
ExperimentWorkspacePrebuilds,
3153+
}
31283154

31293155
// Experiments is a list of experiments.
31303156
// Multiple experiments may be enabled at the same time.

docs/reference/api/schemas.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/coderd/coderd.go

+17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/ed25519"
66
"fmt"
7+
"github.com/coder/coder/v2/coderd/prebuilds"
78
"math"
89
"net/http"
910
"net/url"
@@ -581,6 +582,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
581582
}
582583
go api.runEntitlementsLoop(ctx)
583584

585+
if api.AGPL.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds) {
586+
if !api.Entitlements.Enabled(codersdk.FeatureWorkspacePrebuilds) {
587+
options.Logger.Warn(ctx, "prebuilds experiment enabled but not entitled to use")
588+
} else {
589+
api.prebuildsController = prebuilds.NewController(options.Database, options.Pubsub, options.DeploymentValues.Prebuilds, options.Logger.Named("prebuilds.controller"))
590+
go api.prebuildsController.Loop(ctx)
591+
}
592+
}
593+
584594
return api, nil
585595
}
586596

@@ -634,6 +644,8 @@ type API struct {
634644

635645
licenseMetricsCollector *license.MetricsCollector
636646
tailnetService *tailnet.ClientService
647+
648+
prebuildsController *prebuilds.Controller
637649
}
638650

639651
// writeEntitlementWarningsHeader writes the entitlement warnings to the response header
@@ -664,6 +676,11 @@ func (api *API) Close() error {
664676
if api.Options.CheckInactiveUsersCancelFunc != nil {
665677
api.Options.CheckInactiveUsersCancelFunc()
666678
}
679+
680+
if api.prebuildsController != nil {
681+
api.prebuildsController.Close(xerrors.New("api closed")) // TODO: determine root cause (requires changes up the stack, though).
682+
}
683+
667684
return api.AGPL.Close()
668685
}
669686

0 commit comments

Comments
 (0)