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

Skip to content

Commit 1033852

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 1033852

21 files changed

+724
-216
lines changed

.vscode/settings.json

+1
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

+176-121
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)