Skip to content

Commit 4a270f4

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

File tree

19 files changed

+2054
-1020
lines changed

19 files changed

+2054
-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: 22 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,18 +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+
376+
// DoneOut must be completed before we aggregate timings to ensure all logs have been processed.
377+
<-doneOut
389378
msg := &proto.PlanComplete{
390-
Parameters: state.Parameters,
391-
Resources: state.Resources,
392-
ExternalAuthProviders: state.ExternalAuthProviders,
393-
Timings: append(e.timings.aggregate(), graphTimings.aggregate()...),
394-
Presets: state.Presets,
395-
Plan: planJSON,
396-
ResourceReplacements: resReps,
397-
ModuleFiles: moduleFiles,
398-
HasAiTasks: state.HasAITasks,
399-
AiTasks: state.AITasks,
400-
HasExternalAgents: state.HasExternalAgents,
379+
Timings: e.timings.aggregate(),
380+
Plan: planJSON,
381+
DailyCost: state.DailyCost,
382+
ResourceReplacements: resReps,
383+
AiTaskCount: state.AITaskCount,
401384
}
402385

403386
return msg, nil
@@ -420,42 +403,6 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
420403
return filtered
421404
}
422405

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

609-
// `terraform show` & `terraform graph`
610-
state, err := e.stateResources(ctx, killCtx)
611-
if err != nil {
612-
return nil, err
613-
}
614558
statefilePath := e.files.StateFilePath()
615559
stateContent, err := os.ReadFile(statefilePath)
616560
if err != nil {
617561
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
618562
}
619563

564+
// DoneOut must be completed before we aggregate timings to ensure all logs have been processed.
565+
<-doneOut
620566
agg := e.timings.aggregate()
621567
return &proto.ApplyComplete{
622-
Parameters: state.Parameters,
623-
Resources: state.Resources,
624-
ExternalAuthProviders: state.ExternalAuthProviders,
625-
State: stateContent,
626-
Timings: agg,
627-
AiTasks: state.AITasks,
568+
State: stateContent,
569+
Timings: agg,
628570
}, nil
629571
}
630572

631-
// stateResources must only be called while the lock is held.
632-
func (e *executor) stateResources(ctx, killCtx context.Context) (*State, error) {
633-
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
634-
defer span.End()
635-
636-
state, err := e.state(ctx, killCtx)
637-
if err != nil {
638-
return nil, err
639-
}
640-
rawGraph, err := e.graph(ctx, killCtx)
641-
if err != nil {
642-
return nil, xerrors.Errorf("get terraform graph: %w", err)
643-
}
644-
converted := &State{}
645-
if state.Values == nil {
646-
return converted, nil
647-
}
648-
649-
converted, err = ConvertState(ctx, []*tfjson.StateModule{
650-
state.Values.RootModule,
651-
}, rawGraph, e.server.logger)
652-
if err != nil {
653-
return nil, err
654-
}
655-
return converted, nil
656-
}
657-
658573
// state must only be called while the lock is held.
659574
func (e *executor) state(ctx, killCtx context.Context) (*tfjson.State, error) {
660575
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)