mirror of
https://github.com/coder/coder.git
synced 2025-07-29 10:35:52 +00:00
Hide prebuilds behind premium license & experiment
Signed-off-by: Danny Kopping <danny@coder.com>
This commit is contained in:
@@ -31,8 +31,6 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/prebuilds"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/coreos/go-systemd/daemon"
|
||||
@@ -943,11 +941,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
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.")
|
||||
}
|
||||
|
||||
// TODO: implement experiment and configs
|
||||
prebuildsCtrl := prebuilds.NewController(options.Database, options.Pubsub, logger.Named("prebuilds.controller"))
|
||||
go prebuildsCtrl.Loop(ctx)
|
||||
defer prebuildsCtrl.Stop()
|
||||
|
||||
// Since errCh only has one buffered slot, all routines
|
||||
// sending on it must be wrapped in a select/default to
|
||||
// avoid leaving dangling goroutines waiting for the
|
||||
|
7
coderd/apidoc/docs.go
generated
7
coderd/apidoc/docs.go
generated
@@ -11504,19 +11504,22 @@ const docTemplate = `{
|
||||
"example",
|
||||
"auto-fill-parameters",
|
||||
"notifications",
|
||||
"workspace-usage"
|
||||
"workspace-usage",
|
||||
"workspace-prebuilds"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
|
||||
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
|
||||
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentNotifications",
|
||||
"ExperimentWorkspaceUsage"
|
||||
"ExperimentWorkspaceUsage",
|
||||
"ExperimentWorkspacePrebuilds"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
|
7
coderd/apidoc/swagger.json
generated
7
coderd/apidoc/swagger.json
generated
@@ -10295,19 +10295,22 @@
|
||||
"example",
|
||||
"auto-fill-parameters",
|
||||
"notifications",
|
||||
"workspace-usage"
|
||||
"workspace-usage",
|
||||
"workspace-prebuilds"
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
|
||||
"ExperimentExample": "This isn't used for anything.",
|
||||
"ExperimentNotifications": "Sends notifications via SMTP and webhooks following certain events.",
|
||||
"ExperimentWorkspacePrebuilds": "Enables the new workspace prebuilds feature.",
|
||||
"ExperimentWorkspaceUsage": "Enables the new workspace usage tracking."
|
||||
},
|
||||
"x-enum-varnames": [
|
||||
"ExperimentExample",
|
||||
"ExperimentAutoFillParameters",
|
||||
"ExperimentNotifications",
|
||||
"ExperimentWorkspaceUsage"
|
||||
"ExperimentWorkspaceUsage",
|
||||
"ExperimentWorkspacePrebuilds"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuth": {
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"math"
|
||||
mrand "math/rand"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@@ -34,66 +35,68 @@ import (
|
||||
|
||||
type Controller struct {
|
||||
store database.Store
|
||||
cfg codersdk.PrebuildsConfig
|
||||
pubsub pubsub.Pubsub
|
||||
logger slog.Logger
|
||||
|
||||
nudgeCh chan *uuid.UUID
|
||||
closeCh chan struct{}
|
||||
logger slog.Logger
|
||||
nudgeCh chan *uuid.UUID
|
||||
cancelFn context.CancelCauseFunc
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func NewController(store database.Store, pubsub pubsub.Pubsub, logger slog.Logger) *Controller {
|
||||
func NewController(store database.Store, pubsub pubsub.Pubsub, cfg codersdk.PrebuildsConfig, logger slog.Logger) *Controller {
|
||||
return &Controller{
|
||||
store: store,
|
||||
pubsub: pubsub,
|
||||
logger: logger,
|
||||
cfg: cfg,
|
||||
nudgeCh: make(chan *uuid.UUID, 1),
|
||||
closeCh: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func (c Controller) Loop(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Second * 5) // TODO: configurable? 1m probably lowest valid value
|
||||
func (c *Controller) Loop(ctx context.Context) error {
|
||||
ticker := time.NewTicker(c.cfg.ReconciliationInterval.Value())
|
||||
defer ticker.Stop()
|
||||
|
||||
// TODO: create new authz role
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
ctx, cancel := context.WithCancelCause(dbauthz.AsSystemRestricted(ctx))
|
||||
c.cancelFn = cancel
|
||||
|
||||
// TODO: bounded concurrency?
|
||||
var eg errgroup.Group
|
||||
for {
|
||||
select {
|
||||
// Accept nudges from outside the control loop to trigger a new iteration.
|
||||
case template := <-c.nudgeCh:
|
||||
eg.Go(func() error {
|
||||
c.reconcile(ctx, template)
|
||||
return nil
|
||||
})
|
||||
c.reconcile(ctx, template)
|
||||
// Trigger a new iteration on each tick.
|
||||
case <-ticker.C:
|
||||
eg.Go(func() error {
|
||||
c.reconcile(ctx, nil)
|
||||
return nil
|
||||
})
|
||||
case <-c.closeCh:
|
||||
c.logger.Info(ctx, "control loop stopped")
|
||||
goto wait
|
||||
c.reconcile(ctx, nil)
|
||||
case <-ctx.Done():
|
||||
c.logger.Error(context.Background(), "control loop exited: %w", ctx.Err())
|
||||
goto wait
|
||||
c.logger.Error(context.Background(), "prebuilds reconciliation loop exited", slog.Error(ctx.Err()), slog.F("cause", context.Cause(ctx)))
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: no gotos
|
||||
wait:
|
||||
_ = eg.Wait()
|
||||
}
|
||||
|
||||
func (c Controller) ReconcileTemplate(templateID uuid.UUID) {
|
||||
func (c *Controller) Close(cause error) {
|
||||
if c.isClosed() {
|
||||
return
|
||||
}
|
||||
c.closed.Store(true)
|
||||
if c.cancelFn != nil {
|
||||
c.cancelFn(cause)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) isClosed() bool {
|
||||
return c.closed.Load()
|
||||
}
|
||||
|
||||
func (c *Controller) ReconcileTemplate(templateID uuid.UUID) {
|
||||
// TODO: replace this with pubsub listening
|
||||
c.nudgeCh <- &templateID
|
||||
}
|
||||
|
||||
func (c Controller) reconcile(ctx context.Context, templateID *uuid.UUID) {
|
||||
func (c *Controller) reconcile(ctx context.Context, templateID *uuid.UUID) {
|
||||
var logger slog.Logger
|
||||
if templateID == nil {
|
||||
logger = c.logger.With(slog.F("reconcile_context", "all"))
|
||||
@@ -167,7 +170,7 @@ type reconciliationActions struct {
|
||||
|
||||
// calculateActions MUST be called within the context of a transaction (TODO: isolation)
|
||||
// with an advisory lock to prevent TOCTOU races.
|
||||
func (c Controller) calculateActions(ctx context.Context, template database.Template, state database.GetTemplatePrebuildStateRow) (*reconciliationActions, error) {
|
||||
func (c *Controller) calculateActions(ctx context.Context, template database.Template, state database.GetTemplatePrebuildStateRow) (*reconciliationActions, error) {
|
||||
// TODO: align workspace states with how we represent them on the FE and the CLI
|
||||
// right now there's some slight differences which can lead to additional prebuilds being created
|
||||
|
||||
@@ -279,7 +282,7 @@ func (c Controller) calculateActions(ctx context.Context, template database.Temp
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func (c Controller) reconcileTemplate(ctx context.Context, template database.Template) error {
|
||||
func (c *Controller) reconcileTemplate(ctx context.Context, template database.Template) error {
|
||||
logger := c.logger.With(slog.F("template_id", template.ID.String()))
|
||||
|
||||
// get number of desired vs actual prebuild instances
|
||||
@@ -360,7 +363,7 @@ func (c Controller) reconcileTemplate(ctx context.Context, template database.Tem
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) createPrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
|
||||
func (c *Controller) createPrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
|
||||
name, err := generateName()
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
return c.provision(ctx, db, prebuildID, template, presetID, database.WorkspaceTransitionStart, workspace)
|
||||
}
|
||||
func (c Controller) deletePrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
|
||||
func (c *Controller) deletePrebuild(ctx context.Context, db database.Store, prebuildID uuid.UUID, template database.Template, presetID uuid.UUID) error {
|
||||
workspace, err := db.GetWorkspaceByID(ctx, prebuildID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace by ID: %w", err)
|
||||
@@ -406,7 +409,7 @@ func (c Controller) deletePrebuild(ctx context.Context, db database.Store, prebu
|
||||
return c.provision(ctx, db, prebuildID, template, presetID, database.WorkspaceTransitionDelete, workspace)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
tvp, err := db.GetPresetParametersByTemplateVersionID(ctx, template.ActiveVersionID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch preset details: %w", err)
|
||||
@@ -464,10 +467,6 @@ func (c Controller) provision(ctx context.Context, db database.Store, prebuildID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) Stop() {
|
||||
c.closeCh <- struct{}{}
|
||||
}
|
||||
|
||||
// generateName generates a 20-byte prebuild name which should safe to use without truncation in most situations.
|
||||
// UUIDs may be too long for a resource name in cloud providers (since this ID will be used in the prebuild's name).
|
||||
//
|
||||
|
@@ -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]
|
||||
}
|
||||
|
||||
@@ -393,6 +396,7 @@ type DeploymentValues struct {
|
||||
TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"`
|
||||
Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"`
|
||||
AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,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"`
|
||||
@@ -747,6 +751,10 @@ type NotificationsWebhookConfig struct {
|
||||
Endpoint serpent.URL `json:"endpoint" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type PrebuildsConfig struct {
|
||||
ReconciliationInterval serpent.Duration `json:"reconciliation_interval" typescript:",notnull"`
|
||||
}
|
||||
|
||||
const (
|
||||
annotationFormatDuration = "format_duration"
|
||||
annotationEnterpriseKey = "enterprise"
|
||||
@@ -977,6 +985,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.",
|
||||
}
|
||||
)
|
||||
|
||||
httpAddress := serpent.Option{
|
||||
@@ -2897,6 +2910,16 @@ 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.
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
return opts
|
||||
@@ -3118,13 +3141,16 @@ const (
|
||||
ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature.
|
||||
ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events.
|
||||
ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking.
|
||||
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
|
||||
)
|
||||
|
||||
// ExperimentsAll 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 ExperimentsAll = Experiments{}
|
||||
var ExperimentsAll = Experiments{
|
||||
ExperimentWorkspacePrebuilds,
|
||||
}
|
||||
|
||||
// Experiments is a list of experiments.
|
||||
// Multiple experiments may be enabled at the same time.
|
||||
|
1
docs/reference/api/schemas.md
generated
1
docs/reference/api/schemas.md
generated
@@ -2789,6 +2789,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| `auto-fill-parameters` |
|
||||
| `notifications` |
|
||||
| `workspace-usage` |
|
||||
| `workspace-prebuilds` |
|
||||
|
||||
## codersdk.ExternalAuth
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"github.com/coder/coder/v2/coderd/prebuilds"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -581,6 +582,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
}
|
||||
go api.runEntitlementsLoop(ctx)
|
||||
|
||||
if api.AGPL.Experiments.Enabled(codersdk.ExperimentWorkspacePrebuilds) {
|
||||
if !api.Entitlements.Enabled(codersdk.FeatureWorkspacePrebuilds) {
|
||||
options.Logger.Warn(ctx, "prebuilds experiment enabled but not entitled to use")
|
||||
} else {
|
||||
api.prebuildsController = prebuilds.NewController(options.Database, options.Pubsub, options.DeploymentValues.Prebuilds, options.Logger.Named("prebuilds.controller"))
|
||||
go api.prebuildsController.Loop(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
@@ -634,6 +644,8 @@ type API struct {
|
||||
|
||||
licenseMetricsCollector *license.MetricsCollector
|
||||
tailnetService *tailnet.ClientService
|
||||
|
||||
prebuildsController *prebuilds.Controller
|
||||
}
|
||||
|
||||
// writeEntitlementWarningsHeader writes the entitlement warnings to the response header
|
||||
@@ -664,6 +676,11 @@ func (api *API) Close() error {
|
||||
if api.Options.CheckInactiveUsersCancelFunc != nil {
|
||||
api.Options.CheckInactiveUsersCancelFunc()
|
||||
}
|
||||
|
||||
if api.prebuildsController != nil {
|
||||
api.prebuildsController.Close(xerrors.New("api closed")) // TODO: determine root cause (requires changes up the stack, though).
|
||||
}
|
||||
|
||||
return api.AGPL.Close()
|
||||
}
|
||||
|
||||
|
9
site/src/api/typesGenerated.ts
generated
9
site/src/api/typesGenerated.ts
generated
@@ -668,6 +668,7 @@ export interface DeploymentValues {
|
||||
readonly terms_of_service_url?: string;
|
||||
readonly notifications?: NotificationsConfig;
|
||||
readonly additional_csp_policy?: string;
|
||||
readonly workspace_prebuilds?: PrebuildsConfig;
|
||||
readonly config?: string;
|
||||
readonly write_config?: boolean;
|
||||
readonly address?: string;
|
||||
@@ -735,6 +736,7 @@ export type Experiment =
|
||||
| "auto-fill-parameters"
|
||||
| "example"
|
||||
| "notifications"
|
||||
| "workspace-prebuilds"
|
||||
| "workspace-usage";
|
||||
|
||||
// From codersdk/deployment.go
|
||||
@@ -849,6 +851,7 @@ export type FeatureName =
|
||||
| "user_limit"
|
||||
| "user_role_management"
|
||||
| "workspace_batch_actions"
|
||||
| "workspace_prebuilds"
|
||||
| "workspace_proxy";
|
||||
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
@@ -869,6 +872,7 @@ export const FeatureNames: FeatureName[] = [
|
||||
"user_limit",
|
||||
"user_role_management",
|
||||
"workspace_batch_actions",
|
||||
"workspace_prebuilds",
|
||||
"workspace_proxy",
|
||||
];
|
||||
|
||||
@@ -1554,6 +1558,11 @@ export interface PprofConfig {
|
||||
readonly address: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface PrebuildsConfig {
|
||||
readonly reconciliation_interval: number;
|
||||
}
|
||||
|
||||
// From codersdk/presets.go
|
||||
export interface Preset {
|
||||
readonly ID: string;
|
||||
|
Reference in New Issue
Block a user