feat: add API support for workspace automatic updates (#10099)

* Added automatic_updates to workspaces table

Signed-off-by: Spike Curtis <spike@coder.com>

* Queries and API updates

Signed-off-by: Spike Curtis <spike@coder.com>

* Golden files

Signed-off-by: Spike Curtis <spike@coder.com>

* Enable automatic updates on autostart

Signed-off-by: Spike Curtis <spike@coder.com>

* db migration number

Signed-off-by: Spike Curtis <spike@coder.com>

* fix imports and ts mock

Signed-off-by: Spike Curtis <spike@coder.com>

* code review updates

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis 2023-10-06 13:27:12 +04:00 committed by GitHub
parent d24d2d2c8d
commit 983e8c3ae8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 847 additions and 138 deletions

View File

@ -27,6 +27,7 @@ func (r *RootCmd) create() *clibase.Cmd {
workspaceName string
parameterFlags workspaceParameterFlags
autoUpdates string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@ -169,6 +170,7 @@ func (r *RootCmd) create() *clibase.Cmd {
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
RichParameterValues: richParameters,
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
})
if err != nil {
return xerrors.Errorf("create workspace: %w", err)
@ -208,6 +210,13 @@ func (r *RootCmd) create() *clibase.Cmd {
Description: "Specify a duration after which the workspace should shut down (e.g. 8h).",
Value: clibase.DurationOf(&stopAfter),
},
clibase.Option{
Flag: "automatic-updates",
Env: "CODER_WORKSPACE_AUTOMATIC_UPDATES",
Description: "Specify automatic updates setting for the workspace (accepts 'always' or 'never').",
Default: string(codersdk.AutomaticUpdatesNever),
Value: clibase.StringOf(&autoUpdates),
},
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)

View File

@ -38,6 +38,7 @@ func TestCreate(t *testing.T) {
"--template", template.Name,
"--start-at", "9:30AM Mon-Fri US/Central",
"--stop-after", "8h",
"--automatic-updates", "always",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
@ -73,6 +74,7 @@ func TestCreate(t *testing.T) {
if assert.NotNil(t, ws.TTLMillis) {
assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
}
assert.Equal(t, codersdk.AutomaticUpdatesAlways, ws.AutomaticUpdates)
}
})

View File

@ -10,6 +10,10 @@ USAGE:
$ coder create <username>/<workspace_name>
OPTIONS:
--automatic-updates string, $CODER_WORKSPACE_AUTOMATIC_UPDATES (default: never)
Specify automatic updates setting for the workspace (accepts 'always'
or 'never').
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".

View File

@ -57,6 +57,7 @@
"health": {
"healthy": true,
"failing_agents": []
}
},
"automatic_updates": "never"
}
]

74
coderd/apidoc/docs.go generated
View File

