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

Skip to content

Commit 9126256

Browse files
committed
feat(provisioner/terraform): parse and resolve script order selectors
Add package-private helpers to parse script and whole-module selectors, resolve them relative to their declaring module, and expand count, for_each, and nested module instances. Distinguish declared module calls with no concrete scripts from unknown module selectors using Terraform plan configuration. These helpers are not yet wired into workspace provisioning. Selector parsing uses go-terraform-address in this PR. A follow-up replaces it with HCL traversal parsing because go-terraform-address rejects valid Unicode identifiers. Keeping that correction separate limits this PR's scope. Refs: https://linear.app/codercom/issue/PLAT-543
1 parent bf50641 commit 9126256

2 files changed

Lines changed: 759 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package terraform
2+
3+
import (
4+
"maps"
5+
"slices"
6+
7+
tfaddr "github.com/hashicorp/go-terraform-address"
8+
tfjson "github.com/hashicorp/terraform-json"
9+
"golang.org/x/xerrors"
10+
)
11+
12+
type scriptOrderSelectorKind int
13+
14+
const (
15+
_ scriptOrderSelectorKind = iota
16+
scriptOrderSelectorScript
17+
scriptOrderSelectorModule
18+
)
19+
20+
type scriptOrderSelector struct {
21+
kind scriptOrderSelectorKind
22+
name string
23+
instanceKey string
24+
}
25+
26+
type scriptOrderSelectorResolution struct {
27+
// contains the sorted, deduplicated Terraform addresses of all
28+
// concrete scripts selected.
29+
addresses []string
30+
// For module selectors, distinguishes a declared module call with
31+
// no resolved scripts from an unknown module selector. Scripts
32+
// may resolve to no instances when a module conditionally sets
33+
// their count to zero.
34+
moduleCallDeclared bool
35+
}
36+
37+
// parseScriptOrderSelector currently limits selectors to scripts in
38+
// the declaring module and whole child module calls.
39+
func parseScriptOrderSelector(raw string) (scriptOrderSelector, error) {
40+
address, err := tfaddr.NewAddress(raw)
41+
if err != nil {
42+
return scriptOrderSelector{}, xerrors.Errorf("parse script order selector %q: %w", raw, err)
43+
}
44+
if len(address.ModulePath) != 0 {
45+
return scriptOrderSelector{}, xerrors.Errorf(
46+
"script order selector %q must reference a coder_script in the declaring module or an entire direct child module call",
47+
raw,
48+
)
49+
}
50+
51+
switch address.ResourceSpec.Type {
52+
case "coder_script":
53+
return scriptOrderSelector{
54+
kind: scriptOrderSelectorScript,
55+
name: address.ResourceSpec.Name,
56+
instanceKey: address.ResourceSpec.Index.String(),
57+
}, nil
58+
case "module":
59+
if address.ResourceSpec.Index.String() != "" {
60+
return scriptOrderSelector{}, xerrors.Errorf("module selector %q must select all module instances", raw)
61+
}
62+
return scriptOrderSelector{
63+
kind: scriptOrderSelectorModule,
64+
name: address.ResourceSpec.Name,
65+
}, nil
66+
default:
67+
return scriptOrderSelector{}, xerrors.Errorf("script order selector %q must select a coder_script or module", raw)
68+
}
69+
}
70+
71+
// resolveScriptOrderSelector expands a selector relative to its
72+
// declaring module. For module selectors, it also reports whether the
73+
// module call is declared so callers can distinguish an empty module
74+
// call from an unknown selector.
75+
//
76+
// `modules` is the evaluated module tree and its concrete script instances.
77+
// `planConfig` contains declared module calls, including calls with no
78+
// instances after evaluation.
79+
// `selector` must have been produced by parseScriptOrderSelector.
80+
func resolveScriptOrderSelector(
81+
modules []*tfjson.StateModule,
82+
planConfig *tfjson.Config,
83+
moduleAddress string,
84+
selector scriptOrderSelector,
85+
) (scriptOrderSelectorResolution, error) {
86+
if selector.kind != scriptOrderSelectorScript && selector.kind != scriptOrderSelectorModule {
87+
return scriptOrderSelectorResolution{},
88+
xerrors.Errorf("unknown script order selector kind %d", selector.kind)
89+
}
90+
91+
var resolution scriptOrderSelectorResolution
92+
if selector.kind == scriptOrderSelectorModule {
93+
declared, err := isModuleCallInConfig(planConfig, moduleAddress, selector.name)
94+
if err != nil {
95+
return scriptOrderSelectorResolution{}, err
96+
}
97+
resolution.moduleCallDeclared = declared
98+
}
99+
100+
resolved := map[string]struct{}{}
101+
for _, rootModule := range modules {
102+
err := walkStateModuleTree(rootModule, func(module *tfjson.StateModule) error {
103+
if module.Address != moduleAddress {
104+
return nil
105+
}
106+
107+
switch selector.kind {
108+
case scriptOrderSelectorScript:
109+
return resolveScriptOrderScriptSelector(module, selector, resolved)
110+
case scriptOrderSelectorModule:
111+
return resolveScriptOrderModuleSelector(module, selector, resolved)
112+
default:
113+
return xerrors.Errorf("unknown script order selector kind %d", selector.kind)
114+
}
115+
})
116+
if err != nil {
117+
return scriptOrderSelectorResolution{}, err
118+
}
119+
}
120+
121+
resolution.addresses = slices.Sorted(maps.Keys(resolved))
122+
return resolution, nil
123+
}
124+
125+
// isModuleCallInConfig reports whether name is a direct child module
126+
// call in the configuration of the declaring module instance.
127+
func isModuleCallInConfig(
128+
config *tfjson.Config,
129+
declaringModuleAddress string,
130+
name string,
131+
) (bool, error) {
132+
if config == nil || config.RootModule == nil {
133+
return false, xerrors.New("terraform plan configuration is required to resolve a module selector")
134+
}
135+
136+
module := config.RootModule
137+
if declaringModuleAddress != "" {
138+
modulePath, err := parseStateModuleAddress(declaringModuleAddress)
139+
if err != nil {
140+
return false, err
141+
}
142+
for _, step := range modulePath {
143+
call := module.ModuleCalls[step.Name]
144+
if call == nil || call.Module == nil {
145+
return false, nil
146+
}
147+
module = call.Module
148+
}
149+
}
150+
151+
return module.ModuleCalls[name] != nil, nil
152+
}
153+
154+
func resolveScriptOrderScriptSelector(
155+
module *tfjson.StateModule,
156+
selector scriptOrderSelector,
157+
resolved map[string]struct{},
158+
) error {
159+
// An unindexed script selector expands every count and for_each
160+
// instance; an indexed selector retains only the matching
161+
// instance key.
162+
for _, resource := range module.Resources {
163+
if resource == nil ||
164+
resource.Mode != tfjson.ManagedResourceMode ||
165+
resource.Type != "coder_script" ||
166+
resource.Name != selector.name {
167+
continue
168+
}
169+
170+
address, err := parseStateResourceAddress(module, resource)
171+
if err != nil {
172+
return err
173+
}
174+
if selector.instanceKey != "" &&
175+
address.ResourceSpec.Index.String() != selector.instanceKey {
176+
continue
177+
}
178+
resolved[resource.Address] = struct{}{}
179+
}
180+
return nil
181+
}
182+
183+
func resolveScriptOrderModuleSelector(
184+
module *tfjson.StateModule,
185+
selector scriptOrderSelector,
186+
resolved map[string]struct{},
187+
) error {
188+
// An unindexed module selector expands every count and for_each
189+
// instance of the selected child module call.
190+
for _, child := range module.ChildModules {
191+
if child == nil {
192+
continue
193+
}
194+
195+
modulePath, err := parseStateModuleAddress(child.Address)
196+
if err != nil {
197+
return err
198+
}
199+
if len(modulePath) == 0 || modulePath[len(modulePath)-1].Name != selector.name {
200+
continue
201+
}
202+
if err := collectModuleCoderScriptAddresses(child, resolved); err != nil {
203+
return err
204+
}
205+
}
206+
return nil
207+
}
208+
209+
func collectModuleCoderScriptAddresses(
210+
module *tfjson.StateModule, resolved map[string]struct{},
211+
) error {
212+
for _, resource := range module.Resources {
213+
if resource == nil ||
214+
resource.Mode != tfjson.ManagedResourceMode ||
215+
resource.Type != "coder_script" {
216+
continue
217+
}
218+
if _, err := parseStateResourceAddress(module, resource); err != nil {
219+
return err
220+
}
221+
resolved[resource.Address] = struct{}{}
222+
}
223+
for _, child := range module.ChildModules {
224+
if child == nil {
225+
continue
226+
}
227+
if err := collectModuleCoderScriptAddresses(child, resolved); err != nil {
228+
return err
229+
}
230+
}
231+
return nil
232+
}
233+
234+
// parseStateResourceAddress parses a concrete resource address and
235+
// verifies that it matches its containing state module and resource
236+
// fields. This prevents inconsistent Terraform output from assigning
237+
// dependencies to the wrong resource.
238+
func parseStateResourceAddress(
239+
module *tfjson.StateModule, resource *tfjson.StateResource,
240+
) (*tfaddr.Address, error) {
241+
address, err := tfaddr.NewAddress(resource.Address)
242+
if err != nil {
243+
return nil, xerrors.Errorf("parse Terraform resource address %q: %w", resource.Address, err)
244+
}
245+
// Defensive: TF should always emit an address consistent with
246+
// these state fields.
247+
if address.ModulePath.String() != module.Address ||
248+
address.ResourceSpec.Type != resource.Type ||
249+
address.ResourceSpec.Name != resource.Name {
250+
return nil, xerrors.Errorf("Terraform resource address %q does not match its state fields", resource.Address)
251+
}
252+
return address, nil
253+
}
254+
255+
func parseStateModuleAddress(address string) (tfaddr.ModulePath, error) {
256+
// go-terraform-address parses a module path only as part of a
257+
// resource address, so append a placeholder resource before
258+
// parsing it.
259+
parsed, err := tfaddr.NewAddress(address + ".placeholder_resource.placeholder")
260+
if err != nil {
261+
return nil, xerrors.Errorf("parse module address %q: %w", address, err)
262+
}
263+
return parsed.ModulePath, nil
264+
}
265+
266+
func walkStateModuleTree(module *tfjson.StateModule, visit func(*tfjson.StateModule) error) error {
267+
if module == nil {
268+
return nil
269+
}
270+
if err := visit(module); err != nil {
271+
return err
272+
}
273+
for _, child := range module.ChildModules {
274+
if err := walkStateModuleTree(child, visit); err != nil {
275+
return err
276+
}
277+
}
278+
return nil
279+
}

0 commit comments

Comments
 (0)