Skip to content

Commit 6f4047f

Browse files
committed
fix: Use explicit resource order when assocating agents
This cleans up agent association code to explicitly map a single agent to a single resource. This will fix #1884, and unblock a prospect from beginning a POC.
1 parent 0ec1e8f commit 6f4047f

21 files changed

+723
-216
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"sdktrace",
6464
"Signup",
6565
"sourcemapped",
66+
"Srcs",
6667
"stretchr",
6768
"TCGETS",
6869
"tcpip",

provisioner/terraform/resources.go

Lines changed: 176 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
1638
func 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,83 @@ 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+
// Data sources cannot be associated with agents for now!
298+
if resource.Mode != tfjson.ManagedResourceMode {
299+
continue
300+
}
301+
// Don't associate Coder resources with other Coder resources!
302+
if strings.HasPrefix(resource.Type, "coder_") {
303+
continue
304+
}
305+
graphResources = append(graphResources, &graphResource{
306+
Label: destinationLabel,
307+
Depth: currentDepth,
308+
})
255309
}
256-
return dependencies
310+
311+
return graphResources
257312
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy