- Notifications
You must be signed in to change notification settings - Fork928
fix: Use explicit resource order when assocating agents#2219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -63,6 +63,7 @@ | ||
"sdktrace", | ||
"Signup", | ||
"sourcemapped", | ||
"Srcs", | ||
"stretchr", | ||
"TCGETS", | ||
"tcpip", | ||
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -11,6 +11,28 @@ import ( | ||||||||
"github.com/coder/coder/provisionersdk/proto" | ||||||||
) | ||||||||
// A mapping of attributes on the "coder_agent" resource. | ||||||||
type agentAttributes struct { | ||||||||
Auth string `mapstructure:"auth"` | ||||||||
OperatingSystem string `mapstructure:"os"` | ||||||||
Architecture string `mapstructure:"arch"` | ||||||||
Directory string `mapstructure:"dir"` | ||||||||
ID string `mapstructure:"id"` | ||||||||
Token string `mapstructure:"token"` | ||||||||
Env map[string]string `mapstructure:"env"` | ||||||||
StartupScript string `mapstructure:"startup_script"` | ||||||||
} | ||||||||
// A mapping of attributes on the "coder_app" resource. | ||||||||
type agentAppAttributes struct { | ||||||||
AgentID string `mapstructure:"agent_id"` | ||||||||
Name string `mapstructure:"name"` | ||||||||
Icon string `mapstructure:"icon"` | ||||||||
URL string `mapstructure:"url"` | ||||||||
Command string `mapstructure:"command"` | ||||||||
RelativePath bool `mapstructure:"relative_path"` | ||||||||
} | ||||||||
// ConvertResources consumes Terraform state and a GraphViz representation produced by | ||||||||
// `terraform graph` to produce resources consumable by Coder. | ||||||||
func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Resource, error) { | ||||||||
@@ -22,52 +44,36 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res | ||||||||
if err != nil { | ||||||||
return nil, xerrors.Errorf("analyze graph: %w", err) | ||||||||
} | ||||||||
resources := make([]*proto.Resource, 0) | ||||||||
resourceAgents := map[string][]*proto.Agent{} | ||||||||
// Indexes Terraform resources by it's label. The label | ||||||||
// is what "terraform graph" uses to reference nodes. | ||||||||
tfResourceByLabel := map[string]*tfjson.StateResource{} | ||||||||
var findTerraformResources func(mod *tfjson.StateModule) | ||||||||
findTerraformResources = func(mod *tfjson.StateModule) { | ||||||||
Comment on lines +54 to +55 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Can't do that because it calls itself ;p | ||||||||
for _, module := range mod.ChildModules { | ||||||||
findTerraformResources(module) | ||||||||
} | ||||||||
for _, resource := range mod.Resources { | ||||||||
tfResourceByLabel[convertAddressToLabel(resource.Address)] = resource | ||||||||
} | ||||||||
} | ||||||||
findTerraformResources(module) | ||||||||
//Find all agents! | ||||||||
for _,tfResource := rangetfResourceByLabel { | ||||||||
iftfResource.Type != "coder_agent" { | ||||||||
continue | ||||||||
} | ||||||||
var attrs agentAttributes | ||||||||
err = mapstructure.Decode(tfResource.AttributeValues, &attrs) | ||||||||
if err != nil { | ||||||||
return nil, xerrors.Errorf("decode agent attributes: %w", err) | ||||||||
} | ||||||||
agent := &proto.Agent{ | ||||||||
Name:tfResource.Name, | ||||||||
Id: attrs.ID, | ||||||||
Env: attrs.Env, | ||||||||
StartupScript: attrs.StartupScript, | ||||||||
@@ -81,14 +87,56 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res | ||||||||
Token: attrs.Token, | ||||||||
} | ||||||||
default: | ||||||||
// If token authentication isn't specified, | ||||||||
// assume instance auth. It's our only other | ||||||||
// authentication type! | ||||||||
agent.Auth = &proto.Agent_InstanceId{} | ||||||||
} | ||||||||
// The label is used to find the graph node! | ||||||||
agentLabel := convertAddressToLabel(tfResource.Address) | ||||||||
var agentNode *gographviz.Node | ||||||||
for _, node := range graph.Nodes.Lookup { | ||||||||
// The node attributes surround the label with quotes. | ||||||||
if strings.Trim(node.Attrs["label"], `"`) != agentLabel { | ||||||||
continue | ||||||||
} | ||||||||
agentNode = node | ||||||||
break | ||||||||
} | ||||||||
if agentNode == nil { | ||||||||
return nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel) | ||||||||
} | ||||||||
var agentResource *graphResource | ||||||||
for _, resource := range findResourcesUpGraph(graph, tfResourceByLabel, agentNode.Name, 0) { | ||||||||
if agentResource == nil { | ||||||||
// Default to the first resource because we have nothing to compare! | ||||||||
agentResource = resource | ||||||||
continue | ||||||||
} | ||||||||
if resource.Depth < agentResource.Depth { | ||||||||
// There's a closer resource! | ||||||||
agentResource = resource | ||||||||
continue | ||||||||
} | ||||||||
if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label { | ||||||||
agentResource = resource | ||||||||
continue | ||||||||
} | ||||||||
} | ||||||||
agents, exists := resourceAgents[agentResource.Label] | ||||||||
if !exists { | ||||||||
agents = make([]*proto.Agent, 0) | ||||||||
} | ||||||||
agents = append(agents, agent) | ||||||||
resourceAgents[agentResource.Label] = agents | ||||||||
} | ||||||||
// Manually associate agents with instance IDs. | ||||||||
for _, resource := rangetfResourceByLabel { | ||||||||
if resource.Type != "coder_agent_instance" { | ||||||||
continue | ||||||||
} | ||||||||
@@ -109,31 +157,25 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res | ||||||||
continue | ||||||||
} | ||||||||
for _, agents := range resourceAgents { | ||||||||
for _, agent := range agents { | ||||||||
if agent.Id != agentID { | ||||||||
continue | ||||||||
} | ||||||||
agent.Auth = &proto.Agent_InstanceId{ | ||||||||
InstanceId: instanceID, | ||||||||
} | ||||||||
break | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
// Associate Apps with agents. | ||||||||
for _, resource := rangetfResourceByLabel { | ||||||||
if resource.Type != "coder_app" { | ||||||||
continue | ||||||||
} | ||||||||
var attrsagentAppAttributes | ||||||||
err = mapstructure.Decode(resource.AttributeValues, &attrs) | ||||||||
if err != nil { | ||||||||
return nil, xerrors.Errorf("decode app attributes: %w", err) | ||||||||
@@ -142,58 +184,34 @@ func ConvertResources(module *tfjson.StateModule, rawGraph string) ([]*proto.Res | ||||||||
// Default to the resource name if none is set! | ||||||||
attrs.Name = resource.Name | ||||||||
} | ||||||||
for _, agents := range resourceAgents { | ||||||||
for _, agent := range agents { | ||||||||
// Find agents with the matching ID and associate them! | ||||||||
if agent.Id != attrs.AgentID { | ||||||||
continue | ||||||||
} | ||||||||
agent.Apps = append(agent.Apps, &proto.App{ | ||||||||
Name: attrs.Name, | ||||||||
Command: attrs.Command, | ||||||||
Url: attrs.URL, | ||||||||
Icon: attrs.Icon, | ||||||||
RelativePath: attrs.RelativePath, | ||||||||
}) | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
for _, resource := rangetfResourceByLabel { | ||||||||
if resource.Mode == tfjson.DataResourceMode { | ||||||||
continue | ||||||||
} | ||||||||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" { | ||||||||
continue | ||||||||
} | ||||||||
agents, exists := resourceAgents[convertAddressToLabel(resource.Address)] | ||||||||
if exists { | ||||||||
applyAutomaticInstanceID(resource, agents) | ||||||||
} | ||||||||
resources = append(resources, &proto.Resource{ | ||||||||
@@ -212,46 +230,83 @@ func convertAddressToLabel(address string) string { | ||||||||
return strings.Split(address, "[")[0] | ||||||||
} | ||||||||
type graphResource struct { | ||||||||
Label string | ||||||||
Depth uint | ||||||||
} | ||||||||
// applyAutomaticInstanceID checks if the resource is one of a set of *magical* IDs | ||||||||
// that automatically index their identifier for automatic authentication. | ||||||||
func applyAutomaticInstanceID(resource *tfjson.StateResource, agents []*proto.Agent) { | ||||||||
// These resource types are for automatically associating an instance ID | ||||||||
// with an agent for authentication. | ||||||||
key, isValid := map[string]string{ | ||||||||
"google_compute_instance": "instance_id", | ||||||||
"aws_instance": "id", | ||||||||
"azurerm_linux_virtual_machine": "id", | ||||||||
"azurerm_windows_virtual_machine": "id", | ||||||||
}[resource.Type] | ||||||||
if !isValid { | ||||||||
return | ||||||||
} | ||||||||
// The resource type doesn't support | ||||||||
// automatically setting the instance ID. | ||||||||
instanceIDRaw, isValid := resource.AttributeValues[key] | ||||||||
if !isValid { | ||||||||
return | ||||||||
} | ||||||||
instanceID, isValid := instanceIDRaw.(string) | ||||||||
if !isValid { | ||||||||
return | ||||||||
} | ||||||||
for _, agent := range agents { | ||||||||
// Didn't use instance identity. | ||||||||
if agent.GetToken() != "" { | ||||||||
continue | ||||||||
} | ||||||||
if agent.GetInstanceId() != "" { | ||||||||
// If an instance ID is manually specified, do not override! | ||||||||
continue | ||||||||
} | ||||||||
agent.Auth = &proto.Agent_InstanceId{ | ||||||||
InstanceId: instanceID, | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
// findResourcesUpGraph traverses upwards in a graph until a resource is found, | ||||||||
// then it stores the depth it was found at, and continues working up the tree. | ||||||||
func findResourcesUpGraph(graph *gographviz.Graph, tfResourceByLabel map[string]*tfjson.StateResource, nodeName string, currentDepth uint) []*graphResource { | ||||||||
graphResources := make([]*graphResource, 0) | ||||||||
for destination := range graph.Edges.DstToSrcs[nodeName] { | ||||||||
destinationNode := graph.Nodes.Lookup[destination] | ||||||||
// Work our way up the tree! | ||||||||
graphResources = append(graphResources, findResourcesUpGraph(graph, tfResourceByLabel, destinationNode.Name, currentDepth+1)...) | ||||||||
destinationLabel, exists := destinationNode.Attrs["label"] | ||||||||
if !exists { | ||||||||
continue | ||||||||
} | ||||||||
destinationLabel = strings.Trim(destinationLabel, `"`) | ||||||||
resource, exists := tfResourceByLabel[destinationLabel] | ||||||||
if !exists { | ||||||||
continue | ||||||||
} | ||||||||
// Data sources cannot be associated with agents for now! | ||||||||
if resource.Mode != tfjson.ManagedResourceMode { | ||||||||
continue | ||||||||
} | ||||||||
// Don't associate Coder resources with other Coder resources! | ||||||||
if strings.HasPrefix(resource.Type, "coder_") { | ||||||||
continue | ||||||||
} | ||||||||
graphResources = append(graphResources, &graphResource{ | ||||||||
Label: destinationLabel, | ||||||||
Depth: currentDepth, | ||||||||
}) | ||||||||
} | ||||||||
return graphResources | ||||||||
} |
Uh oh!
There was an error while loading.Please reload this page.