Skip to content

Commit 4cd5826

Browse files
committed
Add parameter parsing to the Terraform provider
1 parent e71711c commit 4cd5826

File tree

9 files changed

+551
-109
lines changed

9 files changed

+551
-109
lines changed

provisioner/terraform/executor.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,12 @@ func (e executor) planResources(ctx, killCtx context.Context, planfilePath strin
237237
if err != nil {
238238
return nil, nil, xerrors.Errorf("graph: %w", err)
239239
}
240-
return ConvertResourcesAndParameters(plan.PlannedValues.RootModule, rawGraph)
240+
modules := []*tfjson.StateModule{}
241+
if plan.PriorState != nil {
242+
modules = append(modules, plan.PriorState.Values.RootModule)
243+
}
244+
modules = append(modules, plan.PlannedValues.RootModule)
245+
return ConvertResourcesAndParameters(modules, rawGraph)
241246
}
242247

243248
func (e executor) showPlan(ctx, killCtx context.Context, planfilePath string) (*tfjson.Plan, error) {
@@ -332,7 +337,9 @@ func (e executor) stateResources(ctx, killCtx context.Context) ([]*proto.Resourc
332337
var resources []*proto.Resource
333338
var parameters []*proto.Parameter
334339
if state.Values != nil {
335-
resources, parameters, err = ConvertResourcesAndParameters(state.Values.RootModule, rawGraph)
340+
resources, parameters, err = ConvertResourcesAndParameters([]*tfjson.StateModule{
341+
state.Values.RootModule,
342+
}, rawGraph)
336343
if err != nil {
337344
return nil, nil, err
338345
}

provisioner/terraform/resources.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type metadataItem struct {
6363
// ConvertResourcesAndParameters consumes Terraform state and a GraphViz representation produced by
6464
// `terraform graph` to produce resources consumable by Coder.
6565
// nolint:gocyclo
66-
func ConvertResourcesAndParameters(module *tfjson.StateModule, rawGraph string) ([]*proto.Resource, []*proto.Parameter, error) {
66+
func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph string) ([]*proto.Resource, []*proto.Parameter, error) {
6767
parsedGraph, err := gographviz.ParseString(rawGraph)
6868
if err != nil {
6969
return nil, nil, xerrors.Errorf("parse graph: %w", err)
@@ -102,7 +102,9 @@ func ConvertResourcesAndParameters(module *tfjson.StateModule, rawGraph string)
102102
}
103103
}
104104
}
105-
findTerraformResources(module)
105+
for _, module := range modules {
106+
findTerraformResources(module)
107+
}
106108

107109
// Find all agents!
108110
for _, tfResource := range tfResourceByLabel {

provisioner/terraform/resources_test.go

Lines changed: 160 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -22,125 +22,166 @@ func TestConvertResources(t *testing.T) {
2222
t.Parallel()
2323
// nolint:dogsled
2424
_, filename, _, _ := runtime.Caller(0)
25+
type testCase struct {
26+
resources []*proto.Resource
27+
parameters []*proto.Parameter
28+
}
2529
// nolint:paralleltest
26-
for folderName, expected := range map[string][]*proto.Resource{
30+
for folderName, expected := range map[string]testCase{
2731
// When a resource depends on another, the shortest route
2832
// to a resource should always be chosen for the agent.
29-
"chaining-resources": {{
30-
Name: "a",
31-
Type: "null_resource",
32-
}, {
33-
Name: "b",
34-
Type: "null_resource",
35-
Agents: []*proto.Agent{{
36-
Name: "main",
37-
OperatingSystem: "linux",
38-
Architecture: "amd64",
39-
Auth: &proto.Agent_Token{},
33+
"chaining-resources": {
34+
resources: []*proto.Resource{{
35+
Name: "a",
36+
Type: "null_resource",
37+
}, {
38+
Name: "b",
39+
Type: "null_resource",
40+
Agents: []*proto.Agent{{
41+
Name: "main",
42+
OperatingSystem: "linux",
43+
Architecture: "amd64",
44+
Auth: &proto.Agent_Token{},
45+
}},
4046
}},
41-
}},
47+
},
4248
// This can happen when resources hierarchically conflict.
4349
// When multiple resources exist at the same level, the first
4450
// listed in state will be chosen.
45-
"conflicting-resources": {{
46-
Name: "first",
47-
Type: "null_resource",
48-
Agents: []*proto.Agent{{
49-
Name: "main",
50-
OperatingSystem: "linux",
51-
Architecture: "amd64",
52-
Auth: &proto.Agent_Token{},
51+
"conflicting-resources": {
52+
resources: []*proto.Resource{{
53+
Name: "first",
54+
Type: "null_resource",
55+
Agents: []*proto.Agent{{
56+
Name: "main",
57+
OperatingSystem: "linux",
58+
Architecture: "amd64",
59+
Auth: &proto.Agent_Token{},
60+
}},
61+
}, {
62+
Name: "second",
63+
Type: "null_resource",
5364
}},
54-
}, {
55-
Name: "second",
56-
Type: "null_resource",
57-
}},
65+
},
5866
// Ensures the instance ID authentication type surfaces.
59-
"instance-id": {{
60-
Name: "main",
61-
Type: "null_resource",
62-
Agents: []*proto.Agent{{
63-
Name: "main",
64-
OperatingSystem: "linux",
65-
Architecture: "amd64",
66-
Auth: &proto.Agent_InstanceId{},
67+
"instance-id": {
68+
resources: []*proto.Resource{{
69+
Name: "main",
70+
Type: "null_resource",
71+
Agents: []*proto.Agent{{
72+
Name: "main",
73+
OperatingSystem: "linux",
74+
Architecture: "amd64",
75+
Auth: &proto.Agent_InstanceId{},
76+
}},
6777
}},
68-
}},
78+
},
6979
// Ensures that calls to resources through modules work
7080
// as expected.
71-
"calling-module": {{
72-
Name: "example",
73-
Type: "null_resource",
74-
Agents: []*proto.Agent{{
75-
Name: "main",
76-
OperatingSystem: "linux",
77-
Architecture: "amd64",
78-
Auth: &proto.Agent_Token{},
81+
"calling-module": {
82+
resources: []*proto.Resource{{
83+
Name: "example",
84+
Type: "null_resource",
85+
Agents: []*proto.Agent{{
86+
Name: "main",
87+
OperatingSystem: "linux",
88+
Architecture: "amd64",
89+
Auth: &proto.Agent_Token{},
90+
}},
7991
}},
80-
}},
92+
},
8193
// Ensures the attachment of multiple agents to a single
8294
// resource is successful.
83-
"multiple-agents": {{
84-
Name: "dev",
85-
Type: "null_resource",
86-
Agents: []*proto.Agent{{
87-
Name: "dev1",
88-
OperatingSystem: "linux",
89-
Architecture: "amd64",
90-
Auth: &proto.Agent_Token{},
91-
}, {
92-
Name: "dev2",
93-
OperatingSystem: "darwin",
94-
Architecture: "amd64",
95-
Auth: &proto.Agent_Token{},
96-
}, {
97-
Name: "dev3",
98-
OperatingSystem: "windows",
99-
Architecture: "arm64",
100-
Auth: &proto.Agent_Token{},
95+
"multiple-agents": {
96+
resources: []*proto.Resource{{
97+
Name: "dev",
98+
Type: "null_resource",
99+
Agents: []*proto.Agent{{
100+
Name: "dev1",
101+
OperatingSystem: "linux",
102+
Architecture: "amd64",
103+
Auth: &proto.Agent_Token{},
104+
}, {
105+
Name: "dev2",
106+
OperatingSystem: "darwin",
107+
Architecture: "amd64",
108+
Auth: &proto.Agent_Token{},
109+
}, {
110+
Name: "dev3",
111+
OperatingSystem: "windows",
112+
Architecture: "arm64",
113+
Auth: &proto.Agent_Token{},
114+
}},
101115
}},
102-
}},
116+
},
103117
// Ensures multiple applications can be set for a single agent.
104-
"multiple-apps": {{
105-
Name: "dev",
106-
Type: "null_resource",
107-
Agents: []*proto.Agent{{
108-
Name: "dev1",
109-
OperatingSystem: "linux",
110-
Architecture: "amd64",
111-
Apps: []*proto.App{{
112-
Name: "app1",
113-
}, {
114-
Name: "app2",
115-
Healthcheck: &proto.Healthcheck{
116-
Url: "http://localhost:13337/healthz",
117-
Interval: 5,
118-
Threshold: 6,
119-
},
118+
"multiple-apps": {
119+
resources: []*proto.Resource{{
120+
Name: "dev",
121+
Type: "null_resource",
122+
Agents: []*proto.Agent{{
123+
Name: "dev1",
124+
OperatingSystem: "linux",
125+
Architecture: "amd64",
126+
Apps: []*proto.App{{
127+
Name: "app1",
128+
}, {
129+
Name: "app2",
130+
Healthcheck: &proto.Healthcheck{
131+
Url: "http://localhost:13337/healthz",
132+
Interval: 5,
133+
Threshold: 6,
134+
},
135+
}},
136+
Auth: &proto.Agent_Token{},
120137
}},
121-
Auth: &proto.Agent_Token{},
122138
}},
123-
}},
139+
},
124140
// Tests fetching metadata about workspace resources.
125-
"resource-metadata": {{
126-
Name: "about",
127-
Type: "null_resource",
128-
Hide: true,
129-
Icon: "/icon/server.svg",
130-
Metadata: []*proto.Resource_Metadata{{
131-
Key: "hello",
132-
Value: "world",
133-
}, {
134-
Key: "null",
135-
IsNull: true,
136-
}, {
137-
Key: "empty",
138-
}, {
139-
Key: "secret",
140-
Value: "squirrel",
141-
Sensitive: true,
141+
"resource-metadata": {
142+
resources: []*proto.Resource{{
143+
Name: "about",
144+
Type: "null_resource",
145+
Hide: true,
146+
Icon: "/icon/server.svg",
147+
Metadata: []*proto.Resource_Metadata{{
148+
Key: "hello",
149+
Value: "world",
150+
}, {
151+
Key: "null",
152+
IsNull: true,
153+
}, {
154+
Key: "empty",
155+
}, {
156+
Key: "secret",
157+
Value: "squirrel",
158+
Sensitive: true,
159+
}},
160+
}},
161+
},
162+
"parameters": {
163+
resources: []*proto.Resource{{
164+
Name: "dev",
165+
Type: "null_resource",
166+
Agents: []*proto.Agent{{
167+
Name: "dev",
168+
OperatingSystem: "windows",
169+
Architecture: "arm64",
170+
Auth: &proto.Agent_Token{},
171+
}},
142172
}},
143-
}},
173+
parameters: []*proto.Parameter{{
174+
Name: "Example",
175+
Type: "string",
176+
Options: []*proto.ParameterOption{{
177+
Name: "First Option",
178+
Value: "first",
179+
}, {
180+
Name: "Second Option",
181+
Value: "second",
182+
}},
183+
}},
184+
},
144185
} {
145186
folderName := folderName
146187
expected := expected
@@ -158,13 +199,18 @@ func TestConvertResources(t *testing.T) {
158199
tfPlanGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfplan.dot"))
159200
require.NoError(t, err)
160201

161-
resources, parameters, err := terraform.ConvertResourcesAndParameters(tfPlan.PlannedValues.RootModule, string(tfPlanGraph))
202+
modules := []*tfjson.StateModule{}
203+
if tfPlan.PriorState != nil {
204+
modules = append(modules, tfPlan.PriorState.Values.RootModule)
205+
}
206+
modules = append(modules, tfPlan.PlannedValues.RootModule)
207+
resources, parameters, err := terraform.ConvertResourcesAndParameters(modules, string(tfPlanGraph))
162208
require.NoError(t, err)
163209
sortResources(resources)
164210
sortParameters(parameters)
165211

166-
var expectedNoMetadata []*proto.Resource
167-
for _, resource := range expected {
212+
expectedNoMetadata := make([]*proto.Resource, 0)
213+
for _, resource := range expected.resources {
168214
resourceCopy, _ := protobuf.Clone(resource).(*proto.Resource)
169215
// plan cannot know whether values are null or not
170216
for _, metadata := range resourceCopy.Metadata {
@@ -178,6 +224,15 @@ func TestConvertResources(t *testing.T) {
178224
resourcesGot, err := json.Marshal(resources)
179225
require.NoError(t, err)
180226
require.Equal(t, string(resourcesWant), string(resourcesGot))
227+
228+
if expected.parameters == nil {
229+
expected.parameters = []*proto.Parameter{}
230+
}
231+
parametersWant, err := json.Marshal(expected.parameters)
232+
require.NoError(t, err)
233+
parametersGot, err := json.Marshal(parameters)
234+
require.NoError(t, err)
235+
require.Equal(t, string(parametersWant), string(parametersGot))
181236
})
182237
t.Run("Provision", func(t *testing.T) {
183238
t.Parallel()
@@ -189,7 +244,7 @@ func TestConvertResources(t *testing.T) {
189244
tfStateGraph, err := os.ReadFile(filepath.Join(dir, folderName+".tfstate.dot"))
190245
require.NoError(t, err)
191246

192-
resources, parameters, err := terraform.ConvertResourcesAndParameters(tfState.Values.RootModule, string(tfStateGraph))
247+
resources, parameters, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{tfState.Values.RootModule}, string(tfStateGraph))
193248
require.NoError(t, err)
194249
sortResources(resources)
195250
sortParameters(parameters)
@@ -204,7 +259,7 @@ func TestConvertResources(t *testing.T) {
204259
}
205260
}
206261
}
207-
resourcesWant, err := json.Marshal(expected)
262+
resourcesWant, err := json.Marshal(expected.resources)
208263
require.NoError(t, err)
209264
resourcesGot, err := json.Marshal(resources)
210265
require.NoError(t, err)
@@ -248,7 +303,7 @@ func TestInstanceIDAssociation(t *testing.T) {
248303
t.Parallel()
249304
instanceID, err := cryptorand.String(12)
250305
require.NoError(t, err)
251-
resources, _, err := terraform.ConvertResourcesAndParameters(&tfjson.StateModule{
306+
resources, _, err := terraform.ConvertResourcesAndParameters([]*tfjson.StateModule{{
252307
Resources: []*tfjson.StateResource{{
253308
Address: "coder_agent.dev",
254309
Type: "coder_agent",
@@ -269,7 +324,7 @@ func TestInstanceIDAssociation(t *testing.T) {
269324
},
270325
}},
271326
// This is manually created to join the edges.
272-
}, `digraph {
327+
}}, `digraph {
273328
compound = "true"
274329
newrank = "true"
275330
subgraph "root" {

provisioner/terraform/testdata/generate.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set -euo pipefail
44
cd "$(dirname "${BASH_SOURCE[0]}")"
55

66
for d in */; do
7+
d="parameters"
78
pushd "$d"
89
name=$(basename "$(pwd)")
910
terraform init -upgrade
@@ -16,4 +17,5 @@ for d in */; do
1617
rm terraform.tfstate
1718
terraform graph >"$name".tfstate.dot
1819
popd
20+
exit 0
1921
done

0 commit comments

Comments
 (0)