Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cf033aa

Browse files
committed
feat: finish implementing workspace planning
1 parent 3e9ecc3 commit cf033aa

File tree

8 files changed

+291
-37
lines changed

8 files changed

+291
-37
lines changed

cli/cliui/provisionerjob.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cliui
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
@@ -35,6 +36,9 @@ type ProvisionerJobOptions struct {
3536
FetchInterval time.Duration
3637
// Verbose determines whether debug and trace logs will be shown.
3738
Verbose bool
39+
// Silent determines whether log output will be shown unless there is an
40+
// error.
41+
Silent bool
3842
}
3943

4044
// ProvisionerJob renders a provisioner job with interactive cancellation.
@@ -133,12 +137,30 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
133137
return xerrors.Errorf("logs: %w", err)
134138
}
135139

140+
var (
141+
// logOutput is where log output is written
142+
logOutput = writer
143+
// logBuffer is where logs are buffered if opts.Silent is true
144+
logBuffer = &bytes.Buffer{}
145+
)
146+
if opts.Silent {
147+
logOutput = logBuffer
148+
}
149+
flushLogBuffer := func() {
150+
if opts.Silent {
151+
_, _ = io.Copy(writer, logBuffer)
152+
}
153+
}
154+
136155
ticker := time.NewTicker(opts.FetchInterval)
156+
defer ticker.Stop()
137157
for {
138158
select {
139159
case err = <-errChan:
160+
flushLogBuffer()
140161
return err
141162
case <-ctx.Done():
163+
flushLogBuffer()
142164
return ctx.Err()
143165
case <-ticker.C:
144166
updateJob()
@@ -160,8 +182,10 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
160182
}
161183
err = xerrors.New(job.Error)
162184
jobMutex.Unlock()
185+
flushLogBuffer()
163186
return err
164187
}
188+
165189
output := ""
166190
switch log.Level {
167191
case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
@@ -176,14 +200,17 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
176200
case codersdk.LogLevelInfo:
177201
output = log.Output
178202
}
203+
179204
jobMutex.Lock()
180205
if log.Stage != currentStage && log.Stage != "" {
181206
updateStage(log.Stage, log.CreatedAt)
182207
jobMutex.Unlock()
183208
continue
184209
}
185-
_, _ = fmt.Fprintf(writer, "%s %s\n", Styles.Placeholder.Render(" "), output)
186-
didLogBetweenStage = true
210+
_, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
211+
if !opts.Silent {
212+
didLogBetweenStage = true
213+
}
187214
jobMutex.Unlock()
188215
}
189216
}

cli/create.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -170,44 +170,47 @@ func create() *cobra.Command {
170170
}
171171
_, _ = fmt.Fprintln(cmd.OutOrStdout())
172172

173-
resources, err := client.TemplateVersionResources(cmd.Context(), templateVersion.ID)
174-
if err != nil {
175-
return err
176-
}
177-
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
178-
WorkspaceName: workspaceName,
179-
// Since agent's haven't connected yet, hiding this makes more sense.
180-
HideAgentState: true,
181-
Title: "Workspace Preview",
182-
})
183-
if err != nil {
184-
return err
185-
}
186-
187173
// Run a plan with the given parameters to check correctness
188-
planJob, err := client.TemplateVersionPlan(cmd.Context(), templateVersion.ID, codersdk.TemplateVersionPlanRequest{
174+
after := time.Now()
175+
planJob, err := client.CreateTemplateVersionPlan(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionPlanRequest{
189176
ParameterValues: parameters,
190177
})
191178
if err != nil {
192-
return xerrors.Errorf("plan workspace: %w", err)
179+
return xerrors.Errorf("begin workspace plan: %w", err)
193180
}
181+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
194182
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
195183
Fetch: func() (codersdk.ProvisionerJob, error) {
196-
return planJob, nil
184+
return client.TemplateVersionPlan(cmd.Context(), templateVersion.ID, planJob.ID)
197185
},
198186
Cancel: func() error {
199-
// TODO: workspace plan cancellation endpoint
200-
return nil
187+
return client.CancelTemplateVersionPlan(cmd.Context(), templateVersion.ID, planJob.ID)
201188
},
202189
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
203-
// TODO: workspace plan log endpoint
204-
return make(chan codersdk.ProvisionerJobLog), nil
190+
return client.TemplateVersionPlanLogsAfter(cmd.Context(), templateVersion.ID, planJob.ID, after)
205191
},
192+
// Don't show log output for the plan unless there's an error.
193+
Silent: true,
206194
})
207195
if err != nil {
208196
// TODO: reprompt for parameter values if we deem it to be a
209197
// validation error
210-
return xerrors.Errorf("error occurred during workspace plan: %w", err)
198+
return xerrors.Errorf("plan workspace: %w", err)
199+
}
200+
201+
resources, err := client.TemplateVersionPlanResources(cmd.Context(), templateVersion.ID, planJob.ID)
202+
if err != nil {
203+
return xerrors.Errorf("get workspace plan resources: %w", err)
204+
}
205+
206+
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
207+
WorkspaceName: workspaceName,
208+
// Since agent's haven't connected yet, hiding this makes more sense.
209+
HideAgentState: true,
210+
Title: "Workspace Preview",
211+
})
212+
if err != nil {
213+
return err
211214
}
212215

