Skip to content

Commit 3cb370c

Browse files
committed
fix: Allow nested Terraform resources
This fixes the dependency tree by adding recursion. It now finds indirect connections and associates it with an agent. An example is attached which surfaced this issue.
1 parent 3151bef commit 3cb370c

File tree

6 files changed

+275
-71
lines changed

6 files changed

+275
-71
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"tcpip",
5252
"TCSETS",
5353
"tfexec",
54+
"tfjson",
5455
"tfstate",
5556
"trimprefix",
5657
"unconvert",

examples/gcp-vm-container/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
name: Develop in a container on a Google Cloud VM
3+
description: Get started with Linux development on Google Cloud.
4+
tags: [cloud, google, container]
5+
---

examples/gcp-vm-container/main.tf

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "~> 0.3.1"
6+
}
7+
google = {
8+
source = "hashicorp/google"
9+
version = "~> 4.15"
10+
}
11+
}
12+
}
13+
14+
variable "service_account" {
15+
description = <<EOF
16+
Coder requires a Google Cloud Service Account to provision workspaces.
17+
18+
1. Create a service account:
19+
https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create
20+
2. Add the roles:
21+
- Compute Admin
22+
- Service Account User
23+
3. Click on the created key, and navigate to the "Keys" tab.
24+
4. Click "Add key", then "Create new key".
25+
5. Generate a JSON private key, and paste the contents below.
26+
EOF
27+
sensitive = true
28+
}
29+
30+
variable "zone" {
31+
description = "What region should your workspace live in?"
32+
default = "us-central1-a"
33+
validation {
34+
condition = contains(["northamerica-northeast1-a", "us-central1-a", "us-west2-c", "europe-west4-b", "southamerica-east1-a"], var.zone)
35+
error_message = "Invalid zone!"
36+
}
37+
}
38+
39+
provider "google" {
40+
zone = var.zone
41+
credentials = var.service_account
42+
project = jsondecode(var.service_account).project_id
43+
}
44+
45+
data "google_compute_default_service_account" "default" {
46+
}
47+
48+
data "coder_workspace" "me" {
49+
}
50+
51+
resource "coder_agent" "dev" {
52+
auth = "google-instance-identity"
53+
arch = "amd64"
54+
os = "linux"
55+
}
56+
57+
module "gce-container" {
58+
source = "terraform-google-modules/container-vm/google"
59+
version = "3.0.0"
60+
61+
container = {
62+
image = "mcr.microsoft.com/vscode/devcontainers/go:1"
63+
command = ["sh"]
64+
args = ["-c", coder_agent.dev.init_script]
65+
securityContext = {
66+
privileged : true
67+
}
68+
}
69+
}
70+
71+
resource "google_compute_instance" "dev" {
72+
zone = var.zone
73+
count = data.coder_workspace.me.start_count
74+
name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}"
75+
machine_type = "e2-medium"
76+
network_interface {
77+
network = "default"
78+
access_config {
79+
// Ephemeral public IP
80+
}
81+
}
82+
boot_disk {
83+
initialize_params {
84+
image = module.gce-container.source_image
85+
}
86+
}
87+
service_account {
88+
email = data.google_compute_default_service_account.default.email
89+
scopes = ["cloud-platform"]
90+
}
91+
metadata = {
92+
"gce-container-declaration" = module.gce-container.metadata_value
93+
}
94+
labels = {
95+
container-vm = module.gce-container.vm_container_label
96+
}
97+
}
98+
99+
resource "coder_agent_instance" "dev" {
100+
count = data.coder_workspace.me.start_count
101+
agent_id = coder_agent.dev.id
102+
instance_id = google_compute_instance.dev[0].instance_id
103+
}

provisioner/terraform/provision.go

+73-59
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/awalterschulze/gographviz"
1616
"github.com/hashicorp/terraform-exec/tfexec"
17+
tfjson "github.com/hashicorp/terraform-json"
1718
"github.com/mitchellh/mapstructure"
1819
"golang.org/x/xerrors"
1920

@@ -88,15 +89,24 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro
8889
})
8990
}
9091
}()
92+
terraformEnv := map[string]string{}
93+
// Required for "terraform init" to find "git" to
94+
// clone Terraform modules.
95+
for _, env := range os.Environ() {
96+
parts := strings.SplitN(env, "=", 2)
97+
if len(parts) < 2 {
98+
continue
99+
}
100+
terraformEnv[parts[0]] = parts[1]
101+
}
91102
// Only Linux reliably works with the Terraform plugin
92103
// cache directory. It's unknown why this is.
93104
if t.cachePath != "" && runtime.GOOS == "linux" {
94-
err = terraform.SetEnv(map[string]string{
95-
"TF_PLUGIN_CACHE_DIR": t.cachePath,
96-
})
97-
if err != nil {
98-
return xerrors.Errorf("set terraform plugin cache dir: %w", err)
99-
}
105+
terraformEnv["TF_PLUGIN_CACHE_DIR"] = t.cachePath
106+
}
107+
err = terraform.SetEnv(terraformEnv)
108+
if err != nil {
109+
return xerrors.Errorf("set terraform env: %w", err)
100110
}
101111
terraform.SetStdout(writer)
102112
t.logger.Debug(shutdown, "running initialization")
@@ -320,40 +330,22 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi
320330
agent.StartupScript = startupScript
321331
}
322332
}
323-
if _, has := resource.Expressions["instance_id"]; has {
324-
// This is a dynamic value. If it's expressed, we know
325-
// it's at least an instance ID, which is better than nothing.
326-
agent.Auth = &proto.Agent_InstanceId{
327-
InstanceId: "",
328-
}
329-
}
330333

