@@ -11,6 +11,28 @@ import (
1111 "github.com/coder/coder/provisionersdk/proto"
1212)
1313
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+
1436// ConvertResources consumes Terraform state and a GraphViz representation produced by
1537// `terraform graph` to produce resources consumable by Coder.
1638func ConvertResources (module * tfjson.StateModule , rawGraph string ) ([]* proto.Resource , error ) {
@@ -22,52 +44,36 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
2244 if err != nil {
2345 return nil , xerrors .Errorf ("analyze graph: %w" , err )
2446 }
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- }
3447
3548 resources := make ([]* proto.Resource , 0 )
36- agents := map [string ]* proto.Agent {}
49+ resourceAgents := map [string ][ ]* proto.Agent {}
3750
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 ) {
4156 for _ , module := range mod .ChildModules {
42- appendResources (module )
57+ findTerraformResources (module )
58+ }
59+ for _ , resource := range mod .Resources {
60+ tfResourceByLabel [convertAddressToLabel (resource .Address )] = resource
4361 }
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"`
5762 }
63+ findTerraformResources (module )
5864
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" {
6268 continue
6369 }
6470 var attrs agentAttributes
65- err = mapstructure .Decode (resource .AttributeValues , & attrs )
71+ err = mapstructure .Decode (tfResource .AttributeValues , & attrs )
6672 if err != nil {
6773 return nil , xerrors .Errorf ("decode agent attributes: %w" , err )
6874 }
6975 agent := & proto.Agent {
70- Name : resource .Name ,
76+ Name : tfResource .Name ,
7177 Id : attrs .ID ,
7278 Env : attrs .Env ,
7379 StartupScript : attrs .StartupScript ,
@@ -81,14 +87,56 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
8187 Token : attrs .Token ,
8288 }
8389 default :
90+ // If token authentication isn't specified,
91+ // assume instance auth. It's our only other
92+ // authentication type!
8493 agent .Auth = & proto.Agent_InstanceId {}
8594 }
8695
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
88136 }
89137
90138 // Manually associate agents with instance IDs.
91- for _ , resource := range tfResources {
139+ for _ , resource := range tfResourceByLabel {
92140 if resource .Type != "coder_agent_instance" {
93141 continue
94142 }
@@ -109,31 +157,25 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
109157 continue
110158 }
111159
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
115169 }
116- agent .Auth = & proto.Agent_InstanceId {
117- InstanceId : instanceID ,
118- }
119- break
120170 }
121171 }
122172
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- }
131173 // Associate Apps with agents.
132- for _ , resource := range tfResources {
174+ for _ , resource := range tfResourceByLabel {
133175 if resource .Type != "coder_app" {
134176 continue
135177 }
136- var attrs appAttributes
178+ var attrs agentAppAttributes
137179 err = mapstructure .Decode (resource .AttributeValues , & attrs )
138180 if err != nil {
139181 return nil , xerrors .Errorf ("decode app attributes: %w" , err )
@@ -142,58 +184,34 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res
142184 // Default to the resource name if none is set!
143185 attrs .Name = resource .Name
144186 }
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+ })
148200 }
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- })
156201 }
157202 }
158203
159- for _ , resource := range tfResources {
204+ for _ , resource := range tfResourceByLabel {
160205 if resource .Mode == tfjson .DataResourceMode {
161206 continue
162207 }
163208 if resource .Type == "coder_agent" || resource .Type == "coder_agent_instance" || resource .Type == "coder_app" {
164209 continue
165210 }
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- }
172211
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 )
197215 }
198216
199217 resources = append (resources , & proto.Resource {
@@ -212,46 +230,81 @@ func convertAddressToLabel(address string) string {
212230 return strings .Split (address , "[" )[0 ]
213231}
214232
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
222251 }
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 () != "" {
230266 continue
231267 }
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+ }
235276 }
236- return resourceAgents
237277}
238278
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" ]
245289 if ! exists {
246290 continue
247291 }
248- label , exists := dependencyNode .Attrs ["label" ]
292+ destinationLabel = strings .Trim (destinationLabel , `"` )
293+ resource , exists := tfResourceByLabel [destinationLabel ]
249294 if ! exists {
250- dependencies = append (dependencies , findDependenciesWithLabels (graph , dependencyNode .Name )... )
251295 continue
252296 }
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+ })
255307 }
256- return dependencies
308+
309+ return graphResources
257310}
0 commit comments