@@ -11,6 +11,28 @@ import (
11
11
"github.com/coder/coder/provisionersdk/proto"
12
12
)
13
13
14
+ // A mapping of attributes on the "coder_agent" resource.
15
+ type agentAttributes struct {
16
+ Auth string `mapstructure:"auth"`
17
+ OperatingSystem string `mapstructure:"os"`
18
+ Architecture string `mapstructure:"arch"`
19
+ Directory string `mapstructure:"dir"`
20
+ ID string `mapstructure:"id"`
21
+ Token string `mapstructure:"token"`
22
+ Env map [string ]string `mapstructure:"env"`
23
+ StartupScript string `mapstructure:"startup_script"`
24
+ }
25
+
26
+ // A mapping of attributes on the "coder_app" resource.
27
+ type agentAppAttributes struct {
28
+ AgentID string `mapstructure:"agent_id"`
29
+ Name string `mapstructure:"name"`
30
+ Icon string `mapstructure:"icon"`
31
+ URL string `mapstructure:"url"`
32
+ Command string `mapstructure:"command"`
33
+ RelativePath bool `mapstructure:"relative_path"`
34
+ }
35
+
14
36
// ConvertResources consumes Terraform state and a GraphViz representation produced by
15
37
// `terraform graph` to produce resources consumable by Coder.
16
38
func ConvertResources (module * tfjson.StateModule , rawGraph string ) ([]* proto.Resource , error ) {
@@ -22,52 +44,36 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
22
44
if err != nil {
23
45
return nil , xerrors .Errorf ("analyze graph: %w" , err )
24
46
}
25
- resourceDependencies := map [string ][]string {}
26
- for _ , node := range graph .Nodes .Nodes {
27
- label , exists := node .Attrs ["label" ]
28
- if ! exists {
29
- continue
30
- }
31
- label = strings .Trim (label , `"` )
32
- resourceDependencies [label ] = findDependenciesWithLabels (graph , node .Name )
33
- }
34
47
35
48
resources := make ([]* proto.Resource , 0 )
36
- agents := map [string ]* proto.Agent {}
49
+ resourceAgents := map [string ][ ]* proto.Agent {}
37
50
38
- tfResources := make ([]* tfjson.StateResource , 0 )
39
- var appendResources func (mod * tfjson.StateModule )
40
- appendResources = func (mod * tfjson.StateModule ) {
51
+ // Indexes Terraform resources by it's label. The label
52
+ // is what "terraform graph" uses to reference nodes.
53
+ tfResourceByLabel := map [string ]* tfjson.StateResource {}
54
+ var findTerraformResources func (mod * tfjson.StateModule )
55
+ findTerraformResources = func (mod * tfjson.StateModule ) {
41
56
for _ , module := range mod .ChildModules {
42
- appendResources (module )
57
+ findTerraformResources (module )
58
+ }
59
+ for _ , resource := range mod .Resources {
60
+ tfResourceByLabel [convertAddressToLabel (resource .Address )] = resource
43
61
}
44
- tfResources = append (tfResources , mod .Resources ... )
45
- }
46
- appendResources (module )
47
-
48
- type agentAttributes struct {
49
- Auth string `mapstructure:"auth"`
50
- OperatingSystem string `mapstructure:"os"`
51
- Architecture string `mapstructure:"arch"`
52
- Directory string `mapstructure:"dir"`
53
- ID string `mapstructure:"id"`
54
- Token string `mapstructure:"token"`
55
- Env map [string ]string `mapstructure:"env"`
56
- StartupScript string `mapstructure:"startup_script"`
57
62
}
63
+ findTerraformResources (module )
58
64
59
- // Store all agents inside the maps !
60
- for _ , resource := range tfResources {
61
- if resource .Type != "coder_agent" {
65
+ // Find all agents!
66
+ for _ , tfResource := range tfResourceByLabel {
67
+ if tfResource .Type != "coder_agent" {
62
68
continue
63
69
}
64
70
var attrs agentAttributes
65
- err = mapstructure .Decode (resource .AttributeValues , & attrs )
71
+ err = mapstructure .Decode (tfResource .AttributeValues , & attrs )
66
72
if err != nil {
67
73
return nil , xerrors .Errorf ("decode agent attributes: %w" , err )
68
74
}
69
75
agent := & proto.Agent {
70
- Name : resource .Name ,
76
+ Name : tfResource .Name ,
71
77
Id : attrs .ID ,
72
78
Env : attrs .Env ,
73
79
StartupScript : attrs .StartupScript ,
@@ -81,14 +87,56 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
81
87
Token : attrs .Token ,
82
88
}
83
89
default :
90
+ // If token authentication isn't specified,
91
+ // assume instance auth. It's our only other
92
+ // authentication type!
84
93
agent .Auth = & proto.Agent_InstanceId {}
85
94
}
86
95
87
- agents [convertAddressToLabel (resource .Address )] = agent
96
+ // The label is used to find the graph node!
97
+ agentLabel := convertAddressToLabel (tfResource .Address )
98
+
99
+ var agentNode * gographviz.Node
100
+ for _ , node := range graph .Nodes .Lookup {
101
+ // The node attributes surround the label with quotes.
102
+ if strings .Trim (node .Attrs ["label" ], `"` ) != agentLabel {
103
+ continue
104
+ }
105
+ agentNode = node
106
+ break
107
+ }
108
+ if agentNode == nil {
109
+ return nil , xerrors .Errorf ("couldn't find node on graph: %q" , agentLabel )
110
+ }
111
+
112
+ var agentResource * graphResource
113
+ for _ , resource := range findResourcesUpGraph (graph , tfResourceByLabel , agentNode .Name , 0 ) {
114
+ if agentResource == nil {
115
+ // Default to the first resource because we have nothing to compare!
116
+ agentResource = resource
117
+ continue
118
+ }
119
+ if resource .Depth < agentResource .Depth {
120
+ // There's a closer resource!
121
+ agentResource = resource
122
+ continue
123
+ }
124
+ if resource .Depth == agentResource .Depth && resource .Label < agentResource .Label {
125
+ agentResource = resource
126
+ continue
127
+ }
128
+ }
129
+
130
+ agents , exists := resourceAgents [agentResource .Label ]
131
+ if ! exists {
132
+ agents = make ([]* proto.Agent , 0 )
133
+ }
134
+ agents = append (agents , agent )
135
+ resourceAgents [agentResource .Label ] = agents
88
136
}
89
137
90
138
// Manually associate agents with instance IDs.
91
- for _ , resource := range tfResources {
139
+ for _ , resource := range tfResourceByLabel {
92
140
if resource .Type != "coder_agent_instance" {
93
141
continue
94
142
}
@@ -109,31 +157,25 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
109
157
continue
110
158
}
111
159
112
- for _ , agent := range agents {
113
- if agent .Id != agentID {
114
- continue
160
+ for _ , agents := range resourceAgents {
161
+ for _ , agent := range agents {
162
+ if agent .Id != agentID {
163
+ continue
164
+ }
165
+ agent .Auth = & proto.Agent_InstanceId {
166
+ InstanceId : instanceID ,
167
+ }
168
+ break
115
169
}
116
- agent .Auth = & proto.Agent_InstanceId {
117
- InstanceId : instanceID ,
118
- }
119
- break
120
170
}
121
171
}
122
172
123
- type appAttributes struct {
124
- AgentID string `mapstructure:"agent_id"`
125
- Name string `mapstructure:"name"`
126
- Icon string `mapstructure:"icon"`
127
- URL string `mapstructure:"url"`
128
- Command string `mapstructure:"command"`
129
- RelativePath bool `mapstructure:"relative_path"`
130
- }
131
173
// Associate Apps with agents.
132
- for _ , resource := range tfResources {
174
+ for _ , resource := range tfResourceByLabel {
133
175
if resource .Type != "coder_app" {
134
176
continue
135
177
}
136
- var attrs appAttributes
178
+ var attrs agentAppAttributes
137
179
err = mapstructure .Decode (resource .AttributeValues , & attrs )
138
180
if err != nil {
139
181
return nil , xerrors .Errorf ("decode app attributes: %w" , err )
@@ -142,58 +184,34 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
142
184
// Default to the resource name if none is set!
143
185
attrs .Name = resource .Name
144
186
}
145
- for _ , agent := range agents {
146
- if agent .Id != attrs .AgentID {
147
- continue
187
+ for _ , agents := range resourceAgents {
188
+ for _ , agent := range agents {
189
+ // Find agents with the matching ID and associate them!
190
+ if agent .Id != attrs .AgentID {
191
+ continue
192
+ }
193
+ agent .Apps = append (agent .Apps , & proto.App {
194
+ Name : attrs .Name ,
195
+ Command : attrs .Command ,
196
+ Url : attrs .URL ,
197
+ Icon : attrs .Icon ,
198
+ RelativePath : attrs .RelativePath ,
199
+ })
148
200
}
149
- agent .Apps = append (agent .Apps , & proto.App {
150
- Name : attrs .Name ,
151
- Command : attrs .Command ,
152
- Url : attrs .URL ,
153
- Icon : attrs .Icon ,
154
- RelativePath : attrs .RelativePath ,
155
- })
156
201
}
157
202
}
158
203
159
- for _ , resource := range tfResources {
204
+ for _ , resource := range tfResourceByLabel {
160
205
if resource .Mode == tfjson .DataResourceMode {
161
206
continue
162
207
}
163
208
if resource .Type == "coder_agent" || resource .Type == "coder_agent_instance" || resource .Type == "coder_app" {
164
209
continue
165
210
}
166
- agents := findAgents (resourceDependencies , agents , convertAddressToLabel (resource .Address ))
167
- for _ , agent := range agents {
168
- // Didn't use instance identity.
169
- if agent .GetToken () != "" {
170
- continue
171
- }
172
211
173
- // These resource types are for automatically associating an instance ID
174
- // with an agent for authentication.
175
- key , isValid := map [string ]string {
176
- "google_compute_instance" : "instance_id" ,
177
- "aws_instance" : "id" ,
178
- "azurerm_linux_virtual_machine" : "id" ,
179
- "azurerm_windows_virtual_machine" : "id" ,
180
- }[resource .Type ]
181
- if ! isValid {
182
- // The resource type doesn't support
183
- // automatically setting the instance ID.
184
- continue
185
- }
186
- instanceIDRaw , valid := resource .AttributeValues [key ]
187
- if ! valid {
188
- continue
189
- }
190
- instanceID , valid := instanceIDRaw .(string )
191
- if ! valid {
192
- continue
193
- }
194
- agent .Auth = & proto.Agent_InstanceId {
195
- InstanceId : instanceID ,
196
- }
212
+ agents , exists := resourceAgents [convertAddressToLabel (resource .Address )]
213
+ if exists {
214
+ applyAutomaticInstanceID (resource , agents )
197
215
}
198
216
199
217
resources = append (resources , & proto.Resource {
@@ -212,46 +230,81 @@ func convertAddressToLabel(address string) string {
212
230
return strings .Split (address , "[" )[0 ]
213
231
}
214
232
215
- // findAgents recursively searches through resource dependencies
216
- // to find associated agents. Nested is required for indirect
217
- // dependency matching.
218
- func findAgents (resourceDependencies map [string ][]string , agents map [string ]* proto.Agent , resourceLabel string ) []* proto.Agent {
219
- resourceNode , exists := resourceDependencies [resourceLabel ]
220
- if ! exists {
221
- return []* proto.Agent {}
233
+ type graphResource struct {
234
+ Label string
235
+ Depth uint
236
+ }
237
+
238
+ // applyAutomaticInstanceID checks if the resource is one of a set of *magical* IDs
239
+ // that automatically index their identifier for automatic authentication.
240
+ func applyAutomaticInstanceID (resource * tfjson.StateResource , agents []* proto.Agent ) {
241
+ // These resource types are for automatically associating an instance ID
242
+ // with an agent for authentication.
243
+ key , isValid := map [string ]string {
244
+ "google_compute_instance" : "instance_id" ,
245
+ "aws_instance" : "id" ,
246
+ "azurerm_linux_virtual_machine" : "id" ,
247
+ "azurerm_windows_virtual_machine" : "id" ,
248
+ }[resource .Type ]
249
+ if ! isValid {
250
+ return
222
251
}
223
- // Associate resources that depend on an agent.
224
- resourceAgents := make ([]* proto.Agent , 0 )
225
- for _ , dep := range resourceNode {
226
- var has bool
227
- agent , has := agents [dep ]
228
- if ! has {
229
- resourceAgents = append (resourceAgents , findAgents (resourceDependencies , agents , dep )... )
252
+
253
+ // The resource type doesn't support
254
+ // automatically setting the instance ID.
255
+ instanceIDRaw , isValid := resource .AttributeValues [key ]
256
+ if ! isValid {
257
+ return
258
+ }
259
+ instanceID , isValid := instanceIDRaw .(string )
260
+ if ! isValid {
261
+ return
262
+ }
263
+ for _ , agent := range agents {
264
+ // Didn't use instance identity.
265
+ if agent .GetToken () != "" {
230
266
continue
231
267
}
232
- // An agent must be deleted after being assigned so it isn't referenced twice.
233
- delete (agents , dep )
234
- resourceAgents = append (resourceAgents , agent )
268
+ if agent .GetInstanceId () != "" {
269
+ // If an instance ID is manually specified, do not override!
270
+ continue
271
+ }
272
+
273
+ agent .Auth = & proto.Agent_InstanceId {
274
+ InstanceId : instanceID ,
275
+ }
235
276
}
236
- return resourceAgents
237
277
}
238
278
239
- // findDependenciesWithLabels recursively finds nodes with labels (resource and data nodes)
240
- // to build a dependency tree.
241
- func findDependenciesWithLabels (graph * gographviz.Graph , nodeName string ) []string {
242
- dependencies := make ([]string , 0 )
243
- for destination := range graph .Edges .SrcToDsts [nodeName ] {
244
- dependencyNode , exists := graph .Nodes .Lookup [destination ]
279
+ // findResourcesUpGraph traverses upwards in a graph until a resource is found,
280
+ // then it stores the depth it was found at, and continues working up the tree.
281
+ func findResourcesUpGraph (graph * gographviz.Graph , tfResourceByLabel map [string ]* tfjson.StateResource , nodeName string , currentDepth uint ) []* graphResource {
282
+ graphResources := make ([]* graphResource , 0 )
283
+ for destination := range graph .Edges .DstToSrcs [nodeName ] {
284
+ destinationNode := graph .Nodes .Lookup [destination ]
285
+ // Work our way up the tree!
286
+ graphResources = append (graphResources , findResourcesUpGraph (graph , tfResourceByLabel , destinationNode .Name , currentDepth + 1 )... )
287
+
288
+ destinationLabel , exists := destinationNode .Attrs ["label" ]
245
289
if ! exists {
246
290
continue
247
291
}
248
- label , exists := dependencyNode .Attrs ["label" ]
292
+ destinationLabel = strings .Trim (destinationLabel , `"` )
293
+ resource , exists := tfResourceByLabel [destinationLabel ]
249
294
if ! exists {
250
- dependencies = append (dependencies , findDependenciesWithLabels (graph , dependencyNode .Name )... )
251
295
continue
252
296
}
253
- label = strings .Trim (label , `"` )
254
- dependencies = append (dependencies , label )
297
+ if resource .Mode != tfjson .ManagedResourceMode {
298
+ continue
299
+ }
300
+ if strings .HasPrefix (resource .Type , "coder_" ) {
301
+ continue
302
+ }
303
+ graphResources = append (graphResources , & graphResource {
304
+ Label : destinationLabel ,
305
+ Depth : currentDepth ,
306
+ })
255
307
}
256
- return dependencies
308
+
309
+ return graphResources
257
310
}
0 commit comments