mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: allow disabling autostart and custom autostop for template (#6933)
API only, frontend in upcoming PR.
This commit is contained in:
@ -3,6 +3,7 @@ package executor
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -18,11 +19,12 @@ import (
|
||||
|
||||
// Executor automatically starts or stops workspaces.
|
||||
type Executor struct {
|
||||
ctx context.Context
|
||||
db database.Store
|
||||
log slog.Logger
|
||||
tick <-chan time.Time
|
||||
statsCh chan<- Stats
|
||||
ctx context.Context
|
||||
db database.Store
|
||||
templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
|
||||
log slog.Logger
|
||||
tick <-chan time.Time
|
||||
statsCh chan<- Stats
|
||||
}
|
||||
|
||||
// Stats contains information about one run of Executor.
|
||||
@ -33,13 +35,14 @@ type Stats struct {
|
||||
}
|
||||
|
||||
// New returns a new autobuild executor.
|
||||
func New(ctx context.Context, db database.Store, log slog.Logger, tick <-chan time.Time) *Executor {
|
||||
func New(ctx context.Context, db database.Store, tss *atomic.Pointer[schedule.TemplateScheduleStore], log slog.Logger, tick <-chan time.Time) *Executor {
|
||||
le := &Executor{
|
||||
//nolint:gocritic // Autostart has a limited set of permissions.
|
||||
ctx: dbauthz.AsAutostart(ctx),
|
||||
db: db,
|
||||
tick: tick,
|
||||
log: log,
|
||||
ctx: dbauthz.AsAutostart(ctx),
|
||||
db: db,
|
||||
templateScheduleStore: tss,
|
||||
tick: tick,
|
||||
log: log,
|
||||
}
|
||||
return le
|
||||
}
|
||||
@ -102,21 +105,11 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
// NOTE: If a workspace build is created with a given TTL and then the user either
|
||||
// changes or unsets the TTL, the deadline for the workspace build will not
|
||||
// have changed. This behavior is as expected per #2229.
|
||||
workspaceRows, err := e.db.GetWorkspaces(e.ctx, database.GetWorkspacesParams{
|
||||
Deleted: false,
|
||||
})
|
||||
workspaces, err := e.db.GetWorkspacesEligibleForAutoStartStop(e.ctx, t)
|
||||
if err != nil {
|
||||
e.log.Error(e.ctx, "get workspaces for autostart or autostop", slog.Error(err))
|
||||
return stats
|
||||
}
|
||||
workspaces := database.ConvertWorkspaceRows(workspaceRows)
|
||||
|
||||
var eligibleWorkspaceIDs []uuid.UUID
|
||||
for _, ws := range workspaces {
|
||||
if isEligibleForAutoStartStop(ws) {
|
||||
eligibleWorkspaceIDs = append(eligibleWorkspaceIDs, ws.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// We only use errgroup here for convenience of API, not for early
|
||||
// cancellation. This means we only return nil errors in th eg.Go.
|
||||
@ -124,8 +117,8 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
// Limit the concurrency to avoid overloading the database.
|
||||
eg.SetLimit(10)
|
||||
|
||||
for _, wsID := range eligibleWorkspaceIDs {
|
||||
wsID := wsID
|
||||
for _, ws := range workspaces {
|
||||
wsID := ws.ID
|
||||
log := e.log.With(slog.F("workspace_id", wsID))
|
||||
|
||||
eg.Go(func() error {
|
||||
@ -137,9 +130,6 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
log.Error(e.ctx, "get workspace autostart failed", slog.Error(err))
|
||||
return nil
|
||||
}
|
||||
if !isEligibleForAutoStartStop(ws) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Determine the workspace state based on its latest build.
|
||||
priorHistory, err := db.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
|
||||
@ -148,6 +138,16 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return nil
|
||||
}
|
||||
|
||||
templateSchedule, err := (*(e.templateScheduleStore.Load())).GetTemplateScheduleOptions(e.ctx, db, ws.TemplateID)
|
||||
if err != nil {
|
||||
log.Warn(e.ctx, "get template schedule options", slog.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isEligibleForAutoStartStop(ws, priorHistory, templateSchedule) {
|
||||
return nil
|
||||
}
|
||||
|
||||
priorJob, err := db.GetProvisionerJobByID(e.ctx, priorHistory.JobID)
|
||||
if err != nil {
|
||||
log.Warn(e.ctx, "get last provisioner job for workspace %q: %w", slog.Error(err))
|
||||
@ -198,8 +198,20 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
||||
return stats
|
||||
}
|
||||
|
||||
func isEligibleForAutoStartStop(ws database.Workspace) bool {
|
||||
return !ws.Deleted && (ws.AutostartSchedule.String != "" || ws.Ttl.Int64 > 0)
|
||||
func isEligibleForAutoStartStop(ws database.Workspace, priorHistory database.WorkspaceBuild, templateSchedule schedule.TemplateScheduleOptions) bool {
|
||||
if ws.Deleted {
|
||||
return false
|
||||
}
|
||||
if templateSchedule.UserAutostartEnabled && ws.AutostartSchedule.Valid && ws.AutostartSchedule.String != "" {
|
||||
return true
|
||||
}
|
||||
// Don't check the template schedule to see whether it allows autostop, this
|
||||
// is done during the build when determining the deadline.
|
||||
if priorHistory.Transition == database.WorkspaceTransitionStart && !priorHistory.Deadline.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getNextTransition(
|
||||
|
@ -6,9 +6,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
|
||||
"github.com/coder/coder/coderd/autobuild/executor"
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
@ -18,9 +19,6 @@ import (
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/provisioner/echo"
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExecutorAutostartOK(t *testing.T) {
|
||||
@ -445,7 +443,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
||||
workspace = mustProvisionWorkspace(t, client)
|
||||
)
|
||||
|
||||
// Given: the user changes their mind and decides their workspace should not auto-stop
|
||||
// Given: the user changes their mind and decides their workspace should not autostop
|
||||
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -471,7 +469,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
||||
// Start the workspace again
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart)
|
||||
|
||||
// Given: the user changes their mind again and wants to enable auto-stop
|
||||
// Given: the user changes their mind again and wants to enable autostop
|
||||
newTTL := 8 * time.Hour
|
||||
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: ptr.Ref(newTTL.Milliseconds())})
|
||||
require.NoError(t, err)
|
||||
@ -605,6 +603,51 @@ func TestExecutorAutostartWithParameters(t *testing.T) {
|
||||
mustWorkspaceParameters(t, client, workspace.LatestBuild.ID)
|
||||
}
|
||||
|
||||
func TestExecutorAutostartTemplateDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan executor.Stats)
|
||||
|
||||
client = coderdtest.New(t, &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||||
return schedule.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: false,
|
||||
UserAutostopEnabled: true,
|
||||
DefaultTTL: 0,
|
||||
MaxTTL: 0,
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
// futureTime = time.Now().Add(time.Hour)
|
||||
// futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||||
// Given: we have a user with a workspace configured to autostart some time in the future
|
||||
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)
|
||||
|
||||
// When: the autobuild executor ticks before the next scheduled time
|
||||
go func() {
|
||||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(time.Minute)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
// Then: nothing should happen
|
||||
stats := <-statsCh
|
||||
assert.NoError(t, stats.Error)
|
||||
assert.Len(t, stats.Transitions, 0)
|
||||
}
|
||||
|
||||
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||||
t.Helper()
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
Reference in New Issue
Block a user