213216
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
@@ -218,7 +221,6 @@ func create() *cobra.Command {
218221
return err
219222
}
220223

221-
before := time.Now()
222224
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{
223225
TemplateID: template.ID,
224226
Name: workspaceName,
@@ -230,7 +232,7 @@ func create() *cobra.Command {
230232
return err
231233
}
232234

233-
err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, workspace.LatestBuild.ID, before)
235+
err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, workspace.LatestBuild.ID, after)
234236
if err != nil {
235237
return err
236238
}

coderd/coderd.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,13 @@ func newRouter(options *Options, a *api) chi.Router {
208208
r.Get("/parameters", a.templateVersionParameters)
209209
r.Get("/resources", a.templateVersionResources)
210210
r.Get("/logs", a.templateVersionLogs)
211-
r.Post("/plan", a.templateVersionPlan)
211+
r.Route("/plan", func(r chi.Router) {
212+
r.Post("/", a.createTemplateVersionPlan)
213+
r.Get("/{jobID}", a.templateVersionPlan)
214+
r.Get("/{jobID}/resources", a.templateVersionPlanResources)
215+
r.Get("/{jobID}/logs", a.templateVersionPlanLogs)
216+
r.Patch("/{jobID}/cancel", a.templateVersionPlanCancel)
217+
})
212218
})
213219
r.Route("/users", func(r chi.Router) {
214220
r.Get("/first", a.firstUser)

coderd/provisionerdaemons.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,35 @@ func (server *provisionerdServer) CompleteJob(ctx context.Context, completed *pr
628628
if err != nil {
629629
return nil, xerrors.Errorf("complete job: %w", err)
630630
}
631+
case *proto.CompletedJob_TemplatePlan_:
632+
for _, resource := range jobType.TemplatePlan.Resources {
633+
server.Logger.Info(ctx, "inserting template plan job resource",
634+
slog.F("job_id", job.ID.String()),
635+
slog.F("resource_name", resource.Name),
636+
slog.F("resource_type", resource.Type))
637+
638+
err = insertWorkspaceResource(ctx, server.Database, jobID, database.WorkspaceTransitionStart, resource)
639+
if err != nil {
640+
return nil, xerrors.Errorf("insert resource: %w", err)
641+
}
642+
}
643+
644+
err = server.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
645+
ID: jobID,
646+
UpdatedAt: database.Now(),
647+
CompletedAt: sql.NullTime{
648+
Time: database.Now(),
649+
Valid: true,
650+
},
651+
})
652+
if err != nil {
653+
return nil, xerrors.Errorf("update provisioner job: %w", err)
654+
}
655+
server.Logger.Debug(ctx, "marked template plan job as completed", slog.F("job_id", jobID))
656+
if err != nil {
657+
return nil, xerrors.Errorf("complete job: %w", err)
658+
}
659+
631660
default:
632661
return nil, xerrors.Errorf("unknown job type %q; ensure coderd and provisionerd versions match",
633662
reflect.TypeOf(completed.Type).String())

coderd/templateversions.go

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,11 @@ func (api *api) templateVersionParameters(rw http.ResponseWriter, r *http.Reques
147147
httpapi.Write(rw, http.StatusOK, values)
148148
}
149149

150-
func (api *api) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
150+
func (api *api) createTemplateVersionPlan(rw http.ResponseWriter, r *http.Request) {
151151
apiKey := httpmw.APIKey(r)
152-
organization := httpmw.OrganizationParam(r)
153152
templateVersion := httpmw.TemplateVersionParam(r)
154153

155-
var req codersdk.TemplateVersionPlanRequest
154+
var req codersdk.CreateTemplateVersionPlanRequest
156155
if !httpapi.Read(rw, r, &req) {
157156
return
158157
}
@@ -203,7 +202,7 @@ func (api *api) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
203202
ID: jobID,
204203
CreatedAt: database.Now(),
205204
UpdatedAt: database.Now(),
206-
OrganizationID: organization.ID,
205+
OrganizationID: templateVersion.OrganizationID,
207206
InitiatorID: apiKey.UserID,
208207
Provisioner: job.Provisioner,
209208
StorageMethod: job.StorageMethod,
@@ -221,6 +220,124 @@ func (api *api) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
221220
httpapi.Write(rw, http.StatusCreated, convertProvisionerJob(provisionerJob))
222221
}
223222

223+
func (api *api) templateVersionPlan(rw http.ResponseWriter, r *http.Request) {
224+
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
225+
if !ok {
226+
return
227+
}
228+
229+
httpapi.Write(rw, http.StatusOK, convertProvisionerJob(job))
230+
}
231+
232+
func (api *api) templateVersionPlanResources(rw http.ResponseWriter, r *http.Request) {
233+
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
234+
if !ok {
235+
return
236+
}
237+
238+
api.provisionerJobResources(rw, r, job)
239+
}
240+
241+
func (api *api) templateVersionPlanLogs(rw http.ResponseWriter, r *http.Request) {
242+
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
243+
if !ok {
244+
return
245+
}
246+
247+
api.provisionerJobLogs(rw, r, job)
248+
}
249+
250+
func (api *api) templateVersionPlanCancel(rw http.ResponseWriter, r *http.Request) {
251+
job, ok := getTemplateVersionPlanJob(api.Database, rw, r)
252+
if !ok {
253+
return
254+
}
255+
256+
if job.CompletedAt.Valid {
257+
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
258+
Message: "Job has already completed",
259+
})
260+
return
261+
}
262+
if job.CanceledAt.Valid {
263+
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
264+
Message: "Job has already been marked as canceled",
265+
})
266+
return
267+
}
268+
269+
err := api.Database.UpdateProvisionerJobWithCancelByID(r.Context(), database.UpdateProvisionerJobWithCancelByIDParams{
270+
ID: job.ID,
271+
CanceledAt: sql.NullTime{
272+
Time: database.Now(),
273+
Valid: true,
274+
},
275+
})
276+
if err != nil {
277+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
278+
Message: fmt.Sprintf("update provisioner job: %s", err),
279+
})
280+
return
281+
}
282+
283+
httpapi.Write(rw, http.StatusOK, httpapi.Response{
284+
Message: "Job has been marked as canceled",
285+
})
286+
}
287+
288+
func getTemplateVersionPlanJob(db database.Store, rw http.ResponseWriter, r *http.Request) (database.ProvisionerJob, bool) {
289+
var (
290+
apiKey = httpmw.APIKey(r)
291+
templateVersion = httpmw.TemplateVersionParam(r)
292+
jobID = chi.URLParam(r, "jobID")
293+
)
294+
295+
jobUUID, err := uuid.Parse(jobID)
296+
if err != nil {
297+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
298+
Message: "Job ID must be a valid UUID",
299+
})
300+
return database.ProvisionerJob{}, false
301+
}
302+
303+
job, err := db.GetProvisionerJobByID(r.Context(), jobUUID)
304+
if xerrors.Is(err, sql.ErrNoRows) {
305+
httpapi.Forbidden(rw)
306+
return database.ProvisionerJob{}, false
307+
}
308+
if err != nil {
309+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
310+
Message: fmt.Sprintf("get provisioner job by ID %q: %s", jobUUID.String(), err),
311+
})
312+
return database.ProvisionerJob{}, false
313+
}
314+
if job.Type != database.ProvisionerJobTypeTemplateVersionPlan {
315+
httpapi.Forbidden(rw)
316+
return database.ProvisionerJob{}, false
317+
}
318+
// TODO: real RBAC
319+
if job.InitiatorID != apiKey.UserID {
320+
httpapi.Forbidden(rw)
321+
return database.ProvisionerJob{}, false
322+
}
323+
324+
// Verify that the template version is the one used in the request.
325+
var input templateVersionPlanJob
326+
err = json.Unmarshal(job.Input, &input)
327+
if err != nil {
328+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
329+
Message: fmt.Sprintf("unmarshal job metadata: %s", err),
330+
})
331+
return database.ProvisionerJob{}, false
332+
}
333+
if input.TemplateVersionID != templateVersion.ID {
334+
httpapi.Forbidden(rw)
335+
return database.ProvisionerJob{}, false
336+
}
337+
338+
return job, true
339+
}
340+
224341
func (api *api) templateVersionsByTemplate(rw http.ResponseWriter, r *http.Request) {
225342
template := httpmw.TemplateParam(r)
226343

0 commit comments

Comments
 (0)