Skip to content

Commit 04bc86a

Browse files
committed
chore: distinct operations for provisioner's tf operations
'parse', 'init', 'plan', 'apply', 'graph' More granular provisioner options.
1 parent 6b8d466 commit 04bc86a

File tree

19 files changed

+2054
-1024
lines changed

19 files changed

+2054
-1024
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: 16 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"cdr.dev/slog"
2424
"github.com/coder/coder/v2/provisionersdk/tfpath"
2525

26-
"github.com/coder/coder/v2/coderd/database"
2726
"github.com/coder/coder/v2/coderd/tracing"
2827
"github.com/coder/coder/v2/provisionersdk/proto"
2928
)
@@ -283,7 +282,7 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
283282
func checksumFileCRC32(ctx context.Context, logger slog.Logger, path string) uint32 {
284283
content, err := os.ReadFile(path)
285284
if err != nil {
286-
logger.Debug(ctx, "file %s does not exist or can't be read, skip checksum calculation")
285+
logger.Debug(ctx, fmt.Sprintf("file %s does not exist or can't be read, skip checksum calculation", path))
287286
return 0
288287
}
289288
return crc32.ChecksumIEEE(content)
@@ -330,34 +329,16 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
330329
return nil, xerrors.Errorf("terraform plan: %w", err)
331330
}
332331

333-
// Capture the duration of the call to `terraform graph`.
334-
graphTimings := newTimingAggregator(database.ProvisionerJobTimingStageGraph)
335-
graphTimings.ingest(createGraphTimingsEvent(timingGraphStart))
336-
337-
state, plan, err := e.planResources(ctx, killCtx, planfilePath)
332+
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
338333
if err != nil {
339-
graphTimings.ingest(createGraphTimingsEvent(timingGraphErrored))
340-
return nil, xerrors.Errorf("plan resources: %w", err)
334+
return nil, xerrors.Errorf("show terraform plan file: %w", err)
341335
}
336+
342337
planJSON, err := json.Marshal(plan)
343338
if err != nil {
344339
return nil, xerrors.Errorf("marshal plan: %w", err)
345340
}
346341

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

368+
state, err := ConvertPlanState(plan)
369+
if err != nil {
370+
return nil, xerrors.Errorf("convert plan state: %w", err)
371+
}
372+
387373
msg := &proto.PlanComplete{
388-
Parameters: state.Parameters,
389-
Resources: state.Resources,
390-
ExternalAuthProviders: state.ExternalAuthProviders,
391-
Timings: graphTimings.aggregate(),
392-
Presets: state.Presets,
393-
Plan: planJSON,
394-
ResourceReplacements: resReps,
395-
ModuleFiles: moduleFiles,
396-
HasAiTasks: state.HasAITasks,
397-
AiTasks: state.AITasks,
398-
HasExternalAgents: state.HasExternalAgents,
374+
Plan: planJSON,
375+
DailyCost: state.DailyCost,
376+
ResourceReplacements: resReps,
377+
AiTaskCount: state.AITaskCount,
399378
}
400379

401380
return msg, nil
@@ -418,42 +397,6 @@ func onlyDataResources(sm tfjson.StateModule) tfjson.StateModule {
418397
return filtered
419398
}
420399

421-
// planResources must only be called while the lock is held.
422-
func (e *executor) planResources(ctx, killCtx context.Context, planfilePath string) (*State, *tfjson.Plan, error) {
423-
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
424-
defer span.End()
425-
426-
plan, err := e.parsePlan(ctx, killCtx, planfilePath)
427-
if err != nil {
428-
return nil, nil, xerrors.Errorf("show terraform plan file: %w", err)
429-
}
430-
431-
rawGraph, err := e.graph(ctx, killCtx)
432-
if err != nil {
433-
return nil, nil, xerrors.Errorf("graph: %w", err)
434-
}
435-
modules := []*tfjson.StateModule{}
436-
if plan.PriorState != nil {
437-
// We need the data resources for rich parameters. For some reason, they
438-
// only show up in the PriorState.
439-
//
440-
// We don't want all prior resources, because Quotas (and
441-
// future features) would never know which resources are getting
442-
// deleted by a stop.
443-
444-
filtered := onlyDataResources(*plan.PriorState.Values.RootModule)
445-
modules = append(modules, &filtered)
446-
}
447-
modules = append(modules, plan.PlannedValues.RootModule)
448-
449-
state, err := ConvertState(ctx, modules, rawGraph, e.server.logger)
450-
if err != nil {
451-
return nil, nil, err
452-
}
453-
454-
return state, plan, nil
455-
}
456-
457400
// parsePlan must only be called while the lock is held.
458401
func (e *executor) parsePlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) {
459402
ctx, span := e.server.startTrace(ctx, tracing.FuncName())
@@ -541,9 +484,11 @@ func (e *executor) graph(ctx, killCtx context.Context) (string, error) {
541484
// TODO: When the plan is present, we should probably use it?
542485
// "-plan=" + e.files.PlanFilePath(),
543486
}
487+
544488
if ver.GreaterThanOrEqual(version170) {
545489
args = append(args, "-type=plan")
546490
}
491+
547492
var out strings.Builder
548493
cmd := exec.CommandContext(killCtx, e.binaryPath, args...) // #nosec
549494
cmd.Stdout = &out
@@ -602,53 +547,17 @@ func (e *executor) apply(
602547
return nil, xerrors.Errorf("terraform apply: %w", err)
603548
}
604549

605-
// `terraform show` & `terraform graph`
606-
state, err := e.stateResources(ctx, killCtx)
607-
if err != nil {
608-
return nil, err
609-
}
610550
statefilePath := e.files.StateFilePath()
611551
stateContent, err := os.ReadFile(statefilePath)
612552
if err != nil {
613553
return nil, xerrors.Errorf("read statefile %q: %w", statefilePath, err)
614554
}
615555

616556
return &proto.ApplyComplete{
617-
Parameters: state.Parameters,
618-
Resources: state.Resources,
619-
ExternalAuthProviders: state.ExternalAuthProviders,
620-
State: stateContent,
621-
AiTasks: state.AITasks,
557+
State: stateContent,
622558
}, nil
623559
}
624560

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