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

Commitb41750d

Browse files
deansheatherkylecarbs
authored andcommitted
feat: run a terraform plan before creating workspaces with the given template parameters (#1732)
1 parent0c7bc32 commitb41750d

22 files changed

+1422
-218
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
Verbosebool
39+
// Silent determines whether log output will be shown unless there is an
40+
// error.
41+
Silentbool
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
returnxerrors.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+
ifopts.Silent {
147+
logOutput=logBuffer
148+
}
149+
flushLogBuffer:=func() {
150+
ifopts.Silent {
151+
_,_=io.Copy(writer,logBuffer)
152+
}
153+
}
154+
136155
ticker:=time.NewTicker(opts.FetchInterval)
156+
deferticker.Stop()
137157
for {
138158
select {
139159
caseerr=<-errChan:
160+
flushLogBuffer()
140161
returnerr
141162
case<-ctx.Done():
163+
flushLogBuffer()
142164
returnctx.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
returnerr
164187
}
188+
165189
output:=""
166190
switchlog.Level {
167191
casecodersdk.LogLevelTrace,codersdk.LogLevelDebug:
@@ -176,14 +200,17 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
176200
casecodersdk.LogLevelInfo:
177201
output=log.Output
178202
}
203+
179204
jobMutex.Lock()
180205
iflog.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: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,40 @@ func create() *cobra.Command {
170170
}
171171
_,_=fmt.Fprintln(cmd.OutOrStdout())
172172

173-
resources,err:=client.TemplateVersionResources(cmd.Context(),templateVersion.ID)
173+
// Run a dry-run with the given parameters to check correctness
174+
after:=time.Now()
175+
dryRun,err:=client.CreateTemplateVersionDryRun(cmd.Context(),templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
176+
WorkspaceName:workspaceName,
177+
ParameterValues:parameters,
178+
})
174179
iferr!=nil {
175-
returnerr
180+
returnxerrors.Errorf("begin workspace dry-run: %w",err)
181+
}
182+
_,_=fmt.Fprintln(cmd.OutOrStdout(),"Planning workspace...")
183+
err=cliui.ProvisionerJob(cmd.Context(),cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
184+
Fetch:func() (codersdk.ProvisionerJob,error) {
185+
returnclient.TemplateVersionDryRun(cmd.Context(),templateVersion.ID,dryRun.ID)
186+
},
187+
Cancel:func()error {
188+
returnclient.CancelTemplateVersionDryRun(cmd.Context(),templateVersion.ID,dryRun.ID)
189+
},
190+
Logs:func() (<-chan codersdk.ProvisionerJobLog,error) {
191+
returnclient.TemplateVersionDryRunLogsAfter(cmd.Context(),templateVersion.ID,dryRun.ID,after)
192+
},
193+
// Don't show log output for the dry-run unless there's an error.
194+
Silent:true,
195+
})
196+
iferr!=nil {
197+
// TODO (Dean): reprompt for parameter values if we deem it to
198+
// be a validation error
199+
returnxerrors.Errorf("dry-run workspace: %w",err)
176200
}
201+
202+
resources,err:=client.TemplateVersionDryRunResources(cmd.Context(),templateVersion.ID,dryRun.ID)
203+
iferr!=nil {
204+
returnxerrors.Errorf("get workspace dry-run resources: %w",err)
205+
}
206+
177207
err=cliui.WorkspaceResources(cmd.OutOrStdout(),resources, cliui.WorkspaceResourcesOptions{
178208
WorkspaceName:workspaceName,
179209
// Since agent's haven't connected yet, hiding this makes more sense.
@@ -192,7 +222,6 @@ func create() *cobra.Command {
192222
returnerr
193223
}
194224

195-
before:=time.Now()
196225
workspace,err:=client.CreateWorkspace(cmd.Context(),organization.ID, codersdk.CreateWorkspaceRequest{
197226
TemplateID:template.ID,
198227
Name:workspaceName,
@@ -204,7 +233,7 @@ func create() *cobra.Command {
204233
returnerr
205234
}
206235

207-
err=cliui.WorkspaceBuild(cmd.Context(),cmd.OutOrStdout(),client,workspace.LatestBuild.ID,before)
236+
err=cliui.WorkspaceBuild(cmd.Context(),cmd.OutOrStdout(),client,workspace.LatestBuild.ID,after)
208237
iferr!=nil {
209238
returnerr
210239
}

‎cli/create_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli_test
22

33
import (
44
"context"
5+
"database/sql"
56
"fmt"
67
"os"
78
"testing"
@@ -12,6 +13,8 @@ import (
1213

1314
"github.com/coder/coder/cli/clitest"
1415
"github.com/coder/coder/coderd/coderdtest"
16+
"github.com/coder/coder/coderd/database"
17+
"github.com/coder/coder/codersdk"
1518
"github.com/coder/coder/provisioner/echo"
1619
"github.com/coder/coder/provisionersdk/proto"
1720
"github.com/coder/coder/pty/ptytest"
@@ -249,6 +252,7 @@ func TestCreate(t *testing.T) {
249252
<-doneChan
250253
removeTmpDirUntilSuccess(t,tempDir)
251254
})
255+
252256
t.Run("WithParameterFileNotContainingTheValue",func(t*testing.T) {
253257
t.Parallel()
254258
client:=coderdtest.New(t,&coderdtest.Options{IncludeProvisionerD:true})
@@ -279,6 +283,50 @@ func TestCreate(t *testing.T) {
279283
<-doneChan
280284
removeTmpDirUntilSuccess(t,tempDir)
281285
})
286+
287+
t.Run("FailedDryRun",func(t*testing.T) {
288+
t.Parallel()
289+
client,api:=coderdtest.NewWithAPI(t,&coderdtest.Options{IncludeProvisionerD:true})
290+
user:=coderdtest.CreateFirstUser(t,client)
291+
version:=coderdtest.CreateTemplateVersion(t,client,user.OrganizationID,&echo.Responses{
292+
Parse:echo.ParseComplete,
293+
ProvisionDryRun: []*proto.Provision_Response{
294+
{
295+
Type:&proto.Provision_Response_Complete{
296+
Complete:&proto.Provision_Complete{
297+
Error:"test error",
298+
},
299+
},
300+
},
301+
},
302+
})
303+
304+
// The template import job should end up failed, but we need it to be
305+
// succeeded so the dry-run can begin.
306+
version=coderdtest.AwaitTemplateVersionJob(t,client,version.ID)
307+
require.Equal(t,codersdk.ProvisionerJobFailed,version.Job.Status,"job is not failed")
308+
err:=api.Database.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
309+
ID:version.Job.ID,
310+
CompletedAt: sql.NullTime{
311+
Time:time.Now(),
312+
Valid:true,
313+
},
314+
UpdatedAt:time.Now(),
315+
Error: sql.NullString{},
316+
})
317+
require.NoError(t,err,"update provisioner job")
318+
319+
_=coderdtest.CreateTemplate(t,client,user.OrganizationID,version.ID)
320+
cmd,root:=clitest.New(t,"create","test")
321+
clitest.SetupConfig(t,client,root)
322+
pty:=ptytest.New(t)
323+
cmd.SetIn(pty.Input())
324+
cmd.SetOut(pty.Output())
325+
326+
err=cmd.Execute()
327+
require.Error(t,err)
328+
require.ErrorContains(t,err,"dry-run workspace")
329+
})
282330
}
283331

284332
funccreateTestParseResponseWithDefault(defaultValuestring) []*proto.Parse_Response {

‎coderd/coderd.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ func New(options *Options) *API {
207207
r.Get("/parameters",api.templateVersionParameters)
208208
r.Get("/resources",api.templateVersionResources)
209209
r.Get("/logs",api.templateVersionLogs)
210+
r.Route("/dry-run",func(r chi.Router) {
211+
r.Post("/",api.postTemplateVersionDryRun)
212+
r.Get("/{jobID}",api.templateVersionDryRun)
213+
r.Get("/{jobID}/resources",api.templateVersionDryRunResources)
214+
r.Get("/{jobID}/logs",api.templateVersionDryRunLogs)
215+
r.Patch("/{jobID}/cancel",api.patchTemplateVersionDryRunCancel)
216+
})
210217
})
211218
r.Route("/users",func(r chi.Router) {
212219
r.Get("/first",api.firstUser)

‎coderd/coderd_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
9696
require.NoError(t,err,"upload file")
9797
workspaceResources,err:=client.WorkspaceResourcesByBuild(ctx,workspace.LatestBuild.ID)
9898
require.NoError(t,err,"workspace resources")
99+
templateVersionDryRun,err:=client.CreateTemplateVersionDryRun(ctx,version.ID, codersdk.CreateTemplateVersionDryRunRequest{
100+
ParameterValues: []codersdk.CreateParameterRequest{},
101+
})
102+
require.NoError(t,err,"template version dry-run")
99103

100104
// Always fail auth from this point forward
101105
authorizer.AlwaysReturn=rbac.ForbiddenWithInternal(xerrors.New("fake implementation"),nil,nil)
@@ -262,6 +266,27 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
262266
AssertAction:rbac.ActionRead,
263267
AssertObject:rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
264268
},
269+
"POST:/api/v2/templateversions/{templateversion}/dry-run": {
270+
// The first check is to read the template
271+
AssertAction:rbac.ActionRead,
272+
AssertObject:rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
273+
},
274+
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}": {
275+
AssertAction:rbac.ActionRead,
276+
AssertObject:rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
277+
},
278+
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/resources": {
279+
AssertAction:rbac.ActionRead,
280+
AssertObject:rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
281+
},
282+
"GET:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/logs": {
283+
AssertAction:rbac.ActionRead,
284+
AssertObject:rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
285+
},
286+
"PATCH:/api/v2/templateversions/{templateversion}/dry-run/{templateversiondryrun}/cancel": {
287+
AssertAction:rbac.ActionRead,
288+
AssertObject:rbac.ResourceTemplate.InOrg(version.OrganizationID).WithID(template.ID.String()),
289+
},
265290
"GET:/api/v2/provisionerdaemons": {
266291
StatusCode:http.StatusOK,
267292
AssertObject:rbac.ResourceProvisionerDaemon.WithID(provisionerds[0].ID.String()),
@@ -350,6 +375,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
350375
route=strings.ReplaceAll(route,"{hash}",file.Hash)
351376
route=strings.ReplaceAll(route,"{workspaceresource}",workspaceResources[0].ID.String())
352377
route=strings.ReplaceAll(route,"{templateversion}",version.ID.String())
378+
route=strings.ReplaceAll(route,"{templateversiondryrun}",templateVersionDryRun.ID.String())
353379
route=strings.ReplaceAll(route,"{templatename}",template.Name)
354380
// Only checking org scoped params here
355381
route=strings.ReplaceAll(route,"{scope}",string(organizationParam.Scope))

‎coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- It's not possible to drop enum values from enum types, so the UP has "IF NOT
2+
-- EXISTS".
3+
4+
-- Delete all jobs that use the new enum value.
5+
DELETEFROM
6+
provisioner_jobs
7+
WHERE
8+
type='template_version_dry_run'
9+
;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTERTYPE provisioner_job_type
2+
ADD VALUE IF NOT EXISTS'template_version_dry_run';

‎coderd/database/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/parameter/compute.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import (
1313

1414
// ComputeScope targets identifiers to pull parameters from.
1515
typeComputeScopestruct {
16-
TemplateImportJobID uuid.UUID
17-
OrganizationID uuid.UUID
18-
UserID uuid.UUID
19-
TemplateID uuid.NullUUID
20-
WorkspaceID uuid.NullUUID
16+
TemplateImportJobID uuid.UUID
17+
OrganizationID uuid.UUID
18+
UserID uuid.UUID
19+
TemplateID uuid.NullUUID
20+
WorkspaceID uuid.NullUUID
21+
AdditionalParameterValues []database.ParameterValue
2122
}
2223

2324
typeComputeOptionsstruct {
@@ -142,6 +143,14 @@ func Compute(ctx context.Context, db database.Store, scope ComputeScope, options
142143
}
143144
}
144145

146+
// Finally, any additional parameter values declared in the input
147+
for_,v:=rangescope.AdditionalParameterValues {
148+
err=compute.injectSingle(v,false)
149+
iferr!=nil {
150+
returnnil,xerrors.Errorf("inject single parameter value: %w",err)
151+
}
152+
}
153+
145154
values:=make([]ComputedValue,0,len(compute.computedParameterByName))
146155
for_,value:=rangecompute.computedParameterByName {
147156
values=append(values,value)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp