Skip to content

Commit 8acd58a

Browse files
authored
fix: Use plan to detect resource agent association (#381)
* 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.
1 parent b6017a7 commit 8acd58a

File tree

2 files changed

+55
-26
lines changed

2 files changed

+55
-26
lines changed

provisioner/terraform/provision.go

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strings"
1414

1515
"github.com/hashicorp/terraform-exec/tfexec"
16+
tfjson "github.com/hashicorp/terraform-json"
1617
"github.com/mitchellh/mapstructure"
1718
"golang.org/x/xerrors"
1819

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

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

166+
// Maps resource dependencies to expression references.
167+
// This is *required* for a plan, because "DependsOn"
168+
// does not propagate.
169+
resourceDependencies := map[string][]string{}
170+
for _, resource := range plan.Config.RootModule.Resources {
171+
if resource.Expressions == nil {
172+
resource.Expressions = map[string]*tfjson.Expression{}
173+
}
174+
// Count expression is separated for logical reasons,
175+
// but it's simpler syntactically for us to combine here.
176+
if resource.CountExpression != nil {
177+
resource.Expressions["count"] = resource.CountExpression
178+
}
179+
for _, expression := range resource.Expressions {
180+
dependencies, exists := resourceDependencies[resource.Address]
181+
if !exists {
182+
dependencies = []string{}
183+
}
184+
dependencies = append(dependencies, expression.References...)
185+
resourceDependencies[resource.Address] = dependencies
186+
}
187+
}
188+
162189
resources := make([]*proto.Resource, 0)
163190
agents := map[string]*proto.Agent{}
164-
agentDepends := map[string][]string{}
165191

166192
// Store all agents inside the maps!
167193
for _, resource := range plan.Config.RootModule.Resources {
@@ -196,9 +222,10 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
196222
}
197223
switch authTypeValue {
198224
case "google-instance-identity":
225+
instanceID, _ := block["instance_id"].ConstantValue.(string)
199226
agent.Auth = &proto.Agent_GoogleInstanceIdentity{
200227
GoogleInstanceIdentity: &proto.GoogleInstanceIdentityAuth{
201-
InstanceId: block["instance_id"].ConstantValue.(string),
228+
InstanceId: instanceID,
202229
},
203230
}
204231
default:
@@ -208,31 +235,33 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
208235
}
209236
}
210237

211-
resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".")
212-
agents[resourceKey] = agent
213-
agentDepends[resourceKey] = resource.DependsOn
238+
agents[resource.Address] = agent
214239
}
215240

216-
for _, resource := range plan.Config.RootModule.Resources {
241+
for _, resource := range plan.PlannedValues.RootModule.Resources {
217242
if resource.Type == "coder_agent" {
218243
continue
219244
}
245+
// The resource address on planned values can include the indexed
246+
// value like "[0]", but the config doesn't have these, and we don't
247+
// care which index the resource is.
248+
resourceAddress := fmt.Sprintf("%s.%s", resource.Type, resource.Name)
220249
var agent *proto.Agent
221250
// Associate resources that depend on an agent.
222-
for _, dep := range resource.DependsOn {
251+
for _, dependency := range resourceDependencies[resourceAddress] {
223252
var has bool
224-
agent, has = agents[dep]
253+
agent, has = agents[dependency]
225254
if has {
226255
break
227256
}
228257
}
229258
// Associate resources where the agent depends on it.
230-
for agentKey, dependsOn := range agentDepends {
231-
for _, depend := range dependsOn {
232-
if depend != strings.Join([]string{resource.Type, resource.Name}, ".") {
259+
for agentAddress := range agents {
260+
for _, depend := range resourceDependencies[agentAddress] {
261+
if depend != resourceAddress {
233262
continue
234263
}
235-
agent = agents[agentKey]
264+
agent = agents[agentAddress]
236265
break
237266
}
238267
}
@@ -254,10 +283,11 @@ func (t *terraform) runTerraformPlan(ctx context.Context, terraform *tfexec.Terr
254283
}
255284

256285
func (t *terraform) runTerraformApply(ctx context.Context, terraform *tfexec.Terraform, request *proto.Provision_Request, stream proto.DRPCProvisioner_ProvisionStream, statefilePath string) error {
257-
env := []string{
258-
"CODER_URL=" + request.Metadata.CoderUrl,
259-
"CODER_WORKSPACE_TRANSITION=" + strings.ToLower(request.Metadata.WorkspaceTransition.String()),
260-
}
286+
env := os.Environ()
287+
env = append(env,
288+
"CODER_URL="+request.Metadata.CoderUrl,
289+
"CODER_WORKSPACE_TRANSITION="+strings.ToLower(request.Metadata.WorkspaceTransition.String()),
290+
)
261291
vars := []string{}
262292
for _, param := range request.ParameterValues {
263293
switch param.DestinationScheme {

provisioner/terraform/provision_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,10 @@ provider "coder" {
235235
Files: map[string]string{
236236
"main.tf": provider + `
237237
resource "coder_agent" "A" {
238+
count = 1
238239
}
239240
resource "null_resource" "A" {
240-
depends_on = [
241-
coder_agent.A
242-
]
241+
count = length(coder_agent.A)
243242
}`,
244243
},
245244
Request: &proto.Provision_Request{
@@ -266,15 +265,15 @@ provider "coder" {
266265
Files: map[string]string{
267266
"main.tf": provider + `
268267
resource "coder_agent" "A" {
269-
depends_on = [
270-
null_resource.A
271-
]
268+
count = length(null_resource.A)
272269
auth {
273270
type = "google-instance-identity"
274271
instance_id = "an-instance"
275272
}
276273
}
277-
resource "null_resource" "A" {}`,
274+
resource "null_resource" "A" {
275+
count = 1
276+
}`,
278277
},
279278
Request: &proto.Provision_Request{
280279
DryRun: true,

0 commit comments

Comments
 (0)