Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commite8495ed

Browse files
committed
feat: add lifecycle.Executor to autostart workspaces.
1 parentf9ce54a commite8495ed

File tree

8 files changed

+440
-2
lines changed

8 files changed

+440
-2
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package lifecycle
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"time"
7+
8+
"cdr.dev/slog"
9+
10+
"github.com/coder/coder/coderd/autostart/schedule"
11+
"github.com/coder/coder/coderd/database"
12+
"github.com/coder/coder/codersdk"
13+
14+
"github.com/google/uuid"
15+
"github.com/moby/moby/pkg/namesgenerator"
16+
"golang.org/x/xerrors"
17+
)
18+
19+
//var ExecutorUUID = uuid.MustParse("00000000-0000-0000-0000-000000000000")
20+
21+
// Executor executes automated workspace lifecycle operations.
22+
typeExecutorstruct {
23+
ctx context.Context
24+
db database.Store
25+
log slog.Logger
26+
tick<-chan time.Time
27+
}
28+
29+
funcNewExecutor(ctx context.Context,db database.Store,log slog.Logger,tick<-chan time.Time)*Executor {
30+
le:=&Executor{
31+
ctx:ctx,
32+
db:db,
33+
tick:tick,
34+
log:log,
35+
}
36+
returnle
37+
}
38+
39+
func (e*Executor)Run()error {
40+
for {
41+
select {
42+
caset:=<-e.tick:
43+
iferr:=e.runOnce(t);err!=nil {
44+
e.log.Error(e.ctx,"error running once",slog.Error(err))
45+
}
46+
case<-e.ctx.Done():
47+
returnnil
48+
default:
49+
}
50+
}
51+
}
52+
53+
func (e*Executor)runOnce(t time.Time)error {
54+
currentTick:=t.Round(time.Minute)
55+
returne.db.InTx(func(db database.Store)error {
56+
allWorkspaces,err:=db.GetWorkspaces(e.ctx)
57+
iferr!=nil {
58+
returnxerrors.Errorf("get all workspaces: %w",err)
59+
}
60+
61+
for_,ws:=rangeallWorkspaces {
62+
// We only care about workspaces with autostart enabled.
63+
ifws.AutostartSchedule.String=="" {
64+
continue
65+
}
66+
sched,err:=schedule.Weekly(ws.AutostartSchedule.String)
67+
iferr!=nil {
68+
e.log.Warn(e.ctx,"workspace has invalid autostart schedule",
69+
slog.F("workspace_id",ws.ID),
70+
slog.F("autostart_schedule",ws.AutostartSchedule.String),
71+
)
72+
continue
73+
}
74+
75+
// Determine the workspace state based on its latest build. We expect it to be stopped.
76+
// TODO(cian): is this **guaranteed** to be the latest build???
77+
latestBuild,err:=db.GetWorkspaceBuildByWorkspaceIDWithoutAfter(e.ctx,ws.ID)
78+
iferr!=nil {
79+
returnxerrors.Errorf("get latest build for workspace %q: %w",ws.ID,err)
80+
}
81+
iflatestBuild.Transition!=database.WorkspaceTransitionStop {
82+
e.log.Debug(e.ctx,"autostart: skipping workspace: wrong transition",
83+
slog.F("transition",latestBuild.Transition),
84+
slog.F("workspace_id",ws.ID),
85+
)
86+
continue
87+
}
88+
89+
// Round time to the nearest minute, as this is the finest granularity cron supports.
90+
earliestAutostart:=sched.Next(latestBuild.CreatedAt).Round(time.Minute)
91+
ifearliestAutostart.After(currentTick) {
92+
e.log.Debug(e.ctx,"autostart: skipping workspace: too early",
93+
slog.F("workspace_id",ws.ID),
94+
slog.F("earliest_autostart",earliestAutostart),
95+
slog.F("current_tick",currentTick),
96+
)
97+
continue
98+
}
99+
100+
e.log.Info(e.ctx,"autostart: scheduling workspace start",
101+
slog.F("workspace_id",ws.ID),
102+
)
103+
104+
iferr:=doBuild(e.ctx,db,ws,currentTick);err!=nil {
105+
e.log.Error(e.ctx,"autostart workspace",slog.F("workspace_id",ws.ID),slog.Error(err))
106+
}
107+
}
108+
returnnil
109+
})
110+
}
111+
112+
// XXX: cian: this function shouldn't really exist. Refactor.
113+
funcdoBuild(ctx context.Context,store database.Store,workspace database.Workspace,now time.Time)error {
114+
template,err:=store.GetTemplateByID(ctx,workspace.TemplateID)
115+
iferr!=nil {
116+
returnxerrors.Errorf("get template: %w",err)
117+
}
118+
119+
priorHistory,err:=store.GetWorkspaceBuildByWorkspaceIDWithoutAfter(ctx,workspace.ID)
120+
priorJob,err:=store.GetProvisionerJobByID(ctx,priorHistory.JobID)
121+
iferr==nil&&!priorJob.CompletedAt.Valid {
122+
returnxerrors.Errorf("workspace build already active")
123+
}
124+
125+
priorHistoryID:= uuid.NullUUID{
126+
UUID:priorHistory.ID,
127+
Valid:true,
128+
}
129+
130+
varnewWorkspaceBuild database.WorkspaceBuild
131+
// This must happen in a transaction to ensure history can be inserted, and
132+
// the prior history can update it's "after" column to point at the new.
133+
workspaceBuildID:=uuid.New()
134+
input,err:=json.Marshal(struct {
135+
WorkspaceBuildIDstring`json:"workspace_build_id"`
136+
}{
137+
WorkspaceBuildID:workspaceBuildID.String(),
138+
})
139+
iferr!=nil {
140+
returnxerrors.Errorf("marshal provision job: %w",err)
141+
}
142+
provisionerJobID:=uuid.New()
143+
newProvisionerJob,err:=store.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{
144+
ID:provisionerJobID,
145+
CreatedAt:database.Now(),
146+
UpdatedAt:database.Now(),
147+
InitiatorID:workspace.OwnerID,
148+
OrganizationID:template.OrganizationID,
149+
Provisioner:template.Provisioner,
150+
Type:database.ProvisionerJobTypeWorkspaceBuild,
151+
StorageMethod:priorJob.StorageMethod,
152+
StorageSource:priorJob.StorageSource,
153+
Input:input,
154+
})
155+
iferr!=nil {
156+
returnxerrors.Errorf("insert provisioner job: %w",err)
157+
}
158+
newWorkspaceBuild,err=store.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{
159+
ID:workspaceBuildID,
160+
CreatedAt:database.Now(),
161+
UpdatedAt:database.Now(),
162+
WorkspaceID:workspace.ID,
163+
TemplateVersionID:priorHistory.TemplateVersionID,
164+
BeforeID:priorHistoryID,
165+
Name:namesgenerator.GetRandomName(1),
166+
ProvisionerState:priorHistory.ProvisionerState,
167+
InitiatorID:workspace.OwnerID,
168+
Transition:database.WorkspaceTransitionStart,
169+
JobID:newProvisionerJob.ID,
170+
})
171+
iferr!=nil {
172+
returnxerrors.Errorf("insert workspace build: %w",err)
173+
}
174+
175+
ifpriorHistoryID.Valid {
176+
// Update the prior history entries "after" column.
177+
err=store.UpdateWorkspaceBuildByID(ctx, database.UpdateWorkspaceBuildByIDParams{
178+
ID:priorHistory.ID,
179+
ProvisionerState:priorHistory.ProvisionerState,
180+
UpdatedAt:database.Now(),
181+
AfterID: uuid.NullUUID{
182+
UUID:newWorkspaceBuild.ID,
183+
Valid:true,
184+
},
185+
})
186+
iferr!=nil {
187+
returnxerrors.Errorf("update prior workspace build: %w",err)
188+
}
189+
}
190+
returnnil
191+
}
192+
193+
funcprovisionerJobStatus(j database.ProvisionerJob,now time.Time) codersdk.ProvisionerJobStatus {
194+
switch {
195+
casej.CanceledAt.Valid:
196+
ifj.CompletedAt.Valid {
197+
returncodersdk.ProvisionerJobCanceled
198+
}
199+
returncodersdk.ProvisionerJobCanceling
200+
case!j.StartedAt.Valid:
201+
returncodersdk.ProvisionerJobPending
202+
casej.CompletedAt.Valid:
203+
ifj.Error.String=="" {
204+
returncodersdk.ProvisionerJobSucceeded
205+
}
206+
returncodersdk.ProvisionerJobFailed
207+
casenow.Sub(j.UpdatedAt)>30*time.Second:
208+
returncodersdk.ProvisionerJobFailed
209+
default:
210+
returncodersdk.ProvisionerJobRunning
211+
}
212+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package lifecycle_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"cdr.dev/slog"
9+
"cdr.dev/slog/sloggers/slogtest"
10+
11+
"github.com/coder/coder/coderd/autostart/lifecycle"
12+
"github.com/coder/coder/coderd/autostart/schedule"
13+
"github.com/coder/coder/coderd/coderdtest"
14+
"github.com/coder/coder/coderd/database"
15+
"github.com/coder/coder/coderd/database/databasefake"
16+
"github.com/coder/coder/codersdk"
17+
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
funcTest_Executor_Run(t*testing.T) {
22+
t.Parallel()
23+
24+
t.Run("OK",func(t*testing.T) {
25+
t.Parallel()
26+
27+
var (
28+
ctx=context.Background()
29+
cancelCtx,cancel=context.WithCancel(context.Background())
30+
log=slogtest.Make(t,nil).Named("lifecycle.executor").Leveled(slog.LevelDebug)
31+
errerror
32+
tickCh=make(chan time.Time)
33+
db=databasefake.New()
34+
le=lifecycle.NewExecutor(cancelCtx,db,log,tickCh)
35+
client=coderdtest.New(t,&coderdtest.Options{
36+
LifecycleExecutor:le,
37+
Store:db,
38+
})
39+
// Given: we have a user with a workspace
40+
_=coderdtest.NewProvisionerDaemon(t,client)
41+
user=coderdtest.CreateFirstUser(t,client)
42+
version=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
43+
template=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
44+
_=coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
45+
workspace=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
46+
_=coderdtest.AwaitWorkspaceBuildJob(t,client,workspace.LatestBuild.ID)
47+
)
48+
// Given: workspace is stopped
49+
build,err:=client.CreateWorkspaceBuild(ctx,workspace.ID, codersdk.CreateWorkspaceBuildRequest{
50+
TemplateVersionID:template.ActiveVersionID,
51+
Transition:database.WorkspaceTransitionStop,
52+
})
53+
require.NoError(t,err,"stop workspace")
54+
// Given: we wait for the stop to complete
55+
_=coderdtest.AwaitWorkspaceBuildJob(t,client,build.ID)
56+
57+
// Given: we update the workspace with its new state
58+
workspace=coderdtest.MustWorkspace(t,client,workspace.ID)
59+
// Given: we ensure the workspace is now in a stopped state
60+
require.Equal(t,database.WorkspaceTransitionStop,workspace.LatestBuild.Transition)
61+
62+
// Given: the workspace initially has autostart disabled
63+
require.Empty(t,workspace.AutostartSchedule)
64+
65+
// When: we enable workspace autostart
66+
sched,err:=schedule.Weekly("* * * * *")
67+
require.NoError(t,err)
68+
require.NoError(t,client.UpdateWorkspaceAutostart(ctx,workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
69+
Schedule:sched.String(),
70+
}))
71+
72+
// When: the lifecycle executor ticks
73+
gofunc() {
74+
tickCh<-time.Now().UTC().Add(time.Minute)
75+
cancel()
76+
}()
77+
require.NoError(t,le.Run())
78+
79+
// Then: the workspace should be started
80+
require.Eventually(t,func()bool {
81+
ws:=coderdtest.MustWorkspace(t,client,workspace.ID)
82+
returnws.LatestBuild.Job.Status==codersdk.ProvisionerJobSucceeded&&
83+
ws.LatestBuild.Transition==database.WorkspaceTransitionStart
84+
},10*time.Second,1000*time.Millisecond)
85+
})
86+
87+
t.Run("AlreadyRunning",func(t*testing.T) {
88+
t.Parallel()
89+
90+
var (
91+
ctx=context.Background()
92+
cancelCtx,cancel=context.WithCancel(context.Background())
93+
log=slogtest.Make(t,nil).Named("lifecycle.executor").Leveled(slog.LevelDebug)
94+
errerror
95+
tickCh=make(chan time.Time)
96+
db=databasefake.New()
97+
le=lifecycle.NewExecutor(cancelCtx,db,log,tickCh)
98+
client=coderdtest.New(t,&coderdtest.Options{
99+
LifecycleExecutor:le,
100+
Store:db,
101+
})
102+
// Given: we have a user with a workspace
103+
_=coderdtest.NewProvisionerDaemon(t,client)
104+
user=coderdtest.CreateFirstUser(t,client)
105+
version=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,nil)
106+
template=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
107+
_=coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
108+
workspace=coderdtest.CreateWorkspace(t,client,user.OrganizationID,template.ID)
109+
_=coderdtest.AwaitWorkspaceBuildJob(t,client,workspace.LatestBuild.ID)
110+
)
111+
112+
// Given: we ensure the workspace is now in a stopped state
113+
require.Equal(t,database.WorkspaceTransitionStart,workspace.LatestBuild.Transition)
114+
115+
// Given: the workspace initially has autostart disabled
116+
require.Empty(t,workspace.AutostartSchedule)
117+
118+
// When: we enable workspace autostart
119+
sched,err:=schedule.Weekly("* * * * *")
120+
require.NoError(t,err)
121+
require.NoError(t,client.UpdateWorkspaceAutostart(ctx,workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
122+
Schedule:sched.String(),
123+
}))
124+
125+
// When: the lifecycle executor ticks
126+
gofunc() {
127+
tickCh<-time.Now().UTC().Add(time.Minute)
128+
cancel()
129+
}()
130+
require.NoError(t,le.Run())
131+
132+
// Then: the workspace should not be started.
133+
require.Never(t,func()bool {
134+
ws:=coderdtest.MustWorkspace(t,client,workspace.ID)
135+
returnws.LatestBuild.ID!=workspace.LatestBuild.ID
136+
},10*time.Second,1000*time.Millisecond)
137+
})
138+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp