Skip to content

Commit 98ad5d4

Browse files
committed
chore: distinct operations for provisioner's tf operations
'parse', 'init', 'plan', 'apply', 'graph' More granular provisioner options.
1 parent eed2f12 commit 98ad5d4

File tree

19 files changed

+2050
-1020
lines changed

19 files changed

+2050
-1020
lines changed

provisioner/echo/serve.go

Lines changed: 262 additions & 41 deletions
Large diffs are not rendered by default.

provisioner/terraform/executor.go

Lines changed: 18 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
283283
func checksumFileCRC32(ctx context.Context, logger slog.Logger, path string) uint32 {
284284
content, err := os.ReadFile(path)
285285
if err != nil {
286-
logger.Debug(ctx, "file %s does not exist or can't be read, skip checksum calculation")
286+
logger.Debug(ctx, fmt.Sprintf("file %s does not exist or can't be read, skip checksum calculation", path))
287287
return 0
288288
}
289289
return crc32.ChecksumIEEE(content)
@@ -332,34 +332,16 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
332332
return nil, xerrors.Errorf("terraform plan: %w", err)
333333
}
334334

335-
// Capture the duration of the call to `terraform graph`.
336-
graphTimings := newTimingAggregator(database.ProvisionerJobTimingStageGraph)
337-
graphTimings.ingest(createGraphTimingsEvent(timingGraphStart))
338-
339-
state, plan, err := e.planResources(ctx, killCtx, planfilePath)
335+
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
340336
if err != nil {
341-
graphTimings.ingest(createGraphTimingsEvent(timingGraphErrored))
342-
return nil, xerrors.Errorf("plan resources: %w", err)
337+
return nil, xerrors.Errorf("show terraform plan file: %w", err)
343338
}
339+
344340
planJSON, err := json.Marshal(plan)
345341
if err != nil {
346342
return nil, xerrors.Errorf("marshal plan: %w", err)
347343
}
348344

349-
graphTimings.ingest(createGraphTimingsEvent(timingGraphComplete))
350-
351-
var moduleFiles []byte
352-
// Skipping modules archiving is useful if the caller does not need it, eg during
353-
// a workspace build. This removes some added costs of sending the modules
354-
// payload back to coderd if coderd is just going to ignore it.
355-
if !req.OmitModuleFiles {
356-
moduleFiles, err = GetModulesArchive(os.DirFS(e.files.WorkDirectory()))
357-
if err != nil {
358-
// TODO: we probably want to persist this error or make it louder eventually
359-
e.logger.Warn(ctx, "failed to archive terraform modules", slog.Error(err))
360-
}
361-
}
362-
363345
// When a prebuild claim attempt is made, log a warning if a resource is due to be replaced, since this will obviate
364346
// the point of prebuilding if the expensive resource is replaced once claimed!
365347
var (
@@ -386,20 +368,19 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
386368
}
387369
}
388370

371+
state, err := ConvertPlanState(plan)
372+
if err != nil {
373+
return nil, xerrors.Errorf("convert plan state: %w", err)
374+
}
375+
389376
// DoneOut must be completed before we aggregate timings to ensure all logs have been processed.
390377
<-doneOut
391378
msg := &proto.PlanComplete{
392-
Parameters: state.Parameters,
393-
Resources: state.Resources,
394-
ExternalAuthProviders: state.ExternalAuthProviders,
395-
Timings: append(e.timings.aggregate(), graphTimings.aggregate()...),
396-
Presets: state.Presets,
397-
Plan: planJSON,
398-
ResourceReplacements: resReps,
399-
ModuleFiles: moduleFiles,
400-
HasAiTasks: state.HasAITasks,
401-
AiTasks: state.AITasks,
402-
HasExternalAgents: state.HasExternalAgents,
379+
Timings: e.timings.aggregate(),
380+
Plan: planJSON,
381+
DailyCost: state.DailyCost,
382+
ResourceReplacements: resReps,
383+
AiTaskCount: state.AITaskCount,
403384
}
404385

405386
return msg, nil
@@ -422,42 +403,6 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
422403
return filtered
423404
}
424405

425-
// planResources must only be called while the lock is held.
426-
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, *tfjson.Plan, error) {
427-
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
428-
defer span.End()
429-
430-
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
431-
if err != nil {
432-
return nil, nil, xerrors.Errorf("show terraform plan file: %w", err)
433-
}
434-
435-
rawGraph, err := e.graph(ctx, killCtx)
436-
if err != nil {
437-
return nil, nil, xerrors.Errorf("graph: %w", err)
438-
}
439-
modules := []*tfjson.StateModule{}
440-
if plan.PriorState != nil {
441-
// We need the data resources for rich parameters. For some reason, they
442-
// only show up in the PriorState.
443-
//
444-
// We don't want all prior resources, because Quotas (and
445-
// future features) would never know which resources are getting
446-
// deleted by a stop.
447-
448-
filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
449-
modules = append(modules, &filtered)
450-
}
451-
modules = append(modules, plan.PlannedValues.RootModule)
452-
453-
state, err := ConvertState(ctx, modules, rawGraph, e.server.logger)
454-
if err != nil {
455-
return nil, nil, err
456-
}
457-
458-
return state, plan, nil
459-
}
460-
461406
// parsePlan must only be called while the lock is held.
462407
func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) {
463408
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
@@ -545,9 +490,11 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) {
545490
// TODO: When the plan is present, we should probably use it?
546491
// "-plan=" + e.files.PlanFilePath(),
547492
}
493+
548494
if ver.GreaterThanOrEqual(version170) {
549495
args = append(args, "-type=plan")
550496
}
497+
551498
var out strings.Builder
552499
cmd := exec.CommandContext(killCtx, e.binaryPath, args...) // #nosec
553500
cmd.Stdout = &out
@@ -608,11 +555,6 @@ func (e *executor) apply(
608555
return nil, xerrors.Errorf("terraform apply: %w", err)
609556
}
610557

611-
// `terraform show` & `terraform graph`
612-
state, err := e.stateResources(ctx, killCtx)
613-
if err != nil {
614-
return nil, err
615-
}
616558
statefilePath := e.files.StateFilePath()
617559
stateContent, err := os.ReadFile(statefilePath)
618560
if err != nil {
@@ -623,42 +565,11 @@ func (e *executor) apply(
623565
<-doneOut
624566
agg := e.timings.aggregate()
625567
return &proto.ApplyComplete{
626-
Parameters: state.Parameters,
627-
Resources: state.Resources,
628-
ExternalAuthProviders: state.ExternalAuthProviders,
629-
State: stateContent,
630-
Timings: agg,
631-
AiTasks: state.AITasks,
568+
State: stateContent,
569+
Timings: agg,
632570
}, nil
633571
}
634572

635-
// stateResources must only be called while the lock is held.
636-
func (e *executor) stateResources(ctx, killCtx context.Context) (*State, error) {
637-
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
638-
defer span.End()
639-
640-
state, err := e.state(ctx, killCtx)
641-
if err != nil {
642-
return nil, err
643-
}
644-
rawGraph, err := e.graph(ctx, killCtx)
645-
if err != nil {
646-
return nil, xerrors.Errorf("get terraform graph: %w", err)
647-
}
648-
converted := &State{}
649-
if state.Values == nil {
650-
return converted, nil
651-
}
652-
653-
converted, err = ConvertState(ctx, []*tfjson.StateModule{
654-
state.Values.RootModule,
655-
}, rawGraph, e.server.logger)
656-
if err != nil {
657-
return nil, err
658-
}
659-
return converted, nil
660-
}
661-
662573
// state must only be called while the lock is held.
663574
func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) {
664575
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package terraform
2+
3+
import (
4+
tfjson "github.com/hashicorp/terraform-json"
5+
"github.com/mitchellh/mapstructure"
6+
"golang.org/x/xerrors"
7+
)
8+
9+
type PlanState struct {
10+
DailyCost int32
11+
AITaskCount int32
12+
}
13+
14+
func planModules(plan *tfjson.Plan) []*tfjson.StateModule {
15+
modules := []*tfjson.StateModule{}
16+
if plan.PriorState != nil {
17+
// We need the data resources for rich parameters. For some reason, they
18+
// only show up in the PriorState.
19+
//
20+
// We don't want all prior resources, because Quotas (and
21+
// future features) would never know which resources are getting
22+
// deleted by a stop.
23+
24+
filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
25+
modules = append(modules, &filtered)
26+
}
27+
modules = append(modules, plan.PlannedValues.RootModule)
28+
return modules
29+
}
30+
31+
// ConvertPlanState consumes a terraform plan json output and produces a thinner
32+
// version of `State` to be used before `terraform apply`. `ConvertState`
33+
// requires `terraform graph`, this does not.
34+
func ConvertPlanState(plan *tfjson.Plan) (*PlanState, error) {
35+
modules := planModules(plan)
36+
37+
var dailyCost int32
38+
var aiTaskCount int32
39+
for _, mod := range modules {
40+
err := forEachResource(mod, func(res *tfjson.StateResource) error {
41+
switch res.Type {
42+
case "coder_metadata":
43+
var attrs resourceMetadataAttributes
44+
err := mapstructure.Decode(res.AttributeValues, &attrs)
45+
if err != nil {
46+
return xerrors.Errorf("decode metadata attributes: %w", err)
47+
}
48+
dailyCost += attrs.DailyCost
49+
case "coder_ai_task":
50+
aiTaskCount++
51+
}
52+
return nil
53+
})
54+
if err != nil {
55+
return nil, xerrors.Errorf("parse plan: %w", err)
56+
}
57+
}
58+
59+
return &PlanState{
60+
DailyCost: dailyCost,
61+
AITaskCount: aiTaskCount,
62+
}, nil
63+
}
64+
65+
func forEachResource(input *tfjson.StateModule, do func(res *tfjson.StateResource) error) error {
66+
for _, res := range input.Resources {
67+
err := do(res)
68+
if err != nil {
69+
return xerrors.Errorf("in module %s: %w", input.Address, err)
70+
}
71+
}
72+
73+
for _, mod := range input.ChildModules {
74+
err := forEachResource(mod, do)
75+
if err != nil {
76+
return xerrors.Errorf("in module %s: %w", mod.Address, err)
77+
}
78+
}
79+
return nil
80+
}

0 commit comments

Comments
 (0)