mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add template setting to require active template version (#10277)
This commit is contained in:
+1
-1
@@ -940,7 +940,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
autobuildTicker := time.NewTicker(vals.AutobuildPollInterval.Value())
|
||||
defer autobuildTicker.Stop()
|
||||
autobuildExecutor := autobuild.NewExecutor(
|
||||
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, logger, autobuildTicker.C)
|
||||
ctx, options.Database, options.Pubsub, coderAPI.TemplateScheduleStore, &coderAPI.Auditor, coderAPI.AccessControlStore, logger, autobuildTicker.C)
|
||||
autobuildExecutor.Run()
|
||||
|
||||
hangDetectorTicker := time.NewTicker(vals.JobHangDetectorInterval.Value())
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/pretty"
|
||||
|
||||
"github.com/coder/coder/v2/cli/clibase"
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
|
||||
Generated
+8
@@ -7816,6 +7816,10 @@ const docTemplate = `{
|
||||
"description": "Name is the name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"template_version_id": {
|
||||
"description": "VersionID is an in-progress or completed job to use as an initial version\nof the template.\n\nThis is required on creation to enable a user-flow of validating a\ntemplate works. There is no reason the data-model cannot support empty\ntemplates, but it doesn't make sense for users.",
|
||||
"type": "string",
|
||||
@@ -9994,6 +9998,10 @@ const docTemplate = `{
|
||||
"terraform"
|
||||
]
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time_til_dormant_autodelete_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
Generated
+8
@@ -6969,6 +6969,10 @@
|
||||
"description": "Name is the name of the template.",
|
||||
"type": "string"
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"template_version_id": {
|
||||
"description": "VersionID is an in-progress or completed job to use as an initial version\nof the template.\n\nThis is required on creation to enable a user-flow of validating a\ntemplate works. There is no reason the data-model cannot support empty\ntemplates, but it doesn't make sense for users.",
|
||||
"type": "string",
|
||||
@@ -9028,6 +9032,10 @@
|
||||
"type": "string",
|
||||
"enum": ["terraform"]
|
||||
},
|
||||
"require_active_version": {
|
||||
"description": "RequireActiveVersion mandates that workspaces are built with the active\ntemplate version.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"time_til_dormant_autodelete_ms": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@ type Executor struct {
|
||||
db database.Store
|
||||
ps pubsub.Pubsub
|
||||
templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
accessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
auditor *atomic.Pointer[audit.Auditor]
|
||||
log slog.Logger
|
||||
tick <-chan time.Time
|
||||
@@ -46,7 +47,7 @@ type Stats struct {
|
||||
}
|
||||
|
||||
// New returns a new wsactions executor.
|
||||
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], log slog.Logger, tick <-chan time.Time) *Executor {
|
||||
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time) *Executor {
|
||||
le := &Executor{
|
||||
//nolint:gocritic // Autostart has a limited set of permissions.
|
||||
ctx: dbauthz.AsAutostart(ctx),
|
||||
@@ -56,6 +57,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, tss *
|
||||
tick: tick,
|
||||
log: log.Named("autobuild"),
|
||||
auditor: auditor,
|
||||
accessControlStore: acs,
|
||||
}
|
||||
return le
|
||||
}
|
||||
@@ -159,6 +161,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return nil
|
||||
}
|
||||
|
||||
template, err := tx.GetTemplateByID(e.ctx, ws.TemplateID)
|
||||
if err != nil {
|
||||
log.Warn(e.ctx, "get template by id", slog.Error(err))
|
||||
}
|
||||
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
|
||||
|
||||
latestJob, err := tx.GetProvisionerJobByID(e.ctx, latestBuild.JobID)
|
||||
if err != nil {
|
||||
log.Warn(e.ctx, "get last provisioner job for workspace %q: %w", slog.Error(err))
|
||||
@@ -179,7 +187,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
Reason(reason)
|
||||
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
|
||||
if nextTransition == database.WorkspaceTransitionStart &&
|
||||
ws.AutomaticUpdates == database.AutomaticUpdatesAlways {
|
||||
useActiveVersion(accessControl, ws) {
|
||||
log.Debug(e.ctx, "autostarting with active version")
|
||||
builder = builder.ActiveVersion()
|
||||
}
|
||||
@@ -470,3 +478,7 @@ func auditBuild(ctx context.Context, log slog.Logger, auditor audit.Auditor, par
|
||||
AdditionalFields: raw,
|
||||
})
|
||||
}
|
||||
|
||||
func useActiveVersion(opts dbauthz.TemplateAccessControl, ws database.Workspace) bool {
|
||||
return opts.RequireActiveVersion || ws.AutomaticUpdates == database.AutomaticUpdatesAlways
|
||||
}
|
||||
|
||||
@@ -783,6 +783,56 @@ func TestExecutorAutostopTemplateDisabled(t *testing.T) {
|
||||
assert.Len(t, stats.Transitions, 0)
|
||||
}
|
||||
|
||||
// Test that an AGPL AccessControlStore properly disables
|
||||
// functionality.
|
||||
func TestExecutorRequireActiveVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
|
||||
ownerClient = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statCh,
|
||||
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
|
||||
})
|
||||
)
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
|
||||
// Create an active and inactive template version. We'll
|
||||
// build a regular member's workspace using a non-active
|
||||
// template version and assert that the field is not abided
|
||||
// since there is no enterprise license.
|
||||
activeVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, activeVersion.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.RequireActiveVersion = true
|
||||
ctr.VersionID = activeVersion.ID
|
||||
})
|
||||
inactiveVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, activeVersion.ID)
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
ws := coderdtest.CreateWorkspace(t, memberClient, owner.OrganizationID, uuid.Nil, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.TemplateVersionID = inactiveVersion.ID
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
||||
req.TemplateVersionID = inactiveVersion.ID
|
||||
})
|
||||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||||
ticker <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
stats := <-statCh
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
|
||||
ws = coderdtest.MustWorkspace(t, memberClient, ws.ID)
|
||||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||||
}
|
||||
|
||||
// TestExecutorFailedWorkspace test AGPL functionality which mainly
|
||||
// ensures that autostop actions as a result of a failed workspace
|
||||
// build do not trigger.
|
||||
|
||||
@@ -131,6 +131,7 @@ type Options struct {
|
||||
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
|
||||
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
|
||||
// workspace applications. It consists of both a signing and encryption key.
|
||||
AppSecurityKey workspaceapps.SecurityKey
|
||||
@@ -208,11 +209,20 @@ func New(options *Options) *API {
|
||||
if options.Authorizer == nil {
|
||||
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
|
||||
}
|
||||
|
||||
if options.AccessControlStore == nil {
|
||||
options.AccessControlStore = &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var tacs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
|
||||
options.AccessControlStore.Store(&tacs)
|
||||
}
|
||||
|
||||
options.Database = dbauthz.New(
|
||||
options.Database,
|
||||
options.Authorizer,
|
||||
options.Logger.Named("authz_querier"),
|
||||
options.AccessControlStore,
|
||||
)
|
||||
|
||||
experiments := ReadExperiments(
|
||||
options.Logger, options.DeploymentValues.Experiments.Value(),
|
||||
)
|
||||
@@ -369,6 +379,7 @@ func New(options *Options) *API {
|
||||
Auditor: atomic.Pointer[audit.Auditor]{},
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
|
||||
AccessControlStore: options.AccessControlStore,
|
||||
Experiments: experiments,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthcheck.Report]{},
|
||||
Acquirer: provisionerdserver.NewAcquirer(
|
||||
@@ -1008,6 +1019,9 @@ type API struct {
|
||||
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
|
||||
// DERPMapper mutates the DERPMap to include workspace proxies.
|
||||
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]
|
||||
|
||||
HTTPAuth *HTTPAuthorizer
|
||||
|
||||
|
||||
@@ -218,7 +218,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
|
||||
if options.Database == nil {
|
||||
options.Database, options.Pubsub = dbtestutil.NewDB(t)
|
||||
options.Database = dbauthz.New(options.Database, options.Authorizer, options.Logger.Leveled(slog.LevelDebug))
|
||||
}
|
||||
|
||||
// Some routes expect a deployment ID, so just make sure one exists.
|
||||
@@ -260,6 +259,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
t.Cleanup(closeBatcher)
|
||||
}
|
||||
|
||||
accessControlStore := &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var acs dbauthz.AccessControlStore = dbauthz.AGPLTemplateAccessControlStore{}
|
||||
accessControlStore.Store(&acs)
|
||||
|
||||
var templateScheduleStore atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
if options.TemplateScheduleStore == nil {
|
||||
options.TemplateScheduleStore = schedule.NewAGPLTemplateScheduleStore()
|
||||
@@ -279,6 +282,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
options.Pubsub,
|
||||
&templateScheduleStore,
|
||||
&auditor,
|
||||
accessControlStore,
|
||||
*options.Logger,
|
||||
options.AutobuildTicker,
|
||||
).WithStatsChannel(options.AutobuildStats)
|
||||
@@ -416,6 +420,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
|
||||
Authorizer: options.Authorizer,
|
||||
Telemetry: telemetry.NewNoop(),
|
||||
TemplateScheduleStore: &templateScheduleStore,
|
||||
AccessControlStore: accessControlStore,
|
||||
TLSCertificates: options.TLSCertificates,
|
||||
TrialGenerator: options.TrialGenerator,
|
||||
TailnetCoordinator: options.Coordinator,
|
||||
@@ -915,7 +920,7 @@ func CreateWorkspace(t testing.TB, client *codersdk.Client, organization uuid.UU
|
||||
}
|
||||
|
||||
// TransitionWorkspace is a convenience method for transitioning a workspace from one state to another.
|
||||
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition) codersdk.Workspace {
|
||||
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition, muts ...func(req *codersdk.CreateWorkspaceBuildRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
workspace, err := client.Workspace(ctx, workspaceID)
|
||||
@@ -925,10 +930,16 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
|
||||
template, err := client.Template(ctx, workspace.TemplateID)
|
||||
require.NoError(t, err, "fetch workspace template")
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
req := codersdk.CreateWorkspaceBuildRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Transition: codersdk.WorkspaceTransition(to),
|
||||
})
|
||||
}
|
||||
|
||||
for _, mut := range muts {
|
||||
mut(&req)
|
||||
}
|
||||
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, req)
|
||||
require.NoError(t, err, "unexpected error transitioning workspace to %s", to)
|
||||
|
||||
_ = AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package dbauthz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
// AccessControlStore fetches access control-related configuration
|
||||
// that is used when determining whether an actor is authorized
|
||||
// to interact with an RBAC object.
|
||||
type AccessControlStore interface {
|
||||
GetTemplateAccessControl(t database.Template) TemplateAccessControl
|
||||
SetTemplateAccessControl(ctx context.Context, store database.Store, id uuid.UUID, opts TemplateAccessControl) error
|
||||
}
|
||||
|
||||
type TemplateAccessControl struct {
|
||||
RequireActiveVersion bool
|
||||
}
|
||||
|
||||
// AGPLTemplateAccessControlStore always returns the defaults for access control
|
||||
// settings.
|
||||
type AGPLTemplateAccessControlStore struct{}
|
||||
|
||||
var _ AccessControlStore = AGPLTemplateAccessControlStore{}
|
||||
|
||||
func (AGPLTemplateAccessControlStore) GetTemplateAccessControl(database.Template) TemplateAccessControl {
|
||||
return TemplateAccessControl{
|
||||
RequireActiveVersion: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (AGPLTemplateAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, TemplateAccessControl) error {
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -101,9 +102,10 @@ type querier struct {
|
||||
db database.Store
|
||||
auth rbac.Authorizer
|
||||
log slog.Logger
|
||||
acs *atomic.Pointer[AccessControlStore]
|
||||
}
|
||||
|
||||
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) database.Store {
|
||||
func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger, acs *atomic.Pointer[AccessControlStore]) database.Store {
|
||||
// If the underlying db store is already a querier, return it.
|
||||
// Do not double wrap.
|
||||
if slices.Contains(db.Wrappers(), wrapname) {
|
||||
@@ -113,6 +115,7 @@ func New(db database.Store, authorizer rbac.Authorizer, logger slog.Logger) data
|
||||
db: db,
|
||||
auth: authorizer,
|
||||
log: logger,
|
||||
acs: acs,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,7 +510,7 @@ func (q *querier) Ping(ctx context.Context) (time.Duration, error) {
|
||||
func (q *querier) InTx(function func(querier database.Store) error, txOpts *sql.TxOptions) error {
|
||||
return q.db.InTx(func(tx database.Store) error {
|
||||
// Wrap the transaction store in a querier.
|
||||
wrapped := New(tx, q.auth, q.log)
|
||||
wrapped := New(tx, q.auth, q.log, q.acs)
|
||||
return function(wrapped)
|
||||
}, txOpts)
|
||||
}
|
||||
@@ -2200,7 +2203,7 @@ func (q *querier) InsertWorkspaceAppStats(ctx context.Context, arg database.Inse
|
||||
func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertWorkspaceBuildParams) error {
|
||||
w, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
return xerrors.Errorf("get workspace by id: %w", err)
|
||||
}
|
||||
|
||||
var action rbac.Action = rbac.ActionUpdate
|
||||
@@ -2209,7 +2212,28 @@ func (q *querier) InsertWorkspaceBuild(ctx context.Context, arg database.InsertW
|
||||
}
|
||||
|
||||
if err = q.authorizeContext(ctx, action, w.WorkspaceBuildRBAC(arg.Transition)); err != nil {
|
||||
return err
|
||||
return xerrors.Errorf("authorize context: %w", err)
|
||||
}
|
||||
|
||||
// If we're starting a workspace we need to check the template.
|
||||
if arg.Transition == database.WorkspaceTransitionStart {
|
||||
t, err := q.db.GetTemplateByID(ctx, w.TemplateID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by id: %w", err)
|
||||
}
|
||||
|
||||
accessControl := (*q.acs.Load()).GetTemplateAccessControl(t)
|
||||
|
||||
// If the template requires the active version we need to check if
|
||||
// the user is a template admin. If they aren't and are attempting
|
||||
// to use a non-active version then we must fail the request.
|
||||
if accessControl.RequireActiveVersion {
|
||||
if arg.TemplateVersionID != t.ActiveVersionID {
|
||||
if err = q.authorizeContext(ctx, rbac.ActionUpdate, t); err != nil {
|
||||
return xerrors.Errorf("cannot use non-active version: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return q.db.InsertWorkspaceBuild(ctx, arg)
|
||||
@@ -2442,6 +2466,13 @@ func (q *querier) UpdateTemplateACLByID(ctx context.Context, arg database.Update
|
||||
return fetchAndExec(q.log, q.auth, rbac.ActionCreate, fetch, q.db.UpdateTemplateACLByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTemplateAccessControlByID(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) (database.Template, error) {
|
||||
return q.db.GetTemplateByID(ctx, arg.ID)
|
||||
}
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateTemplateAccessControlByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) (database.Template, error) {
|
||||
return q.db.GetTemplateByID(ctx, arg.ID)
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAsNoActor(t *testing.T) {
|
||||
@@ -61,7 +62,7 @@ func TestAsNoActor(t *testing.T) {
|
||||
func TestPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{}, slog.Make())
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{}, slog.Make(), accessControlStorePointer())
|
||||
_, err := q.Ping(context.Background())
|
||||
require.NoError(t, err, "must not error")
|
||||
}
|
||||
@@ -73,7 +74,7 @@ func TestInTX(t *testing.T) {
|
||||
db := dbfake.New()
|
||||
q := dbauthz.New(db, &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: xerrors.New("custom error")},
|
||||
}, slog.Make())
|
||||
}, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@@ -109,8 +110,8 @@ func TestNew(t *testing.T) {
|
||||
|
||||
// Double wrap should not cause an actual double wrap. So only 1 rbac call
|
||||
// should be made.
|
||||
az := dbauthz.New(db, rec, slog.Make())
|
||||
az = dbauthz.New(az, rec, slog.Make())
|
||||
az := dbauthz.New(db, rec, slog.Make(), accessControlStorePointer())
|
||||
az = dbauthz.New(az, rec, slog.Make(), accessControlStorePointer())
|
||||
|
||||
w, err := az.GetWorkspaceByID(ctx, exp.ID)
|
||||
require.NoError(t, err, "must not error")
|
||||
@@ -127,7 +128,7 @@ func TestDBAuthzRecursive(t *testing.T) {
|
||||
t.Parallel()
|
||||
q := dbauthz.New(dbfake.New(), &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
|
||||
}, slog.Make())
|
||||
}, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@@ -1213,13 +1214,67 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
}).Asserts(rbac.ResourceWorkspace.WithOwner(u.ID.String()).InOrg(o.ID), rbac.ActionCreate)
|
||||
}))
|
||||
s.Run("Start/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
t := dbgen.Template(s.T(), db, database.Template{})
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
}).Asserts(w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate)
|
||||
}))
|
||||
s.Run("Start/RequireActiveVersion/VersionMismatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
t := dbgen.Template(s.T(), db, database.Template{})
|
||||
ctx := testutil.Context(s.T(), testutil.WaitShort)
|
||||
err := db.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
|
||||
ID: t.ID,
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(s.T(), err)
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
|
||||
TemplateID: uuid.NullUUID{UUID: t.ID},
|
||||
})
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
TemplateVersionID: v.ID,
|
||||
}).Asserts(
|
||||
w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate,
|
||||
t, rbac.ActionUpdate,
|
||||
)
|
||||
}))
|
||||
s.Run("Start/RequireActiveVersion/VersionsMatch/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
v := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{})
|
||||
t := dbgen.Template(s.T(), db, database.Template{
|
||||
ActiveVersionID: v.ID,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(s.T(), testutil.WaitShort)
|
||||
err := db.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
|
||||
ID: t.ID,
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{
|
||||
TemplateID: t.ID,
|
||||
})
|
||||
// Assert that we do not check for template update permissions
|
||||
// if versions match.
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
WorkspaceID: w.ID,
|
||||
Transition: database.WorkspaceTransitionStart,
|
||||
Reason: database.BuildReasonInitiator,
|
||||
TemplateVersionID: v.ID,
|
||||
}).Asserts(
|
||||
w.WorkspaceBuildRBAC(database.WorkspaceTransitionStart), rbac.ActionUpdate,
|
||||
)
|
||||
}))
|
||||
s.Run("Delete/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {
|
||||
w := dbgen.Workspace(s.T(), db, database.Workspace{})
|
||||
check.Args(database.InsertWorkspaceBuildParams{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
@@ -59,7 +60,7 @@ func (s *MethodTestSuite) SetupSuite() {
|
||||
mockStore := dbmock.NewMockStore(ctrl)
|
||||
// We intentionally set no expectations apart from this.
|
||||
mockStore.EXPECT().Wrappers().Return([]string{}).AnyTimes()
|
||||
az := dbauthz.New(mockStore, nil, slog.Make())
|
||||
az := dbauthz.New(mockStore, nil, slog.Make(), accessControlStorePointer())
|
||||
// Take the underlying type of the interface.
|
||||
azt := reflect.TypeOf(az).Elem()
|
||||
s.methodAccounting = make(map[string]int)
|
||||
@@ -110,7 +111,7 @@ func (s *MethodTestSuite) Subtest(testCaseF func(db database.Store, check *expec
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: fakeAuthorizer,
|
||||
}
|
||||
az := dbauthz.New(db, rec, slog.Make())
|
||||
az := dbauthz.New(db, rec, slog.Make(), accessControlStorePointer())
|
||||
actor := rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleOwner()},
|
||||
@@ -398,3 +399,22 @@ func (emptyPreparedAuthorized) Authorize(_ context.Context, _ rbac.Object) error
|
||||
func (emptyPreparedAuthorized) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func accessControlStorePointer() *atomic.Pointer[dbauthz.AccessControlStore] {
|
||||
acs := &atomic.Pointer[dbauthz.AccessControlStore]{}
|
||||
var tacs dbauthz.AccessControlStore = fakeAccessControlStore{}
|
||||
acs.Store(&tacs)
|
||||
return acs
|
||||
}
|
||||
|
||||
type fakeAccessControlStore struct{}
|
||||
|
||||
func (fakeAccessControlStore) GetTemplateAccessControl(t database.Template) dbauthz.TemplateAccessControl {
|
||||
return dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: t.RequireActiveVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (fakeAccessControlStore) SetTemplateAccessControl(context.Context, database.Store, uuid.UUID, dbauthz.TemplateAccessControl) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
@@ -5643,6 +5643,25 @@ func (q *FakeQuerier) UpdateTemplateACLByID(_ context.Context, arg database.Upda
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateTemplateAccessControlByID(_ context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for idx, tpl := range q.templates {
|
||||
if tpl.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
q.templates[idx].RequireActiveVersion = arg.RequireActiveVersion
|
||||
return nil
|
||||
}
|
||||
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) UpdateTemplateActiveVersionByID(_ context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1523,6 +1523,13 @@ func (m metricsStore) UpdateTemplateACLByID(ctx context.Context, arg database.Up
|
||||
return err
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateTemplateAccessControlByID(ctx context.Context, arg database.UpdateTemplateAccessControlByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateTemplateAccessControlByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateTemplateAccessControlByID").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) UpdateTemplateActiveVersionByID(ctx context.Context, arg database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
start := time.Now()
|
||||
err := m.s.UpdateTemplateActiveVersionByID(ctx, arg)
|
||||
|
||||
@@ -3213,6 +3213,20 @@ func (mr *MockStoreMockRecorder) UpdateTemplateACLByID(arg0, arg1 interface{}) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateACLByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateACLByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateTemplateAccessControlByID mocks base method.
|
||||
func (m *MockStore) UpdateTemplateAccessControlByID(arg0 context.Context, arg1 database.UpdateTemplateAccessControlByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTemplateAccessControlByID", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateTemplateAccessControlByID indicates an expected call of UpdateTemplateAccessControlByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateTemplateAccessControlByID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateAccessControlByID", reflect.TypeOf((*MockStore)(nil).UpdateTemplateAccessControlByID), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateTemplateActiveVersionByID mocks base method.
|
||||
func (m *MockStore) UpdateTemplateActiveVersionByID(arg0 context.Context, arg1 database.UpdateTemplateActiveVersionByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+3
-1
@@ -755,7 +755,8 @@ CREATE TABLE templates (
|
||||
time_til_dormant_autodelete bigint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_days_of_week smallint DEFAULT 0 NOT NULL,
|
||||
autostop_requirement_weeks bigint DEFAULT 0 NOT NULL,
|
||||
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL
|
||||
autostart_block_days_of_week smallint DEFAULT 0 NOT NULL,
|
||||
require_active_version boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN templates.default_ttl IS 'The default duration for autostop for workspaces created from this template.';
|
||||
@@ -800,6 +801,7 @@ CREATE VIEW template_with_users AS
|
||||
templates.autostop_requirement_days_of_week,
|
||||
templates.autostop_requirement_weeks,
|
||||
templates.autostart_block_days_of_week,
|
||||
templates.require_active_version,
|
||||
COALESCE(visible_users.avatar_url, ''::text) AS created_by_avatar_url,
|
||||
COALESCE(visible_users.username, ''::text) AS created_by_username
|
||||
FROM (public.templates
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
BEGIN;
|
||||
|
||||
-- Update the template_with_users view;
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates DROP COLUMN require_active_version;
|
||||
|
||||
-- If you need to update this view, put 'DROP VIEW template_with_users;' before this.
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,23 @@
|
||||
BEGIN;
|
||||
|
||||
DROP VIEW template_with_users;
|
||||
|
||||
ALTER TABLE templates ADD COLUMN require_active_version boolean NOT NULL DEFAULT 'f';
|
||||
|
||||
CREATE VIEW
|
||||
template_with_users
|
||||
AS
|
||||
SELECT
|
||||
templates.*,
|
||||
coalesce(visible_users.avatar_url, '') AS created_by_avatar_url,
|
||||
coalesce(visible_users.username, '') AS created_by_username
|
||||
FROM
|
||||
templates
|
||||
LEFT JOIN
|
||||
visible_users
|
||||
ON
|
||||
templates.created_by = visible_users.id;
|
||||
|
||||
COMMENT ON VIEW template_with_users IS 'Joins in the username + avatar url of the created by user.';
|
||||
|
||||
COMMIT;
|
||||
@@ -179,7 +179,7 @@ func (w Workspace) ApplicationConnectRBAC() rbac.Object {
|
||||
}
|
||||
|
||||
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
|
||||
// If a workspace is locked it cannot be built.
|
||||
// If a workspace is dormant it cannot be built.
|
||||
// However we need to allow stopping a workspace by a caller once a workspace
|
||||
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
|
||||
// a locked workspace, they shouldn't have to have it unlocked first.
|
||||
|
||||
@@ -86,6 +86,7 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
|
||||
@@ -1892,6 +1892,7 @@ type Template struct {
|
||||
AutostopRequirementDaysOfWeek int16 `db:"autostop_requirement_days_of_week" json:"autostop_requirement_days_of_week"`
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
CreatedByAvatarURL sql.NullString `db:"created_by_avatar_url" json:"created_by_avatar_url"`
|
||||
CreatedByUsername string `db:"created_by_username" json:"created_by_username"`
|
||||
}
|
||||
@@ -1930,6 +1931,7 @@ type TemplateTable struct {
|
||||
AutostopRequirementWeeks int64 `db:"autostop_requirement_weeks" json:"autostop_requirement_weeks"`
|
||||
// A bitmap of days of week that autostart of a workspace is not allowed. Default allows all days. This is intended as a cost savings measure to prevent auto start on weekends (for example).
|
||||
AutostartBlockDaysOfWeek int16 `db:"autostart_block_days_of_week" json:"autostart_block_days_of_week"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
}
|
||||
|
||||
// Joins in the username + avatar url of the created by user.
|
||||
|
||||
@@ -301,6 +301,7 @@ type sqlcQuerier interface {
|
||||
UpdateProvisionerJobWithCompleteByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteByIDParams) error
|
||||
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
|
||||
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error
|
||||
UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error
|
||||
UpdateTemplateActiveVersionByID(ctx context.Context, arg UpdateTemplateActiveVersionByIDParams) error
|
||||
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
|
||||
UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error
|
||||
|
||||
@@ -4726,7 +4726,7 @@ func (q *sqlQuerier) GetTemplateAverageBuildTime(ctx context.Context, arg GetTem
|
||||
|
||||
const getTemplateByID = `-- name: GetTemplateByID :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users
|
||||
WHERE
|
||||
@@ -4764,6 +4764,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@@ -4772,7 +4773,7 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
|
||||
|
||||
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@@ -4818,6 +4819,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
)
|
||||
@@ -4825,7 +4827,7 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
|
||||
}
|
||||
|
||||
const getTemplates = `-- name: GetTemplates :many
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username FROM template_with_users AS templates
|
||||
ORDER BY (name, id) ASC
|
||||
`
|
||||
|
||||
@@ -4864,6 +4866,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@@ -4882,7 +4885,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
|
||||
|
||||
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
|
||||
SELECT
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, created_by_avatar_url, created_by_username
|
||||
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, default_ttl, created_by, icon, user_acl, group_acl, display_name, allow_user_cancel_workspace_jobs, max_ttl, allow_user_autostart, allow_user_autostop, failure_ttl, time_til_dormant, time_til_dormant_autodelete, autostop_requirement_days_of_week, autostop_requirement_weeks, autostart_block_days_of_week, require_active_version, created_by_avatar_url, created_by_username
|
||||
FROM
|
||||
template_with_users AS templates
|
||||
WHERE
|
||||
@@ -4958,6 +4961,7 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
|
||||
&i.AutostopRequirementDaysOfWeek,
|
||||
&i.AutostopRequirementWeeks,
|
||||
&i.AutostartBlockDaysOfWeek,
|
||||
&i.RequireActiveVersion,
|
||||
&i.CreatedByAvatarURL,
|
||||
&i.CreatedByUsername,
|
||||
); err != nil {
|
||||
@@ -5054,6 +5058,25 @@ func (q *sqlQuerier) UpdateTemplateACLByID(ctx context.Context, arg UpdateTempla
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateAccessControlByID = `-- name: UpdateTemplateAccessControlByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
SET
|
||||
require_active_version = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateTemplateAccessControlByIDParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
RequireActiveVersion bool `db:"require_active_version" json:"require_active_version"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateTemplateAccessControlByID, arg.ID, arg.RequireActiveVersion)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateTemplateActiveVersionByID = `-- name: UpdateTemplateActiveVersionByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
|
||||
@@ -169,3 +169,12 @@ SELECT
|
||||
coalesce((PERCENTILE_DISC(0.95) WITHIN GROUP(ORDER BY exec_time_sec) FILTER (WHERE transition = 'delete')), -1)::FLOAT AS delete_95
|
||||
FROM build_times
|
||||
;
|
||||
|
||||
-- name: UpdateTemplateAccessControlByID :exec
|
||||
UPDATE
|
||||
templates
|
||||
SET
|
||||
require_active_version = $2
|
||||
WHERE
|
||||
id = $1
|
||||
;
|
||||
|
||||
+21
-1
@@ -347,6 +347,15 @@ func (api *API) postTemplateByOrganization(rw http.ResponseWriter, r *http.Reque
|
||||
return xerrors.Errorf("insert template: %s", err)
|
||||
}
|
||||
|
||||
if createTemplate.RequireActiveVersion {
|
||||
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, id, dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: createTemplate.RequireActiveVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template access control: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
dbTemplate, err = tx.GetTemplateByID(ctx, id)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get template by id: %s", err)
|
||||
@@ -614,7 +623,8 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
req.AutostopRequirement.Weeks == scheduleOpts.AutostopRequirement.Weeks &&
|
||||
req.FailureTTLMillis == time.Duration(template.FailureTTL).Milliseconds() &&
|
||||
req.TimeTilDormantMillis == time.Duration(template.TimeTilDormant).Milliseconds() &&
|
||||
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() {
|
||||
req.TimeTilDormantAutoDeleteMillis == time.Duration(template.TimeTilDormantAutoDelete).Milliseconds() &&
|
||||
req.RequireActiveVersion == template.RequireActiveVersion {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -638,6 +648,15 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
|
||||
return xerrors.Errorf("update template metadata: %w", err)
|
||||
}
|
||||
|
||||
if template.RequireActiveVersion != req.RequireActiveVersion {
|
||||
err = (*api.AccessControlStore.Load()).SetTemplateAccessControl(ctx, tx, template.ID, dbauthz.TemplateAccessControl{
|
||||
RequireActiveVersion: req.RequireActiveVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("set template access control: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
updated, err = tx.GetTemplateByID(ctx, template.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch updated template metadata: %w", err)
|
||||
@@ -824,5 +843,6 @@ func (api *API) convertTemplate(
|
||||
AutostartRequirement: codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: codersdk.BitmapToWeekdays(template.AutostartAllowedDays()),
|
||||
},
|
||||
RequireActiveVersion: template.RequireActiveVersion,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
@@ -216,29 +215,19 @@ func (b *Builder) Build(
|
||||
// RepeatableRead isolation ensures that we get a consistent view of the database while
|
||||
// computing the new build. This simplifies the logic so that we do not need to worry if
|
||||
// later reads are consistent with earlier ones.
|
||||
for retries := 0; retries < 5; retries++ {
|
||||
var workspaceBuild *database.WorkspaceBuild
|
||||
var provisionerJob *database.ProvisionerJob
|
||||
err := store.InTx(func(store database.Store) error {
|
||||
b.store = store
|
||||
err = database.ReadModifyUpdate(store, func(tx database.Store) error {
|
||||
var err error
|
||||
b.store = tx
|
||||
workspaceBuild, provisionerJob, err = b.buildTx(authFunc)
|
||||
return err
|
||||
}, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
|
||||
var pqe *pq.Error
|
||||
if xerrors.As(err, &pqe) {
|
||||
if pqe.Code == "40001" {
|
||||
// serialization error, retry
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
// Other (hard) error
|
||||
return nil, nil, err
|
||||
return nil, nil, xerrors.Errorf("build tx: %w", err)
|
||||
}
|
||||
return workspaceBuild, provisionerJob, nil
|
||||
}
|
||||
return nil, nil, xerrors.Errorf("too many errors; last error: %w", err)
|
||||
}
|
||||
|
||||
// buildTx contains the business logic of computing a new build. Attributes of the new database objects are computed
|
||||
// in a functional style, rather than imperative, to emphasize the logic of how they are defined. A simple cache
|
||||
@@ -360,7 +349,11 @@ func (b *Builder) buildTx(authFunc func(action rbac.Action, object rbac.Objecter
|
||||
MaxDeadline: time.Time{}, // set by provisioner upon completion
|
||||
})
|
||||
if err != nil {
|
||||
return BuildError{http.StatusInternalServerError, "insert workspace build", err}
|
||||
code := http.StatusInternalServerError
|
||||
if rbac.IsUnauthorizedError(err) {
|
||||
code = http.StatusUnauthorized
|
||||
}
|
||||
return BuildError{code, "insert workspace build", err}
|
||||
}
|
||||
|
||||
names, values, err := b.getParameters()
|
||||
|
||||
@@ -50,6 +50,7 @@ const (
|
||||
FeatureExternalTokenEncryption FeatureName = "external_token_encryption"
|
||||
FeatureTemplateAutostopRequirement FeatureName = "template_autostop_requirement"
|
||||
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
|
||||
FeatureAccessControl FeatureName = "access_control"
|
||||
)
|
||||
|
||||
// FeatureNames must be kept in-sync with the Feature enum above.
|
||||
@@ -70,6 +71,7 @@ var FeatureNames = []FeatureName{
|
||||
FeatureExternalTokenEncryption,
|
||||
FeatureTemplateAutostopRequirement,
|
||||
FeatureWorkspaceBatchActions,
|
||||
FeatureAccessControl,
|
||||
}
|
||||
|
||||
// Humanize returns the feature name in a human-readable format.
|
||||
|
||||
@@ -124,6 +124,10 @@ type CreateTemplateRequest struct {
|
||||
// and must be explicitly granted to users or groups in the permissions settings
|
||||
// of the template.
|
||||
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
|
||||
|
||||
// RequireActiveVersion mandates that workspaces are built with the active
|
||||
// template version.
|
||||
RequireActiveVersion bool `json:"require_active_version"`
|
||||
}
|
||||
|
||||
// CreateWorkspaceRequest provides options for creating a new workspace.
|
||||
|
||||
@@ -52,6 +52,10 @@ type Template struct {
|
||||
FailureTTLMillis int64 `json:"failure_ttl_ms"`
|
||||
TimeTilDormantMillis int64 `json:"time_til_dormant_ms"`
|
||||
TimeTilDormantAutoDeleteMillis int64 `json:"time_til_dormant_autodelete_ms"`
|
||||
|
||||
// RequireActiveVersion mandates that workspaces are built with the active
|
||||
// template version.
|
||||
RequireActiveVersion bool `json:"require_active_version"`
|
||||
}
|
||||
|
||||
// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
|
||||
@@ -221,6 +225,10 @@ type UpdateTemplateMeta struct {
|
||||
// from the template. This is useful for preventing dormant workspaces being immediately
|
||||
// deleted when updating the dormant_ttl field to a new, shorter value.
|
||||
UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"`
|
||||
// RequireActiveVersion mandates workspaces built using this template
|
||||
// use the active version of the template. This option has no
|
||||
// effect on template admins.
|
||||
RequireActiveVersion bool `json:"require_active_version"`
|
||||
}
|
||||
|
||||
type TemplateExample struct {
|
||||
|
||||
@@ -9,13 +9,13 @@ We track the following resources:
|
||||
<!-- Code generated by 'make docs/admin/audit-logs.md'. DO NOT EDIT -->
|
||||
|
||||
| <b>Resource<b> | |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| APIKey<br><i>login, logout, register, create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scope</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| Group<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>members</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>quota_allowance</td><td>true</td></tr><tr><td>source</td><td>false</td></tr></tbody></table> |
|
||||
| GitSSHKey<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>created_at</td><td>false</td></tr><tr><td>private_key</td><td>true</td></tr><tr><td>public_key</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| Template<br><i>write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>active_version_id</td><td>true</td></tr><tr><td>allow_user_autostart</td><td>true</td></tr><tr><td>allow_user_autostop</td><td>true</td></tr><tr><td>allow_user_cancel_workspace_jobs</td><td>true</td></tr><tr><td>autostart_block_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_days_of_week</td><td>true</td></tr><tr><td>autostop_requirement_weeks</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>default_ttl</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>failure_ttl</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>max_ttl</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>provisioner</td><td>true</td></tr><tr><td>require_active_version</td><td>true</td></tr><tr><td>time_til_dormant</td><td>true</td></tr><tr><td>time_til_dormant_autodelete</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>avatar_url</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||
| Workspace<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
|
||||
Generated
+4
@@ -1584,6 +1584,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"icon": "string",
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"require_active_version": true,
|
||||
"template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1"
|
||||
}
|
||||
```
|
||||
@@ -1607,6 +1608,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
|
||||
| `max_ttl_ms` | integer | false | | Max ttl ms remove max_ttl once autostop_requirement is matured |
|
||||
| `name` | string | true | | Name is the name of the template. |
|
||||
| `require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. |
|
||||
| `template_version_id` | string | true | | Template version ID is an in-progress or completed job to use as an initial version of the template. |
|
||||
| This is required on creation to enable a user-flow of validating a template works. There is no reason the data-model cannot support empty templates, but it doesn't make sense for users. |
|
||||
|
||||
@@ -4370,6 +4372,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
@@ -4401,6 +4404,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `provisioner` | string | false | | |
|
||||
| `require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. |
|
||||
| `time_til_dormant_autodelete_ms` | integer | false | | |
|
||||
| `time_til_dormant_ms` | integer | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
|
||||
Generated
+7
@@ -61,6 +61,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
@@ -109,6 +110,7 @@ Status Code **200**
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» provisioner` | string | false | | |
|
||||
| `» require_active_version` | boolean | false | | Require active version mandates that workspaces are built with the active template version. |
|
||||
| `» time_til_dormant_autodelete_ms` | integer | false | | |
|
||||
| `» time_til_dormant_ms` | integer | false | | |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
@@ -159,6 +161,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
||||
"icon": "string",
|
||||
"max_ttl_ms": 0,
|
||||
"name": "string",
|
||||
"require_active_version": true,
|
||||
"template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1"
|
||||
}
|
||||
```
|
||||
@@ -211,6 +214,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/templa
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
@@ -346,6 +350,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/templat
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
@@ -657,6 +662,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template} \
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
@@ -775,6 +781,7 @@ curl -X PATCH http://coder-server:8080/api/v2/templates/{template} \
|
||||
"name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"provisioner": "terraform",
|
||||
"require_active_version": true,
|
||||
"time_til_dormant_autodelete_ms": 0,
|
||||
"time_til_dormant_ms": 0,
|
||||
"updated_at": "2019-08-24T14:15:22Z"
|
||||
|
||||
@@ -85,6 +85,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"failure_ttl": ActionTrack,
|
||||
"time_til_dormant": ActionTrack,
|
||||
"time_til_dormant_autodelete": ActionTrack,
|
||||
"require_active_version": ActionTrack,
|
||||
},
|
||||
&database.TemplateVersion{}: {
|
||||
"id": ActionTrack,
|
||||
|
||||
@@ -24,12 +24,13 @@ import (
|
||||
"cdr.dev/slog"
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
agplaudit "github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
agpldbauthz "github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
agplschedule "github.com/coder/coder/v2/coderd/schedule"
|
||||
"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/proxyhealth"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
@@ -113,7 +114,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
api.AGPL.SiteHandler.RegionsFetcher = func(ctx context.Context) (any, error) {
|
||||
// If the user can read the workspace proxy resource, return that.
|
||||
// If not, always default to the regions.
|
||||
actor, ok := dbauthz.ActorFromContext(ctx)
|
||||
actor, ok := agpldbauthz.ActorFromContext(ctx)
|
||||
if ok && api.Authorizer.Authorize(ctx, actor, rbac.ActionRead, rbac.ResourceWorkspaceProxy) == nil {
|
||||
return api.fetchWorkspaceProxies(ctx)
|
||||
}
|
||||
@@ -482,6 +483,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
codersdk.FeatureTemplateAutostopRequirement: api.AGPL.Experiments.Enabled(codersdk.ExperimentTemplateAutostopRequirement) && api.DefaultQuietHoursSchedule != "",
|
||||
codersdk.FeatureWorkspaceProxy: true,
|
||||
codersdk.FeatureUserRoleManagement: true,
|
||||
codersdk.FeatureAccessControl: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -654,6 +656,14 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if initial, changed, enabled := featureChanged(codersdk.FeatureAccessControl); shouldUpdate(initial, changed, enabled) {
|
||||
var acs agpldbauthz.AccessControlStore = agpldbauthz.AGPLTemplateAccessControlStore{}
|
||||
if enabled {
|
||||
acs = dbauthz.EnterpriseTemplateAccessControlStore{}
|
||||
}
|
||||
api.AGPL.AccessControlStore.Store(&acs)
|
||||
}
|
||||
|
||||
// External token encryption is soft-enforced
|
||||
featureExternalTokenEncryption := entitlements.Features[codersdk.FeatureExternalTokenEncryption]
|
||||
featureExternalTokenEncryption.Enabled = len(api.ExternalTokenEncryption) > 0
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package dbauthz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
agpldbz "github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
)
|
||||
|
||||
type EnterpriseTemplateAccessControlStore struct{}
|
||||
|
||||
func (EnterpriseTemplateAccessControlStore) GetTemplateAccessControl(t database.Template) agpldbz.TemplateAccessControl {
|
||||
return agpldbz.TemplateAccessControl{
|
||||
RequireActiveVersion: t.RequireActiveVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func (EnterpriseTemplateAccessControlStore) SetTemplateAccessControl(ctx context.Context, store database.Store, id uuid.UUID, opts agpldbz.TemplateAccessControl) error {
|
||||
err := store.UpdateTemplateAccessControlByID(ctx, database.UpdateTemplateAccessControlByIDParams{
|
||||
ID: id,
|
||||
RequireActiveVersion: opts.RequireActiveVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template access control: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/schedule"
|
||||
"github.com/coder/coder/v2/provisioner/echo"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
@@ -571,6 +572,51 @@ func TestTemplates(t *testing.T) {
|
||||
require.Equal(t, updatedDormantWS.DormantAt, dormantWorkspace.DormantAt)
|
||||
require.True(t, updatedDormantWS.LastUsedAt.After(dormantWorkspace.LastUsedAt))
|
||||
})
|
||||
|
||||
t.Run("RequireActiveVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAccessControl: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.RequireActiveVersion = true
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
require.True(t, template.RequireActiveVersion)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Update the field and assert it persists.
|
||||
updatedTemplate, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
RequireActiveVersion: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, updatedTemplate.RequireActiveVersion)
|
||||
|
||||
// Flip it back to ensure we aren't hardcoding to a default value.
|
||||
updatedTemplate, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, updatedTemplate.RequireActiveVersion)
|
||||
|
||||
// Assert that fetching a template is no different from the response
|
||||
// when updating.
|
||||
template, err = client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, updatedTemplate, template)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateACL(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestWorkspaceBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("TemplateRequiresActiveVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAccessControl: 1,
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Create an initial version.
|
||||
oldVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||||
// Create a template that mandates the promoted version.
|
||||
// This should be enforced for everyone except template admins.
|
||||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, oldVersion.ID)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, oldVersion.ID)
|
||||
require.Equal(t, oldVersion.ID, template.ActiveVersionID)
|
||||
template, err := ownerClient.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
RequireActiveVersion: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, template.RequireActiveVersion)
|
||||
|
||||
// Create a new version that we will promote.
|
||||
activeVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, activeVersion.ID)
|
||||
err = ownerClient.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: activeVersion.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
templateAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
templateACLAdminClient, templateACLAdmin := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
templateGroupACLAdminClient, templateGroupACLAdmin := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
// Create a group so we can also test group template admin ownership.
|
||||
group, err := ownerClient.CreateGroup(ctx, owner.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add the user who gains template admin via group membership.
|
||||
group, err = ownerClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
|
||||
AddUsers: []string{templateGroupACLAdmin.ID.String()},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update the template for both users and groups.
|
||||
err = ownerClient.UpdateTemplateACL(ctx, template.ID, codersdk.UpdateTemplateACL{
|
||||
UserPerms: map[string]codersdk.TemplateRole{
|
||||
templateACLAdmin.ID.String(): codersdk.TemplateRoleAdmin,
|
||||
},
|
||||
GroupPerms: map[string]codersdk.TemplateRole{
|
||||
group.ID.String(): codersdk.TemplateRoleAdmin,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
type testcase struct {
|
||||
Name string
|
||||
Client *codersdk.Client
|
||||
ExpectedStatusCode int
|
||||
}
|
||||
|
||||
cases := []testcase{
|
||||
{
|
||||
Name: "OwnerOK",
|
||||
Client: ownerClient,
|
||||
ExpectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "TemplateAdminOK",
|
||||
Client: templateAdminClient,
|
||||
ExpectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "TemplateACLAdminOK",
|
||||
Client: templateACLAdminClient,
|
||||
ExpectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "TemplateGroupACLAdminOK",
|
||||
Client: templateGroupACLAdminClient,
|
||||
ExpectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
Name: "MemberFails",
|
||||
Client: memberClient,
|
||||
ExpectedStatusCode: http.StatusUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.Name, func(t *testing.T) {
|
||||
_, err = c.Client.CreateWorkspace(ctx, owner.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
||||
TemplateVersionID: oldVersion.ID,
|
||||
Name: "abc123",
|
||||
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
|
||||
})
|
||||
if c.ExpectedStatusCode == http.StatusOK {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, c.ExpectedStatusCode, cerr.StatusCode())
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -734,6 +734,96 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Equal(t, database.WorkspaceTransitionDelete, stats.Transitions[ws.ID])
|
||||
})
|
||||
|
||||
t.Run("RequireActiveVersion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
ctx = testutil.Context(t, testutil.WaitMedium)
|
||||
)
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAccessControl: 1},
|
||||
},
|
||||
})
|
||||
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 * * * *")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a template version1 that passes to get a functioning workspace.
|
||||
version1 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
||||
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version1.ID)
|
||||
require.Equal(t, version1.ID, template.ActiveVersionID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// Create a new version so that we can assert we don't update
|
||||
// to the latest by default.
|
||||
version2 := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
||||
|
||||
// Make sure to promote it.
|
||||
err = client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Kick of an autostart build.
|
||||
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
stats := <-statsCh
|
||||
require.NoError(t, stats.Error)
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Contains(t, stats.Transitions, ws.ID)
|
||||
require.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID])
|
||||
|
||||
// Validate that we didn't update to the promoted version.
|
||||
started := coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
firstBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, started.LatestBuild.ID)
|
||||
require.Equal(t, version1.ID, firstBuild.TemplateVersionID)
|
||||
|
||||
// Update the template to require the promoted version.
|
||||
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
RequireActiveVersion: true,
|
||||
AllowUserAutostart: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reset the workspace to the stopped state so we can try
|
||||
// to autostart again.
|
||||
coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
||||
req.TemplateVersionID = ws.LatestBuild.TemplateVersionID
|
||||
})
|
||||
|
||||
// Force an autostart transition again.
|
||||
tickCh <- sched.Next(firstBuild.CreatedAt)
|
||||
stats = <-statsCh
|
||||
require.NoError(t, stats.Error)
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Contains(t, stats.Transitions, ws.ID)
|
||||
require.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID])
|
||||
|
||||
// Validate that we are using the promoted version.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.Equal(t, version2.ID, ws.LatestBuild.TemplateVersionID)
|
||||
})
|
||||
}
|
||||
|
||||
// Blocked by autostart requirements
|
||||
|
||||
Generated
+5
@@ -221,6 +221,7 @@ export interface CreateTemplateRequest {
|
||||
readonly dormant_ttl_ms?: number;
|
||||
readonly delete_ttl_ms?: number;
|
||||
readonly disable_everyone_group_access: boolean;
|
||||
readonly require_active_version: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/templateversions.go
|
||||
@@ -916,6 +917,7 @@ export interface Template {
|
||||
readonly failure_ttl_ms: number;
|
||||
readonly time_til_dormant_ms: number;
|
||||
readonly time_til_dormant_autodelete_ms: number;
|
||||
readonly require_active_version: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
@@ -1166,6 +1168,7 @@ export interface UpdateTemplateMeta {
|
||||
readonly time_til_dormant_autodelete_ms?: number;
|
||||
readonly update_workspace_last_used_at: boolean;
|
||||
readonly update_workspace_dormant_at: boolean;
|
||||
readonly require_active_version: boolean;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
@@ -1715,6 +1718,7 @@ export const Experiments: Experiment[] = [
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type FeatureName =
|
||||
| "access_control"
|
||||
| "advanced_template_scheduling"
|
||||
| "appearance"
|
||||
| "audit_log"
|
||||
@@ -1731,6 +1735,7 @@ export type FeatureName =
|
||||
| "workspace_batch_actions"
|
||||
| "workspace_proxy";
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"access_control",
|
||||
"advanced_template_scheduling",
|
||||
"appearance",
|
||||
"audit_log",
|
||||
|
||||
@@ -37,6 +37,7 @@ export const newTemplate = (formData: CreateTemplateData) => {
|
||||
autostart_requirement: {
|
||||
days_of_week: autostart_requirement_days_of_week,
|
||||
},
|
||||
require_active_version: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
||||
template.allow_user_cancel_workspace_jobs,
|
||||
update_workspace_last_used_at: false,
|
||||
update_workspace_dormant_at: false,
|
||||
require_active_version: false,
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
|
||||
+1
@@ -43,6 +43,7 @@ const validFormValues: FormValues = {
|
||||
time_til_dormant_autodelete_ms: 0,
|
||||
update_workspace_last_used_at: false,
|
||||
update_workspace_dormant_at: false,
|
||||
require_active_version: false,
|
||||
};
|
||||
|
||||
const renderTemplateSettingsPage = async () => {
|
||||
|
||||
@@ -113,6 +113,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
Boolean(template.time_til_dormant_autodelete_ms),
|
||||
update_workspace_last_used_at: false,
|
||||
update_workspace_dormant_at: false,
|
||||
require_active_version: false,
|
||||
},
|
||||
validationSchema,
|
||||
onSubmit: () => {
|
||||
@@ -229,6 +230,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
|
||||
allow_user_autostop: form.values.allow_user_autostop,
|
||||
update_workspace_last_used_at: form.values.update_workspace_last_used_at,
|
||||
update_workspace_dormant_at: form.values.update_workspace_dormant_at,
|
||||
require_active_version: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const validFormValues: TemplateScheduleFormValues = {
|
||||
failure_cleanup_enabled: false,
|
||||
inactivity_cleanup_enabled: false,
|
||||
dormant_autodeletion_cleanup_enabled: false,
|
||||
require_active_version: false,
|
||||
autostart_requirement_days_of_week: [
|
||||
"monday",
|
||||
"tuesday",
|
||||
|
||||
@@ -467,6 +467,7 @@ export const MockTemplate: TypesGen.Template = {
|
||||
time_til_dormant_autodelete_ms: 0,
|
||||
allow_user_autostart: true,
|
||||
allow_user_autostop: true,
|
||||
require_active_version: false,
|
||||
};
|
||||
|
||||
export const MockTemplateVersionFiles: TemplateVersionFiles = {
|
||||
|
||||
Reference in New Issue
Block a user