-
Notifications
You must be signed in to change notification settings - Fork 937
fix: Allow nested Terraform resources #1093
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,7 @@ | |
"tcpip", | ||
"TCSETS", | ||
"tfexec", | ||
"tfjson", | ||
"tfstate", | ||
"trimprefix", | ||
"unconvert", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
name: Develop in a container on a Google Cloud VM | ||
description: Get started with Linux development on Google Cloud. | ||
tags: [cloud, google, container] | ||
--- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
terraform { | ||
required_providers { | ||
coder = { | ||
source = "coder/coder" | ||
version = "~> 0.3.1" | ||
} | ||
google = { | ||
source = "hashicorp/google" | ||
version = "~> 4.15" | ||
} | ||
} | ||
} | ||
|
||
variable "service_account" { | ||
description = <<EOF | ||
Coder requires a Google Cloud Service Account to provision workspaces. | ||
|
||
1. Create a service account: | ||
https://console.cloud.google.com/projectselector/iam-admin/serviceaccounts/create | ||
2. Add the roles: | ||
- Compute Admin | ||
- Service Account User | ||
3. Click on the created key, and navigate to the "Keys" tab. | ||
4. Click "Add key", then "Create new key". | ||
5. Generate a JSON private key, and paste the contents below. | ||
EOF | ||
sensitive = true | ||
} | ||
|
||
variable "zone" { | ||
description = "What region should your workspace live in?" | ||
default = "us-central1-a" | ||
validation { | ||
condition = contains(["northamerica-northeast1-a", "us-central1-a", "us-west2-c", "europe-west4-b", "southamerica-east1-a"], var.zone) | ||
error_message = "Invalid zone!" | ||
} | ||
} | ||
|
||
provider "google" { | ||
zone = var.zone | ||
credentials = var.service_account | ||
project = jsondecode(var.service_account).project_id | ||
} | ||
|
||
data "google_compute_default_service_account" "default" { | ||
} | ||
|
||
data "coder_workspace" "me" { | ||
} | ||
|
||
resource "coder_agent" "dev" { | ||
auth = "google-instance-identity" | ||
arch = "amd64" | ||
os = "linux" | ||
} | ||
|
||
module "gce-container" { | ||
source = "terraform-google-modules/container-vm/google" | ||
version = "3.0.0" | ||
|
||
container = { | ||
image = "mcr.microsoft.com/vscode/devcontainers/go:1" | ||
command = ["sh"] | ||
args = ["-c", coder_agent.dev.init_script] | ||
securityContext = { | ||
privileged : true | ||
} | ||
} | ||
} | ||
|
||
resource "google_compute_instance" "dev" { | ||
zone = var.zone | ||
count = data.coder_workspace.me.start_count | ||
name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}" | ||
machine_type = "e2-medium" | ||
network_interface { | ||
network = "default" | ||
access_config { | ||
// Ephemeral public IP | ||
} | ||
} | ||
boot_disk { | ||
initialize_params { | ||
image = module.gce-container.source_image | ||
} | ||
} | ||
service_account { | ||
email = data.google_compute_default_service_account.default.email | ||
scopes = ["cloud-platform"] | ||
} | ||
metadata = { | ||
"gce-container-declaration" = module.gce-container.metadata_value | ||
} | ||
labels = { | ||
container-vm = module.gce-container.vm_container_label | ||
} | ||
} | ||
|
||
resource "coder_agent_instance" "dev" { | ||
count = data.coder_workspace.me.start_count | ||
agent_id = coder_agent.dev.id | ||
instance_id = google_compute_instance.dev[0].instance_id | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import ( | |
|
||
"github.com/awalterschulze/gographviz" | ||
"github.com/hashicorp/terraform-exec/tfexec" | ||
tfjson "github.com/hashicorp/terraform-json" | ||
"github.com/mitchellh/mapstructure" | ||
"golang.org/x/xerrors" | ||
|
||
|
@@ -88,15 +89,24 @@ func (t *terraform) Provision(stream proto.DRPCProvisioner_ProvisionStream) erro | |
}) | ||
} | ||
}() | ||
terraformEnv := map[string]string{} | ||
// Required for "terraform init" to find "git" to | ||
// clone Terraform modules. | ||
for _, env := range os.Environ() { | ||
parts := strings.SplitN(env, "=", 2) | ||
if len(parts) < 2 { | ||
continue | ||
} | ||
terraformEnv[parts[0]] = parts[1] | ||
} | ||
// Only Linux reliably works with the Terraform plugin | ||
// cache directory. It's unknown why this is. | ||
if t.cachePath != "" && runtime.GOOS == "linux" { | ||
err = terraform.SetEnv(map[string]string{ | ||
"TF_PLUGIN_CACHE_DIR": t.cachePath, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("set terraform plugin cache dir: %w", err) | ||
} | ||
terraformEnv["TF_PLUGIN_CACHE_DIR"] = t.cachePath | ||
} | ||
err = terraform.SetEnv(terraformEnv) | ||
if err != nil { | ||
return xerrors.Errorf("set terraform env: %w", err) | ||
} | ||
terraform.SetStdout(writer) | ||
t.logger.Debug(shutdown, "running initialization") | ||
|
@@ -320,40 +330,22 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi | |
agent.StartupScript = startupScript | ||
} | ||
} | ||
if _, has := resource.Expressions["instance_id"]; has { | ||
// This is a dynamic value. If it's expressed, we know | ||
// it's at least an instance ID, which is better than nothing. | ||
agent.Auth = &proto.Agent_InstanceId{ | ||
InstanceId: "", | ||
} | ||
} | ||
|
||
agents[resource.Address] = agent | ||
} | ||
|
||
for _, resource := range plan.PlannedValues.RootModule.Resources { | ||
if resource.Type == "coder_agent" { | ||
if resource.Mode == tfjson.DataResourceMode { | ||
continue | ||
} | ||
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") | ||
resourceNode, exists := resourceDependencies[resourceKey] | ||
if !exists { | ||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { | ||
continue | ||
} | ||
// Associate resources that depend on an agent. | ||
resourceAgents := make([]*proto.Agent, 0) | ||
for _, dep := range resourceNode { | ||
var has bool | ||
agent, has := agents[dep] | ||
if !has { | ||
continue | ||
} | ||
resourceAgents = append(resourceAgents, agent) | ||
} | ||
|
||
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") | ||
resources = append(resources, &proto.Resource{ | ||
Name: resource.Name, | ||
Type: resource.Type, | ||
Agents: resourceAgents, | ||
Agents: findAgents(resourceDependencies, agents, resourceKey), | ||
}) | ||
} | ||
|
||
|
@@ -460,32 +452,25 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state | |
} | ||
|
||
for _, resource := range state.Values.RootModule.Resources { | ||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { | ||
if resource.Mode == tfjson.DataResourceMode { | ||
continue | ||
} | ||
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") | ||
resourceNode, exists := resourceDependencies[resourceKey] | ||
if !exists { | ||
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { | ||
continue | ||
} | ||
// Associate resources that depend on an agent. | ||
resourceAgents := make([]*proto.Agent, 0) | ||
for _, dep := range resourceNode { | ||
var has bool | ||
agent, has := agents[dep] | ||
if !has { | ||
continue | ||
} | ||
resourceAgents = append(resourceAgents, agent) | ||
|
||
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") | ||
resourceAgents := findAgents(resourceDependencies, agents, resourceKey) | ||
for _, agent := range resourceAgents { | ||
// Didn't use instance identity. | ||
if agent.GetToken() != "" { | ||
continue | ||
} | ||
|
||
key, isValid := map[string]string{ | ||
"google_compute_instance": "instance_id", | ||
"aws_instance": "id", | ||
"google_compute_instance": "instance_id", | ||
"aws_instance": "id", | ||
"azurerm_linux_virtual_machine": "id", | ||
"azurerm_windows_virtual_machine": "id", | ||
}[resource.Type] | ||
if !isValid { | ||
// The resource type doesn't support | ||
|
@@ -571,21 +556,50 @@ func findDirectDependencies(rawGraph string) (map[string][]string, error) { | |
continue | ||
} | ||
label = strings.Trim(label, `"`) | ||
direct[label] = findDependenciesWithLabels(graph, node.Name) | ||
} | ||
|
||
dependencies := make([]string, 0) | ||
for destination := range graph.Edges.SrcToDsts[node.Name] { | ||
dependencyNode, exists := graph.Nodes.Lookup[destination] | ||
if !exists { | ||
continue | ||
} | ||
label, exists := dependencyNode.Attrs["label"] | ||
if !exists { | ||
continue | ||
} | ||
label = strings.Trim(label, `"`) | ||
dependencies = append(dependencies, label) | ||
return direct, nil | ||
} | ||
|
||
// findDependenciesWithLabels recursively finds nodes with labels (resource and data nodes) | ||
// to build a dependency tree. | ||
func findDependenciesWithLabels(graph *gographviz.Graph, nodeName string) []string { | ||
dependencies := make([]string, 0) | ||
for destination := range graph.Edges.SrcToDsts[nodeName] { | ||
dependencyNode, exists := graph.Nodes.Lookup[destination] | ||
if !exists { | ||
continue | ||
} | ||
direct[label] = dependencies | ||
label, exists := dependencyNode.Attrs["label"] | ||
if !exists { | ||
dependencies = append(dependencies, findDependenciesWithLabels(graph, dependencyNode.Name)...) | ||
continue | ||
} | ||
label = strings.Trim(label, `"`) | ||
dependencies = append(dependencies, label) | ||
} | ||
return direct, nil | ||
return dependencies | ||
} | ||
|
||
// findAgents recursively searches through resource dependencies | ||
// to find associated agents. Nested is required for indirect | ||
// dependency matching. | ||
func findAgents(resourceDependencies map[string][]string, agents map[string]*proto.Agent, resourceKey string) []*proto.Agent { | ||
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. Would it make sense to have an upper bound on recursion depth "just in case"? 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. We limit payload file size, so I don't think it'd be possible to bomb it beyond 10mb. |
||
resourceNode, exists := resourceDependencies[resourceKey] | ||
if !exists { | ||
return []*proto.Agent{} | ||
} | ||
// Associate resources that depend on an agent. | ||
resourceAgents := make([]*proto.Agent, 0) | ||
for _, dep := range resourceNode { | ||
var has bool | ||
agent, has := agents[dep] | ||
if !has { | ||
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. 👍 |
||
resourceAgents = append(resourceAgents, findAgents(resourceDependencies, agents, dep)...) | ||
continue | ||
} | ||
resourceAgents = append(resourceAgents, agent) | ||
} | ||
return resourceAgents | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