@ -5998,6 +5998,47 @@ const docTemplate = `{
}
}
},
"/workspaces/{workspace}/autoupdates": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": [
"application/json"
],
"tags": [
"Workspaces"
],
"summary": "Update workspace automatic updates by ID",
"operationId": "update-workspace-automatic-updates-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace ID",
"name": "workspace",
"in": "path",
"required": true
},
{
"description": "Automatic updates request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateWorkspaceAutomaticUpdatesRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/workspaces/{workspace}/builds": {
"get": {
"security": [
@ -7264,6 +7305,17 @@ const docTemplate = `{
"type": "boolean"
}
},
"codersdk.AutomaticUpdates": {
"type": "string",
"enum": [
"always",
"never"
],
"x-enum-varnames": [
"AutomaticUpdatesAlways",
"AutomaticUpdatesNever"
]
},
"codersdk.BuildInfoResponse": {
"type": "object",
"properties": {
@ -7740,6 +7792,9 @@ const docTemplate = `{
"name"
],
"properties": {
"automatic_updates": {
"$ref": "#/definitions/codersdk.AutomaticUpdates"
},
"autostart_schedule": {
"type": "string"
},
@ -10309,6 +10364,14 @@ const docTemplate = `{
}
}
},
"codersdk.UpdateWorkspaceAutomaticUpdatesRequest": {
"type": "object",
"properties": {
"automatic_updates": {
"$ref": "#/definitions/codersdk.AutomaticUpdates"
}
}
},
"codersdk.UpdateWorkspaceAutostartRequest": {
"type": "object",
"properties": {
@ -10626,6 +10689,17 @@ const docTemplate = `{
"codersdk.Workspace": {
"type": "object",
"properties": {
"automatic_updates": {
"enum": [
"always",
"never"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.AutomaticUpdates"
}
]
},
"autostart_schedule": {
"type": "string"
},

View File

@ -5288,6 +5288,43 @@
}
}
},
"/workspaces/{workspace}/autoupdates": {
"put": {
"security": [
{
"CoderSessionToken": []
}
],
"consumes": ["application/json"],
"tags": ["Workspaces"],
"summary": "Update workspace automatic updates by ID",
"operationId": "update-workspace-automatic-updates-by-id",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Workspace ID",
"name": "workspace",
"in": "path",
"required": true
},
{
"description": "Automatic updates request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/codersdk.UpdateWorkspaceAutomaticUpdatesRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
}
}
}
},
"/workspaces/{workspace}/builds": {
"get": {
"security": [
@ -6469,6 +6506,11 @@
"type": "boolean"
}
},
"codersdk.AutomaticUpdates": {
"type": "string",
"enum": ["always", "never"],
"x-enum-varnames": ["AutomaticUpdatesAlways", "AutomaticUpdatesNever"]
},
"codersdk.BuildInfoResponse": {
"type": "object",
"properties": {
@ -6892,6 +6934,9 @@
"type": "object",
"required": ["name"],
"properties": {
"automatic_updates": {
"$ref": "#/definitions/codersdk.AutomaticUpdates"
},
"autostart_schedule": {
"type": "string"
},
@ -9332,6 +9377,14 @@
}
}
},
"codersdk.UpdateWorkspaceAutomaticUpdatesRequest": {
"type": "object",
"properties": {
"automatic_updates": {
"$ref": "#/definitions/codersdk.AutomaticUpdates"
}
}
},
"codersdk.UpdateWorkspaceAutostartRequest": {
"type": "object",
"properties": {
@ -9631,6 +9684,14 @@
"codersdk.Workspace": {
"type": "object",
"properties": {
"automatic_updates": {
"enum": ["always", "never"],
"allOf": [
{
"$ref": "#/definitions/codersdk.AutomaticUpdates"
}
]
},
"autostart_schedule": {
"type": "string"
},

View File

@ -177,6 +177,12 @@ func (e *Executor) runOnce(t time.Time) Stats {
SetLastWorkspaceBuildInTx(&latestBuild).
SetLastWorkspaceBuildJobInTx(&latestJob).
Reason(reason)
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
if nextTransition == database.WorkspaceTransitionStart &&
ws.AutomaticUpdates == database.AutomaticUpdatesAlways {
log.Debug(e.ctx, "autostarting with active version")
builder = builder.ActiveVersion()
}
build, job, err = builder.Build(e.ctx, tx, nil)
if err != nil {

View File

@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/autobuild"
@ -64,50 +65,129 @@ func TestExecutorAutostartOK(t *testing.T) {
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
t.Parallel()
var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
testCases := []struct {
name string
automaticUpdates codersdk.AutomaticUpdates
compatibleParameters bool
expectStart bool
expectUpdate bool
}{
{
name: "Never",
automaticUpdates: codersdk.AutomaticUpdatesNever,
compatibleParameters: true,
expectStart: true,
expectUpdate: false,
},
{
name: "Always_Compatible",
automaticUpdates: codersdk.AutomaticUpdatesAlways,
compatibleParameters: true,
expectStart: true,
expectUpdate: true,
},
{
name: "Always_Incompatible",
automaticUpdates: codersdk.AutomaticUpdatesAlways,
compatibleParameters: false,
expectStart: false,
expectUpdate: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
ctx = context.Background()
err error
tickCh = make(chan time.Time)
statsCh = make(chan autobuild.Stats)
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
client = coderdtest.New(t, &coderdtest.Options{
AutobuildTicker: tickCh,
IncludeProvisionerDaemon: true,
AutobuildStats: statsCh,
Logger: &logger,
})
// Given: we have a user with a workspace that has autostart enabled
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
// Given: automatic updates from the test case
cwr.AutomaticUpdates = tc.automaticUpdates
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(
t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err)
require.Len(t, orgs, 1)
var res *echo.Responses
if !tc.compatibleParameters {
// Given, parameters of the new version are not compatible.
// Since initial version has no parameters, any parameters in the new version will be incompatible
res = &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Parameters: []*proto.RichParameter{
{
Name: "new",
Mutable: false,
Required: true,
},
},
},
},
}},
}
}
// Given: the workspace template has been updated
newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, res, workspace.TemplateID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
require.NoError(t, client.UpdateActiveTemplateVersion(
ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
ID: newVersion.ID,
},
))
t.Log("sending autobuild tick")
// When: the autobuild executor ticks after the scheduled time
go func() {
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
close(tickCh)
}()
stats := <-statsCh
assert.NoError(t, stats.Error)
if !tc.expectStart {
// Then: the workspace should not be started
assert.Len(t, stats.Transitions, 0)
return
}
// Then: the workspace should be started
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
if tc.expectUpdate {
// Then: uses the updated version
assert.Equal(t, newVersion.ID, ws.LatestBuild.TemplateVersionID,
"expected workspace build to be using the updated template version")
} else {
// Then: uses the previous template version
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID,
"expected workspace build to be using the old template version")
}
})
// Given: we have a user with a workspace that has autostart enabled
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
// Given: the workspace template has been updated
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err)
require.Len(t, orgs, 1)
newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, nil, workspace.TemplateID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
require.NoError(t, client.UpdateActiveTemplateVersion(ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
ID: newVersion.ID,
}))
// When: the autobuild executor ticks after the scheduled time
go func() {
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
close(tickCh)
}()
// Then: the workspace should be started using the previous template version, and not the updated version.
stats := <-statsCh
assert.NoError(t, stats.Error)
assert.Len(t, stats.Transitions, 1)
assert.Contains(t, stats.Transitions, workspace.ID)
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID, "expected workspace build to be using the old template version")
}
}
func TestExecutorAutostartAlreadyRunning(t *testing.T) {

View File

@ -867,6 +867,7 @@ func New(options *Options) *API {
r.Get("/watch", api.watchWorkspace)
r.Put("/extend", api.putExtendWorkspace)
r.Put("/dormant", api.putWorkspaceDormant)
r.Put("/autoupdates", api.putWorkspaceAutoupdates)
})
})
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {

View File

@ -182,6 +182,10 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
if options == nil {
options = &Options{}
}
if options.Logger == nil {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
options.Logger = &logger
}
if options.GoogleTokenValidator == nil {
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
@ -214,7 +218,7 @@ 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, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
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.
@ -275,14 +279,14 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
options.Pubsub,
&templateScheduleStore,
&auditor,
slogtest.Make(t, nil).Named("autobuild.executor").Leveled(slog.LevelDebug),
*options.Logger,
options.AutobuildTicker,
).WithStatsChannel(options.AutobuildStats)
lifecycleExecutor.Run()
hangDetectorTicker := time.NewTicker(options.DeploymentValues.JobHangDetectorInterval.Value())
defer hangDetectorTicker.Stop()
hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, slogtest.Make(t, nil).Named("unhanger.detector"), hangDetectorTicker.C)
hangDetector := unhanger.New(ctx, options.Database, options.Pubsub, options.Logger.Named("unhanger.detector"), hangDetectorTicker.C)
hangDetector.Start()
t.Cleanup(hangDetector.Close)
@ -341,7 +345,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
stunAddresses = options.DeploymentValues.DERP.Server.STUNAddresses.Value()
}
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(slogtest.Make(t, nil).Named("derp").Leveled(slog.LevelDebug)))
derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(options.Logger.Named("derp").Leveled(slog.LevelDebug)))
derpServer.SetMeshKey("test-key")
// match default with cli default
@ -356,10 +360,6 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
require.NoError(t, err)
}
if options.Logger == nil {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
options.Logger = &logger
}
region := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: int(options.DeploymentValues.DERP.Server.RegionID.Value()),
@ -893,6 +893,7 @@ func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UU
Name: randomUsername(t),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central 30 9 * * 1-5"),
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
}
for _, mutator := range mutators {
mutator(&req)

View File

@ -2715,6 +2715,19 @@ func (q *querier) UpdateWorkspaceAppHealthByID(ctx context.Context, arg database
return q.db.UpdateWorkspaceAppHealthByID(ctx, arg)
}
func (q *querier) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg database.UpdateWorkspaceAutomaticUpdatesParams) error {
workspace, err := q.db.GetWorkspaceByID(ctx, arg.ID)
if err != nil {
return err
}
err = q.authorizeContext(ctx, rbac.ActionUpdate, workspace.RBACObject())
if err != nil {
return err
}
return q.db.UpdateWorkspaceAutomaticUpdates(ctx, arg)
}
func (q *querier) UpdateWorkspaceAutostart(ctx context.Context, arg database.UpdateWorkspaceAutostartParams) error {
fetch := func(ctx context.Context, arg database.UpdateWorkspaceAutostartParams) (database.Workspace, error) {
return q.db.GetWorkspaceByID(ctx, arg.ID)

View File

@ -1171,9 +1171,10 @@ func (s *MethodTestSuite) TestWorkspace() {
u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{})
check.Args(database.InsertWorkspaceParams{
ID: uuid.New(),
OwnerID: u.ID,
OrganizationID: o.ID,
ID: uuid.New(),
OwnerID: u.ID,
OrganizationID: o.ID,
AutomaticUpdates: database.AutomaticUpdatesNever,
}).Asserts(rbac.ResourceWorkspace.WithOwner(u.ID.String()).InOrg(o.ID), rbac.ActionCreate)
}))
s.Run("Start/InsertWorkspaceBuild", s.Subtest(func(db database.Store, check *expects) {

View File

@ -354,6 +354,7 @@ func (q *FakeQuerier) convertToWorkspaceRowsNoLock(ctx context.Context, workspac
DormantAt: w.DormantAt,
DeletingAt: w.DeletingAt,
Count: count,
AutomaticUpdates: w.AutomaticUpdates,
}
for _, t := range q.templates {
@ -4765,6 +4766,7 @@ func (q *FakeQuerier) InsertWorkspace(_ context.Context, arg database.InsertWork
AutostartSchedule: arg.AutostartSchedule,
Ttl: arg.Ttl,
LastUsedAt: arg.LastUsedAt,
AutomaticUpdates: arg.AutomaticUpdates,
}
q.workspaces = append(q.workspaces, workspace)
return workspace, nil
@ -6089,6 +6091,26 @@ func (q *FakeQuerier) UpdateWorkspaceAppHealthByID(_ context.Context, arg databa
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAutomaticUpdates(_ context.Context, arg database.UpdateWorkspaceAutomaticUpdatesParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for index, workspace := range q.workspaces {
if workspace.ID != arg.ID {
continue
}
workspace.AutomaticUpdates = arg.AutomaticUpdates
q.workspaces[index] = workspace
return nil
}
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateWorkspaceAutostart(_ context.Context, arg database.UpdateWorkspaceAutostartParams) error {
if err := validateDatabaseType(arg); err != nil {
return err

View File

@ -172,6 +172,7 @@ func Workspace(t testing.TB, db database.Store, orig database.Workspace) databas
Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)),
AutostartSchedule: orig.AutostartSchedule,
Ttl: orig.Ttl,
AutomaticUpdates: takeFirst(orig.AutomaticUpdates, database.AutomaticUpdatesNever),
})
require.NoError(t, err, "insert workspace")
return workspace

View File

@ -1684,6 +1684,13 @@ func (m metricsStore) UpdateWorkspaceAppHealthByID(ctx context.Context, arg data
return err
}
func (m metricsStore) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg database.UpdateWorkspaceAutomaticUpdatesParams) error {
start := time.Now()
r0 := m.s.UpdateWorkspaceAutomaticUpdates(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateWorkspaceAutomaticUpdates").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpdateWorkspaceAutostart(ctx context.Context, arg database.UpdateWorkspaceAutostartParams) error {
start := time.Now()
err := m.s.UpdateWorkspaceAutostart(ctx, arg)

View File

@ -3543,6 +3543,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspaceAppHealthByID(arg0, arg1 interfa
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAppHealthByID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAppHealthByID), arg0, arg1)
}
// UpdateWorkspaceAutomaticUpdates mocks base method.
func (m *MockStore) UpdateWorkspaceAutomaticUpdates(arg0 context.Context, arg1 database.UpdateWorkspaceAutomaticUpdatesParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWorkspaceAutomaticUpdates", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateWorkspaceAutomaticUpdates indicates an expected call of UpdateWorkspaceAutomaticUpdates.
func (mr *MockStoreMockRecorder) UpdateWorkspaceAutomaticUpdates(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspaceAutomaticUpdates", reflect.TypeOf((*MockStore)(nil).UpdateWorkspaceAutomaticUpdates), arg0, arg1)
}
// UpdateWorkspaceAutostart mocks base method.
func (m *MockStore) UpdateWorkspaceAutostart(arg0 context.Context, arg1 database.UpdateWorkspaceAutostartParams) error {
m.ctrl.T.Helper()

View File

@ -22,6 +22,11 @@ CREATE TYPE audit_action AS ENUM (
'register'
);
CREATE TYPE automatic_updates AS ENUM (
'always',
'never'
);
CREATE TYPE build_reason AS ENUM (
'initiator',
'autostart',
@ -1127,7 +1132,8 @@ CREATE TABLE workspaces (
ttl bigint,
last_used_at timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
dormant_at timestamp with time zone,
deleting_at timestamp with time zone
deleting_at timestamp with time zone,
automatic_updates automatic_updates DEFAULT 'never'::automatic_updates NOT NULL
);
ALTER TABLE ONLY licenses ALTER COLUMN id SET DEFAULT nextval('licenses_id_seq'::regclass);

View File

@ -0,0 +1,4 @@
BEGIN;
ALTER TABLE workspaces DROP COLUMN IF EXISTS automatic_updates;
DROP TYPE IF EXISTS automatic_updates;
COMMIT;

View File

@ -0,0 +1,8 @@
BEGIN;
-- making this an enum in case we want to later add other options, like 'if_compatible_vars'
CREATE TYPE automatic_updates AS ENUM (
'always',
'never'
);
ALTER TABLE workspaces ADD COLUMN IF NOT EXISTS automatic_updates automatic_updates NOT NULL DEFAULT 'never'::automatic_updates;
COMMIT;

View File

@ -358,6 +358,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
LastUsedAt: r.LastUsedAt,
DormantAt: r.DormantAt,
DeletingAt: r.DeletingAt,
AutomaticUpdates: r.AutomaticUpdates,
}
}

View File

@ -244,6 +244,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.TemplateName,
&i.TemplateVersionID,
&i.TemplateVersionName,

View File

@ -211,6 +211,64 @@ func AllAuditActionValues() []AuditAction {
}
}
type AutomaticUpdates string
const (
AutomaticUpdatesAlways AutomaticUpdates = "always"
AutomaticUpdatesNever AutomaticUpdates = "never"
)
func (e *AutomaticUpdates) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = AutomaticUpdates(s)
case string:
*e = AutomaticUpdates(s)
default:
return fmt.Errorf("unsupported scan type for AutomaticUpdates: %T", src)
}
return nil
}
type NullAutomaticUpdates struct {
AutomaticUpdates AutomaticUpdates `json:"automatic_updates"`
Valid bool `json:"valid"` // Valid is true if AutomaticUpdates is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullAutomaticUpdates) Scan(value interface{}) error {
if value == nil {
ns.AutomaticUpdates, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.AutomaticUpdates.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullAutomaticUpdates) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.AutomaticUpdates), nil
}
func (e AutomaticUpdates) Valid() bool {
switch e {
case AutomaticUpdatesAlways,
AutomaticUpdatesNever:
return true
}
return false
}
func AllAutomaticUpdatesValues() []AutomaticUpdates {
return []AutomaticUpdates{
AutomaticUpdatesAlways,
AutomaticUpdatesNever,
}
}
type BuildReason string
const (
@ -1995,19 +2053,20 @@ type VisibleUser struct {
}
type Workspace struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
}
type WorkspaceAgent struct {

View File

@ -317,6 +317,7 @@ type sqlcQuerier interface {
UpdateWorkspaceAgentMetadata(ctx context.Context, arg UpdateWorkspaceAgentMetadataParams) error
UpdateWorkspaceAgentStartupByID(ctx context.Context, arg UpdateWorkspaceAgentStartupByIDParams) error
UpdateWorkspaceAppHealthByID(ctx context.Context, arg UpdateWorkspaceAppHealthByIDParams) error
UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
UpdateWorkspaceBuildCostByID(ctx context.Context, arg UpdateWorkspaceBuildCostByIDParams) error
UpdateWorkspaceBuildDeadlineByID(ctx context.Context, arg UpdateWorkspaceBuildDeadlineByIDParams) error

View File

@ -9647,7 +9647,7 @@ func (q *sqlQuerier) GetDeploymentWorkspaceStats(ctx context.Context) (GetDeploy
const getWorkspaceByAgentID = `-- name: GetWorkspaceByAgentID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
FROM
workspaces
WHERE
@ -9692,13 +9692,14 @@ func (q *sqlQuerier) GetWorkspaceByAgentID(ctx context.Context, agentID uuid.UUI
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
const getWorkspaceByID = `-- name: GetWorkspaceByID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
FROM
workspaces
WHERE
@ -9724,13 +9725,14 @@ func (q *sqlQuerier) GetWorkspaceByID(ctx context.Context, id uuid.UUID) (Worksp
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
const getWorkspaceByOwnerIDAndName = `-- name: GetWorkspaceByOwnerIDAndName :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
FROM
workspaces
WHERE
@ -9763,13 +9765,14 @@ func (q *sqlQuerier) GetWorkspaceByOwnerIDAndName(ctx context.Context, arg GetWo
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
const getWorkspaceByWorkspaceAppID = `-- name: GetWorkspaceByWorkspaceAppID :one
SELECT
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
FROM
workspaces
WHERE
@ -9821,13 +9824,14 @@ func (q *sqlQuerier) GetWorkspaceByWorkspaceAppID(ctx context.Context, workspace
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
const getWorkspaces = `-- name: GetWorkspaces :many
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at,
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates,
COALESCE(template_name.template_name, 'unknown') as template_name,
latest_build.template_version_id,
latest_build.template_version_name,
@ -10046,23 +10050,24 @@ type GetWorkspacesParams struct {
}
type GetWorkspacesRow struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
Count int64 `db:"count" json:"count"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Deleted bool `db:"deleted" json:"deleted"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
DormantAt sql.NullTime `db:"dormant_at" json:"dormant_at"`
DeletingAt sql.NullTime `db:"deleting_at" json:"deleting_at"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
TemplateName string `db:"template_name" json:"template_name"`
TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"`
TemplateVersionName sql.NullString `db:"template_version_name" json:"template_version_name"`
Count int64 `db:"count" json:"count"`
}
func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]GetWorkspacesRow, error) {
@ -10103,6 +10108,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
&i.TemplateName,
&i.TemplateVersionID,
&i.TemplateVersionName,
@ -10123,7 +10129,7 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
const getWorkspacesEligibleForTransition = `-- name: GetWorkspacesEligibleForTransition :many
SELECT
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates
FROM
workspaces
LEFT JOIN
@ -10210,6 +10216,7 @@ func (q *sqlQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, now
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
); err != nil {
return nil, err
}
@ -10236,23 +10243,25 @@ INSERT INTO
name,
autostart_schedule,
ttl,
last_used_at
last_used_at,
automatic_updates
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
`
type InsertWorkspaceParams struct {
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
ID uuid.UUID `db:"id" json:"id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Name string `db:"name" json:"name"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
Ttl sql.NullInt64 `db:"ttl" json:"ttl"`
LastUsedAt time.Time `db:"last_used_at" json:"last_used_at"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
}
func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspaceParams) (Workspace, error) {
@ -10267,6 +10276,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
arg.AutostartSchedule,
arg.Ttl,
arg.LastUsedAt,
arg.AutomaticUpdates,
)
var i Workspace
err := row.Scan(
@ -10283,6 +10293,7 @@ func (q *sqlQuerier) InsertWorkspace(ctx context.Context, arg InsertWorkspacePar
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
@ -10313,7 +10324,7 @@ SET
WHERE
id = $1
AND deleted = false
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at
RETURNING id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at, dormant_at, deleting_at, automatic_updates
`
type UpdateWorkspaceParams struct {
@ -10338,10 +10349,30 @@ func (q *sqlQuerier) UpdateWorkspace(ctx context.Context, arg UpdateWorkspacePar
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}
const updateWorkspaceAutomaticUpdates = `-- name: UpdateWorkspaceAutomaticUpdates :exec
UPDATE
workspaces
SET
automatic_updates = $2
WHERE
id = $1
`
type UpdateWorkspaceAutomaticUpdatesParams struct {
ID uuid.UUID `db:"id" json:"id"`
AutomaticUpdates AutomaticUpdates `db:"automatic_updates" json:"automatic_updates"`
}
func (q *sqlQuerier) UpdateWorkspaceAutomaticUpdates(ctx context.Context, arg UpdateWorkspaceAutomaticUpdatesParams) error {
_, err := q.db.ExecContext(ctx, updateWorkspaceAutomaticUpdates, arg.ID, arg.AutomaticUpdates)
return err
}
const updateWorkspaceAutostart = `-- name: UpdateWorkspaceAutostart :exec
UPDATE
workspaces
@ -10397,7 +10428,7 @@ WHERE
workspaces.template_id = templates.id
AND
workspaces.id = $1
RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at
RETURNING workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates
`
type UpdateWorkspaceDormantDeletingAtParams struct {
@ -10422,6 +10453,7 @@ func (q *sqlQuerier) UpdateWorkspaceDormantDeletingAt(ctx context.Context, arg U
&i.LastUsedAt,
&i.DormantAt,
&i.DeletingAt,
&i.AutomaticUpdates,
)
return i, err
}

View File

@ -299,10 +299,11 @@ INSERT INTO
name,
autostart_schedule,
ttl,
last_used_at
last_used_at,
automatic_updates
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *;
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING *;
-- name: UpdateWorkspaceDeletedByID :exec
UPDATE
@ -512,3 +513,11 @@ SET
last_used_at = @last_used_at::timestamptz
WHERE
template_id = @template_id;
-- name: UpdateWorkspaceAutomaticUpdates :exec
UPDATE
workspaces
SET
automatic_updates = $2
WHERE
id = $1;

View File

@ -121,9 +121,10 @@ func TestWorkspaceParam(t *testing.T) {
})
r, user := setup(db)
workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{
ID: uuid.New(),
OwnerID: user.ID,
Name: "hello",
ID: uuid.New(),
OwnerID: user.ID,
Name: "hello",
AutomaticUpdates: database.AutomaticUpdatesNever,
})
require.NoError(t, err)
chi.RouteContext(r.Context()).URLParams.Add("workspace", workspace.ID.String())

View File

@ -783,7 +783,8 @@ func TestFailJob(t *testing.T) {
srvID := uuid.New()
srv, db, ps := setup(t, ignoreLogErrors, &overrides{id: &srvID})
workspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
ID: uuid.New(),
ID: uuid.New(),
AutomaticUpdates: database.AutomaticUpdatesNever,
})
require.NoError(t, err)
buildID := uuid.New()

View File

@ -506,6 +506,7 @@ func ConvertWorkspace(workspace database.Workspace) Workspace {
Deleted: workspace.Deleted,
Name: workspace.Name,
AutostartSchedule: workspace.AutostartSchedule.String,
AutomaticUpdates: string(workspace.AutomaticUpdates),
}
}
@ -840,6 +841,7 @@ type Workspace struct {
Deleted bool `json:"deleted"`
Name string `json:"name"`
AutostartSchedule string `json:"autostart_schedule"`
AutomaticUpdates string `json:"automatic_updates"`
}
type Template struct {

View File

@ -425,6 +425,19 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
return
}
// back-compatibility: default to "never" if not included.
dbAU := database.AutomaticUpdatesNever
if createWorkspace.AutomaticUpdates != "" {
dbAU, err = validWorkspaceAutomaticUpdates(createWorkspace.AutomaticUpdates)
if err != nil {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid Workspace Automatic Updates setting.",
Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: err.Error()}},
})
return
}
}
// TODO: This should be a system call as the actor might not be able to
// read other workspaces. Ideally we check the error on create and look for
// a postgres conflict error.
@ -470,7 +483,8 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
Ttl: dbTTL,
// The workspaces page will sort by last used at, and it's useful to
// have the newly created workspace at the top of the list!
LastUsedAt: dbtime.Now(),
LastUsedAt: dbtime.Now(),
AutomaticUpdates: dbAU,
})
if err != nil {
return xerrors.Errorf("insert workspace: %w", err)
@ -977,6 +991,66 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, code, resp)
}
// @Summary Update workspace automatic updates by ID
// @ID update-workspace-automatic-updates-by-id
// @Security CoderSessionToken
// @Accept json
// @Tags Workspaces
// @Param workspace path string true "Workspace ID" format(uuid)
// @Param request body codersdk.UpdateWorkspaceAutomaticUpdatesRequest true "Automatic updates request"
// @Success 204
// @Router /workspaces/{workspace}/autoupdates [put]
func (api *API) putWorkspaceAutoupdates(rw http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
workspace = httpmw.WorkspaceParam(r)
auditor = api.Auditor.Load()
aReq, commitAudit = audit.InitRequest[database.Workspace](rw, &audit.RequestParams{
Audit: *auditor,
Log: api.Logger,
Request: r,
Action: database.AuditActionWrite,
})
)
defer commitAudit()
aReq.Old = workspace
var req codersdk.UpdateWorkspaceAutomaticUpdatesRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}
if !database.AutomaticUpdates(req.AutomaticUpdates).Valid() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid request",
Validations: []codersdk.ValidationError{{Field: "automatic_updates", Detail: "must be always or never"}},
})
return
}
err := api.Database.UpdateWorkspaceAutomaticUpdates(ctx, database.UpdateWorkspaceAutomaticUpdatesParams{
ID: workspace.ID,
AutomaticUpdates: database.AutomaticUpdates(req.AutomaticUpdates),
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error updating workspace automatic updates setting",
Detail: err.Error(),
})
return
}
newWorkspace := workspace
newWorkspace.AutomaticUpdates = database.AutomaticUpdates(req.AutomaticUpdates)
aReq.New = newWorkspace
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Watch workspace by ID
// @ID watch-workspace-by-id
// @Security CoderSessionToken
@ -1256,6 +1330,7 @@ func convertWorkspace(
Healthy: len(failingAgents) == 0,
FailingAgents: failingAgents,
},
AutomaticUpdates: codersdk.AutomaticUpdates(workspace.AutomaticUpdates),
}
}
@ -1311,6 +1386,17 @@ func validWorkspaceTTLMillis(millis *int64, templateDefault, templateMax time.Du
}, nil
}
func validWorkspaceAutomaticUpdates(updates codersdk.AutomaticUpdates) (database.AutomaticUpdates, error) {
if updates == "" {
return database.AutomaticUpdatesNever, nil
}
dbAU := database.AutomaticUpdates(updates)
if !dbAU.Valid() {
return "", xerrors.New("Automatic updates must be always or never")
}
return dbAU, nil
}
func validWorkspaceDeadline(startedAt, newDeadline time.Time) error {
soon := time.Now().Add(29 * time.Minute)
if newDeadline.Before(soon) {

View File

@ -729,6 +729,7 @@ func TestWorkspaceByOwnerAndName(t *testing.T) {
Name: workspace.Name,
AutostartSchedule: workspace.AutostartSchedule,
TTLMillis: workspace.TTLMillis,
AutomaticUpdates: workspace.AutomaticUpdates,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
@ -2173,6 +2174,72 @@ func TestWorkspaceExtend(t *testing.T) {
require.WithinDuration(t, oldDeadline.Add(-time.Hour), updated.LatestBuild.Deadline.Time, time.Minute)
}
func TestWorkspaceUpdateAutomaticUpdates_OK(t *testing.T) {
t.Parallel()
var (
auditor = audit.NewMock()
adminClient = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
admin = coderdtest.CreateFirstUser(t, adminClient)
client, user = coderdtest.CreateAnotherUser(t, adminClient, admin.OrganizationID)
version = coderdtest.CreateTemplateVersion(t, adminClient, admin.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
project = coderdtest.CreateTemplate(t, adminClient, admin.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, admin.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = nil
cwr.TTLMillis = nil
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesNever
})
)
// await job to ensure audit logs for workspace_build start are created
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// ensure test invariant: new workspaces have automatic updates set to never
require.Equal(t, codersdk.AutomaticUpdatesNever, workspace.AutomaticUpdates, "expected newly-minted workspace to automatic updates set to never")
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
err := client.UpdateWorkspaceAutomaticUpdates(ctx, workspace.ID, codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
AutomaticUpdates: codersdk.AutomaticUpdatesAlways,
})
require.NoError(t, err)
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, codersdk.AutomaticUpdatesAlways, updated.AutomaticUpdates)
require.Eventually(t, func() bool {
return len(auditor.AuditLogs()) >= 9
}, testutil.WaitShort, testutil.IntervalFast)
l := auditor.AuditLogs()[8]
require.Equal(t, database.AuditActionWrite, l.Action)
require.Equal(t, user.ID, l.UserID)
require.Equal(t, workspace.ID, l.ResourceID)
}
func TestUpdateWorkspaceAutomaticUpdates_NotFound(t *testing.T) {
t.Parallel()
var (
client = coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
wsid = uuid.New()
req = codersdk.UpdateWorkspaceAutomaticUpdatesRequest{
AutomaticUpdates: codersdk.AutomaticUpdatesNever,
}
)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
err := client.UpdateWorkspaceAutomaticUpdates(ctx, wsid, req)
require.IsType(t, err, &codersdk.Error{}, "expected codersdk.Error")
coderSDKErr, _ := err.(*codersdk.Error) //nolint:errorlint
require.Equal(t, coderSDKErr.StatusCode(), 404, "expected status code 404")
require.Contains(t, coderSDKErr.Message, "Resource not found", "unexpected response code")
}
func TestWorkspaceWatcher(t *testing.T) {
t.Parallel()
client, closeFunc := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{IncludeProvisionerDaemon: true})

View File

@ -136,6 +136,7 @@ type CreateWorkspaceRequest struct {
// RichParameterValues allows for additional parameters to be provided
// during the initial provision.
RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"`
AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"`
}
func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {

View File

@ -14,6 +14,13 @@ import (
"github.com/coder/coder/v2/coderd/tracing"
)
type AutomaticUpdates string
const (
AutomaticUpdatesAlways AutomaticUpdates = "always"
AutomaticUpdatesNever AutomaticUpdates = "never"
)
// Workspace is a deployment of a template. It references a specific
// version and can be updated.
type Workspace struct {
@ -47,7 +54,8 @@ type Workspace struct {
DormantAt *time.Time `json:"dormant_at" format:"date-time"`
// Health shows the health of the workspace and information about
// what is causing an unhealthy status.
Health WorkspaceHealth `json:"health"`
Health WorkspaceHealth `json:"health"`
AutomaticUpdates AutomaticUpdates `json:"automatic_updates" enums:"always,never"`
}
func (w Workspace) FullName() string {
@ -316,6 +324,25 @@ func (c *Client) UpdateWorkspaceDormancy(ctx context.Context, id uuid.UUID, req
return nil
}
// UpdateWorkspaceAutomaticUpdatesRequest is a request to updates a workspace's automatic updates setting.
type UpdateWorkspaceAutomaticUpdatesRequest struct {
AutomaticUpdates AutomaticUpdates `json:"automatic_updates"`
}
// UpdateWorkspaceAutomaticUpdates sets the automatic updates setting for workspace by id.
func (c *Client) UpdateWorkspaceAutomaticUpdates(ctx context.Context, id uuid.UUID, req UpdateWorkspaceAutomaticUpdatesRequest) error {
path := fmt.Sprintf("/api/v2/workspaces/%s/autoupdates", id.String())
res, err := c.Request(ctx, http.MethodPut, path, req)
if err != nil {
return xerrors.Errorf("update workspace automatic updates: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
type WorkspaceFilter struct {
// Owner can be "me" or a username
Owner string `json:"owner,omitempty" typescript:"-"`

View File

@ -18,7 +18,7 @@ We track the following resources:
| 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>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> |
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><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>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> |
| 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> |
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody><tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>provisioner_state</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
| WorkspaceProxy<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>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |

87
docs/api/schemas.md generated
View File

@ -1342,6 +1342,21 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| ---------------- | ------- | -------- | ------------ | ----------- |
| `[any property]` | boolean | false | | |
## codersdk.AutomaticUpdates
```json
"always"
```
### Properties
#### Enumerated Values
| Value |
| -------- |
| `always` |
| `never` |
## codersdk.BuildInfoResponse
```json
@ -1757,6 +1772,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"name": "string",
"rich_parameter_values": [
@ -1775,6 +1791,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| Name | Type | Required | Restrictions | Description |
| ----------------------- | ----------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------- |
| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | |
| `autostart_schedule` | string | false | | |
| `name` | string | true | | |
| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. |
@ -5071,6 +5088,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
The schedule must be daily with a single time, and should have a timezone specified via a CRON_TZ prefix (otherwise UTC will be used).
If the schedule is empty, the user will be updated to use the default schedule.|
## codersdk.UpdateWorkspaceAutomaticUpdatesRequest
```json
{
"automatic_updates": "always"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------- | ------------------------------------------------------ | -------- | ------------ | ----------- |
| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | |
## codersdk.UpdateWorkspaceAutostartRequest
```json
@ -5465,6 +5496,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
@ -5639,29 +5671,37 @@ If the schedule is empty, the user will be updated to use the default schedule.|
### Properties
| Name | Type | Required | Restrictions | Description |
| ------------------------------------------- | ---------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `autostart_schedule` | string | false | | |
| `created_at` | string | false | | |
| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. |
| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time*til* field on its template. |
| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. |
| `id` | string | false | | |
| `last_used_at` | string | false | | |
| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `outdated` | boolean | false | | |
| `owner_id` | string | false | | |
| `owner_name` | string | false | | |
| `template_active_version_id` | string | false | | |
| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
| `template_display_name` | string | false | | |
| `template_icon` | string | false | | |
| `template_id` | string | false | | |
| `template_name` | string | false | | |
| `ttl_ms` | integer | false | | |
| `updated_at` | string | false | | |
| Name | Type | Required | Restrictions | Description |
| ------------------------------------------- | ------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | |
| `autostart_schedule` | string | false | | |
| `created_at` | string | false | | |
| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. |
| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time*til* field on its template. |
| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. |
| `id` | string | false | | |
| `last_used_at` | string | false | | |
| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | |
| `name` | string | false | | |
| `organization_id` | string | false | | |
| `outdated` | boolean | false | | |
| `owner_id` | string | false | | |
| `owner_name` | string | false | | |
| `template_active_version_id` | string | false | | |
| `template_allow_user_cancel_workspace_jobs` | boolean | false | | |
| `template_display_name` | string | false | | |
| `template_icon` | string | false | | |
| `template_id` | string | false | | |
| `template_name` | string | false | | |
| `ttl_ms` | integer | false | | |
| `updated_at` | string | false | | |
#### Enumerated Values
| Property | Value |
| ------------------- | -------- |
| `automatic_updates` | `always` |
| `automatic_updates` | `never` |
## codersdk.WorkspaceAgent
@ -6708,6 +6748,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"count": 0,
"workspaces": [
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",

42
docs/api/workspaces.md generated
View File

@ -18,6 +18,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"name": "string",
"rich_parameter_values": [
@ -46,6 +47,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
@ -253,6 +255,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
@ -463,6 +466,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"count": 0,
"workspaces": [
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
@ -667,6 +671,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",
@ -919,6 +924,42 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/autostart \
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Update workspace automatic updates by ID
### Code samples
```shell
# Example request using curl
curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/autoupdates \
-H 'Content-Type: application/json' \
-H 'Coder-Session-Token: API_KEY'
```
`PUT /workspaces/{workspace}/autoupdates`
> Body parameter
```json
{
"automatic_updates": "always"
}
```
### Parameters
| Name | In | Type | Required | Description |
| ----------- | ---- | ------------------------------------------------------------------------------------------------------------ | -------- | ------------------------- |
| `workspace` | path | string(uuid) | true | Workspace ID |
| `body` | body | [codersdk.UpdateWorkspaceAutomaticUpdatesRequest](schemas.md#codersdkupdateworkspaceautomaticupdatesrequest) | true | Automatic updates request |
### Responses
| Status | Meaning | Description | Schema |
| ------ | --------------------------------------------------------------- | ----------- | ------ |
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Update workspace dormancy status by id.
### Code samples
@ -954,6 +995,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
```json
{
"automatic_updates": "always",
"autostart_schedule": "string",
"created_at": "2019-08-24T14:15:22Z",
"deleting_at": "2019-08-24T14:15:22Z",

10
docs/cli/create.md generated
View File

@ -20,6 +20,16 @@ coder create [flags] [name]
## Options
### --automatic-updates
| | |
| ----------- | ----------------------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_WORKSPACE_AUTOMATIC_UPDATES</code> |
| Default | <code>never</code> |
Specify automatic updates setting for the workspace (accepts 'always' or 'never').
### --parameter
| | |

View File

@ -129,6 +129,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
"last_used_at": ActionIgnore,
"dormant_at": ActionTrack,
"deleting_at": ActionTrack,
"automatic_updates": ActionTrack,
},
&database.WorkspaceBuild{}: {
"id": ActionIgnore,

View File

@ -279,6 +279,7 @@ export interface CreateWorkspaceRequest {
readonly autostart_schedule?: string;
readonly ttl_ms?: number;
readonly rich_parameter_values?: WorkspaceBuildParameter[];
readonly automatic_updates?: AutomaticUpdates;
}
// From codersdk/deployment.go
@ -1155,6 +1156,11 @@ export interface UpdateUserQuietHoursScheduleRequest {
readonly schedule: string;
}
// From codersdk/workspaces.go
export interface UpdateWorkspaceAutomaticUpdatesRequest {
readonly automatic_updates: AutomaticUpdates;
}
// From codersdk/workspaces.go
export interface UpdateWorkspaceAutostartRequest {
readonly schedule?: string;
@ -1323,6 +1329,7 @@ export interface Workspace {
readonly deleting_at?: string;
readonly dormant_at?: string;
readonly health: WorkspaceHealth;
readonly automatic_updates: AutomaticUpdates;
}
// From codersdk/workspaceagents.go
@ -1611,6 +1618,10 @@ export const AuditActions: AuditAction[] = [
"write",
];
// From codersdk/workspaces.go
export type AutomaticUpdates = "always" | "never";
export const AutomaticUpdateses: AutomaticUpdates[] = ["always", "never"];
// From codersdk/workspacebuilds.go
export type BuildReason = "autostart" | "autostop" | "initiator";
export const BuildReasons: BuildReason[] = [

View File

@ -956,6 +956,7 @@ export const MockWorkspace: TypesGen.Workspace = {
healthy: true,
failing_agents: [],
},
automatic_updates: "never",
};
export const MockStoppedWorkspace: TypesGen.Workspace = {