mirror of https://github.com/coder/coder.git
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:
parent
d24d2d2c8d
commit
983e8c3ae8
|
@ -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()...)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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".
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"health": {
|
||||
"healthy": true,
|
||||
"failing_agents": []
|
||||
}
|
||||
},
|
||||
"automatic_updates": "never"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
BEGIN;
|
||||
ALTER TABLE workspaces DROP COLUMN IF EXISTS automatic_updates;
|
||||
DROP TYPE IF EXISTS automatic_updates;
|
||||
COMMIT;
|
|
@ -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;
|
|
@ -358,6 +358,7 @@ func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
|
|||
LastUsedAt: r.LastUsedAt,
|
||||
DormantAt: r.DormantAt,
|
||||
DeletingAt: r.DeletingAt,
|
||||
AutomaticUpdates: r.AutomaticUpdates,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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:"-"`
|
||||
|
|
|
@ -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> |
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
| | |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[] = [
|
||||
|
|
|
@ -956,6 +956,7 @@ export const MockWorkspace: TypesGen.Workspace = {
|
|||
healthy: true,
|
||||
failing_agents: [],
|
||||
},
|
||||
automatic_updates: "never",
|
||||
};
|
||||
|
||||
export const MockStoppedWorkspace: TypesGen.Workspace = {
|
||||
|
|
Loading…
Reference in New Issue