331334
agents[resource.Address] = agent
332335
}
336+
333337
for _, resource := range plan.PlannedValues.RootModule.Resources {
334-
if resource.Type == "coder_agent" {
338+
if resource.Mode == tfjson.DataResourceMode {
335339
continue
336340
}
337-
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
338-
resourceNode, exists := resourceDependencies[resourceKey]
339-
if !exists {
341+
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" {
340342
continue
341343
}
342-
// Associate resources that depend on an agent.
343-
resourceAgents := make([]*proto.Agent, 0)
344-
for _, dep := range resourceNode {
345-
var has bool
346-
agent, has := agents[dep]
347-
if !has {
348-
continue
349-
}
350-
resourceAgents = append(resourceAgents, agent)
351-
}
352-
344+
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
353345
resources = append(resources, &proto.Resource{
354346
Name: resource.Name,
355347
Type: resource.Type,
356-
Agents: resourceAgents,
348+
Agents: findAgents(resourceDependencies, agents, resourceKey),
357349
})
358350
}
359351

@@ -460,32 +452,25 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state
460452
}
461453

462454
for _, resource := range state.Values.RootModule.Resources {
463-
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" {
455+
if resource.Mode == tfjson.DataResourceMode {
464456
continue
465457
}
466-
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
467-
resourceNode, exists := resourceDependencies[resourceKey]
468-
if !exists {
458+
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" {
469459
continue
470460
}
471-
// Associate resources that depend on an agent.
472-
resourceAgents := make([]*proto.Agent, 0)
473-
for _, dep := range resourceNode {
474-
var has bool
475-
agent, has := agents[dep]
476-
if !has {
477-
continue
478-
}
479-
resourceAgents = append(resourceAgents, agent)
480-
461+
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
462+
resourceAgents := findAgents(resourceDependencies, agents, resourceKey)
463+
for _, agent := range resourceAgents {
481464
// Didn't use instance identity.
482465
if agent.GetToken() != "" {
483466
continue
484467
}
485468

486469
key, isValid := map[string]string{
487-
"google_compute_instance": "instance_id",
488-
"aws_instance": "id",
470+
"google_compute_instance": "instance_id",
471+
"aws_instance": "id",
472+
"azurerm_linux_virtual_machine": "id",
473+
"azurerm_windows_virtual_machine": "id",
489474
}[resource.Type]
490475
if !isValid {
491476
// The resource type doesn't support
@@ -571,21 +556,50 @@ func findDirectDependencies(rawGraph string) (map[string][]string, error) {
571556
continue
572557
}
573558
label = strings.Trim(label, `"`)
559+
direct[label] = findDependenciesWithLabels(graph, node.Name)
560+
}
574561

575-
dependencies := make([]string, 0)
576-
for destination := range graph.Edges.SrcToDsts[node.Name] {
577-
dependencyNode, exists := graph.Nodes.Lookup[destination]
578-
if !exists {
579-
continue
580-
}
581-
label, exists := dependencyNode.Attrs["label"]
582-
if !exists {
583-
continue
584-
}
585-
label = strings.Trim(label, `"`)
586-
dependencies = append(dependencies, label)
562+
return direct, nil
563+
}
564+
565+
// findDependenciesWithLabels recursively finds nodes with labels (resource and data nodes)
566+
// to build a dependency tree.
567+
func findDependenciesWithLabels(graph *gographviz.Graph, nodeName string) []string {
568+
dependencies := make([]string, 0)
569+
for destination := range graph.Edges.SrcToDsts[nodeName] {
570+
dependencyNode, exists := graph.Nodes.Lookup[destination]
571+
if !exists {
572+
continue
587573
}
588-
direct[label] = dependencies
574+
label, exists := dependencyNode.Attrs["label"]
575+
if !exists {
576+
dependencies = append(dependencies, findDependenciesWithLabels(graph, dependencyNode.Name)...)
577+
continue
578+
}
579+
label = strings.Trim(label, `"`)
580+
dependencies = append(dependencies, label)
589581
}
590-
return direct, nil
582+
return dependencies
583+
}
584+
585+
// findAgents recursively searches through resource dependencies
586+
// to find associated agents. Nested is required for indirect
587+
// dependency matching.
588+
func findAgents(resourceDependencies map[string][]string, agents map[string]*proto.Agent, resourceKey string) []*proto.Agent {
589+
resourceNode, exists := resourceDependencies[resourceKey]
590+
if !exists {
591+
return []*proto.Agent{}
592+
}
593+
// Associate resources that depend on an agent.
594+
resourceAgents := make([]*proto.Agent, 0)
595+
for _, dep := range resourceNode {
596+
var has bool
597+
agent, has := agents[dep]
598+
if !has {
599+
resourceAgents = append(resourceAgents, findAgents(resourceDependencies, agents, dep)...)
600+
continue
601+
}
602+
resourceAgents = append(resourceAgents, agent)
603+
}
604+
return resourceAgents
591605
}

0 commit comments

Comments
 (0)