Skip to content

Commit

Permalink
fix: Use plan to detect resource agent association (#381)
Browse files Browse the repository at this point in the history
* ci: Update DataDog GitHub branch to fallback to GITHUB_REF

This was detecting branches, but not our "main" branch before.
Hopefully this fixes it!

* Add basic Terraform Provider

* Rename post files to upload

* Add tests for resources

* Skip instance identity test

* Add tests for ensuring agent get's passed through properly

* Fix linting errors

* Add echo path

* Fix agent authentication

* fix: Convert all jobs to use a common resource and agent type

This enables a consistent API for project import and provisioned resources.

* Add "coder_workspace" data source

* feat: Remove magical parameters from being injected

This is a much cleaner abstraction. Explicitly declaring the user
parameters for each provisioner makes for significantly simpler
testing.

* feat: Add graceful exits to provisionerd

Terraform (or other provisioners) may need to cleanup state, or
cancel actions before exit. This adds the ability to gracefully
exit provisionerd.

* Fix cancel error check

* feat: Add destroy to workspace provision job

This enables the full flow of create/update/delete.

* fix: Use plan to detect resource agent association

Before this used the configuration object which detected all resources
regardless of count.
  • Loading branch information
kylecarbs committed Feb 28, 2022
1 parent b6017a7 commit 8acd58a
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 26 deletions.
68 changes: 49 additions & 19 deletions provisioner/terraform/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/mapstructure"
"golang.org/x/xerrors"

Expand Down Expand Up @@ -75,10 +76,13 @@ func (t *terraform) Provision(request *proto.Provision_Request, stream proto.DRP
}

func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream) error {
env := map[string]string{
"CODER_URL": request.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION": strings.ToLower(request.Metadata.WorkspaceTransition.String()),
env := map[string]string{}
for _, envEntry := range os.Environ() {
parts := strings.SplitN(envEntry, "=", 2)
env[parts[0]] = parts[1]
}
env["CODER_URL"] = request.Metadata.CoderUrl
env["CODER_WORKSPACE_TRANSITION"] = strings.ToLower(request.Metadata.WorkspaceTransition.String())
planfilePath := filepath.Join(request.Directory, "terraform.tfplan")
options := []tfexec.PlanOption{tfexec.JSON(true), tfexec.Out(planfilePath)}
for _, param := range request.ParameterValues {
Expand Down Expand Up @@ -159,9 +163,31 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
_ = reader.Close()
<-closeChan

// Maps resource dependencies to expression references.
// This is *required* for a plan, because "DependsOn"
// does not propagate.
resourceDependencies := map[string][]string{}
for _, resource := range plan.Config.RootModule.Resources {
if resource.Expressions == nil {
resource.Expressions = map[string]*tfjson.Expression{}
}
// Count expression is separated for logical reasons,
// but it's simpler syntactically for us to combine here.
if resource.CountExpression != nil {
resource.Expressions["count"] = resource.CountExpression
}
for _, expression := range resource.Expressions {
dependencies, exists := resourceDependencies[resource.Address]
if !exists {
dependencies = []string{}
}
dependencies = append(dependencies, expression.References...)
resourceDependencies[resource.Address] = dependencies
}
}

resources := make([]*proto.Resource, 0)
agents := map[string]*proto.Agent{}
agentDepends := map[string][]string{}

// Store all agents inside the maps!
for _, resource := range plan.Config.RootModule.Resources {
Expand Down Expand Up @@ -196,9 +222,10 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
}
switch authTypeValue {
case "google-instance-identity":
instanceID, _ := block["instance_id"].ConstantValue.(string)
agent.Auth = &proto.Agent_GoogleInstanceIdentity{
GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{
InstanceId: block["instance_id"].ConstantValue.(string),
InstanceId: instanceID,
},
}
default:
Expand All @@ -208,31 +235,33 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
}
}

resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
agents[resourceKey] = agent
agentDepends[resourceKey] = resource.DependsOn
agents[resource.Address] = agent
}

for _, resource := range plan.Config.RootModule.Resources {
for _, resource := range plan.PlannedValues.RootModule.Resources {
if resource.Type == "coder_agent" {
continue
}
// The resource address on planned values can include the indexed
// value like "[0]", but the config doesn't have these, and we don't
// care which index the resource is.
resourceAddress := fmt.Sprintf("%s.%s", resource.Type, resource.Name)
var agent *proto.Agent
// Associate resources that depend on an agent.
for _, dep := range resource.DependsOn {
for _, dependency := range resourceDependencies[resourceAddress] {
var has bool
agent, has = agents[dep]
agent, has = agents[dependency]
if has {
break
}
}
// Associate resources where the agent depends on it.
for agentKey, dependsOn := range agentDepends {
for _, depend := range dependsOn {
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
for agentAddress := range agents {
for _, depend := range resourceDependencies[agentAddress] {
if depend != resourceAddress {
continue
}
agent = agents[agentKey]
agent = agents[agentAddress]
break
}
}
Expand All @@ -254,10 +283,11 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
}

func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Terraform, request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream, statefilePath string) error {
env := []string{
"CODER_URL=" + request.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION=" + strings.ToLower(request.Metadata.WorkspaceTransition.String()),
}
env := os.Environ()
env = append(env,
"CODER_URL="+request.Metadata.CoderUrl,
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(request.Metadata.WorkspaceTransition.String()),
)
vars := []string{}
for _, param := range request.ParameterValues {
switch param.DestinationScheme {
Expand Down
13 changes: 6 additions & 7 deletions provisioner/terraform/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,10 @@ provider "coder" {
Files: map[string]string{
"main.tf": provider + `
resource "coder_agent" "A" {
count = 1
}
resource "null_resource" "A" {
depends_on = [
coder_agent.A
]
count = length(coder_agent.A)
}`,
},
Request: &proto.Provision_Request{
Expand All @@ -266,15 +265,15 @@ provider "coder" {
Files: map[string]string{
"main.tf": provider + `
resource "coder_agent" "A" {
depends_on = [
null_resource.A
]
count = length(null_resource.A)
auth {
type = "google-instance-identity"
instance_id = "an-instance"
}
}
resource "null_resource" "A" {}`,
resource "null_resource" "A" {
count = 1
}`,
},
Request: &proto.Provision_Request{
DryRun: true,
Expand Down

0 comments on commit 8acd58a

Please sign in to comment.