Skip to content

Commit 07d4171

Browse files
authored
fix(provisioner): handle multiple agents, apps, scripts and envs (#13741)
1 parent f6639b7 commit 07d4171

17 files changed

+2786
-4
lines changed

provisioner/terraform/resources.go

+29-3
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,11 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
427427
for _, agents := range resourceAgents {
428428
for _, agent := range agents {
429429
// Find agents with the matching ID and associate them!
430-
if agent.Id != attrs.AgentID {
430+
431+
if !dependsOnAgent(graph, agent, attrs.AgentID, resource) {
431432
continue
432433
}
434+
433435
agent.Apps = append(agent.Apps, &proto.App{
434436
Slug: attrs.Slug,
435437
DisplayName: attrs.DisplayName,
@@ -461,7 +463,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
461463
for _, agents := range resourceAgents {
462464
for _, agent := range agents {
463465
// Find agents with the matching ID and associate them!
464-
if agent.Id != attrs.AgentID {
466+
if !dependsOnAgent(graph, agent, attrs.AgentID, resource) {
465467
continue
466468
}
467469
agent.ExtraEnvs = append(agent.ExtraEnvs, &proto.Env{
@@ -487,7 +489,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
487489
for _, agents := range resourceAgents {
488490
for _, agent := range agents {
489491
// Find agents with the matching ID and associate them!
490-
if agent.Id != attrs.AgentID {
492+
if !dependsOnAgent(graph, agent, attrs.AgentID, resource) {
491493
continue
492494
}
493495
agent.Scripts = append(agent.Scripts, &proto.Script{
@@ -748,6 +750,30 @@ func convertAddressToLabel(address string) string {
748750
return cut
749751
}
750752

753+
func dependsOnAgent(graph *gographviz.Graph, agent *proto.Agent, resourceAgentID string, resource *tfjson.StateResource) bool {
754+
// Plan: we need to find if there is edge between the agent and the resource.
755+
if agent.Id == "" && resourceAgentID == "" {
756+
resourceNodeSuffix := fmt.Sprintf(`] %s.%s (expand)"`, resource.Type, resource.Name)
757+
agentNodeSuffix := fmt.Sprintf(`] coder_agent.%s (expand)"`, agent.Name)
758+
759+
// Traverse the graph to check if the coder_<resource_type> depends on coder_agent.
760+
for _, dst := range graph.Edges.SrcToDsts {
761+
for _, edges := range dst {
762+
for _, edge := range edges {
763+
if strings.HasSuffix(edge.Src, resourceNodeSuffix) &&
764+
strings.HasSuffix(edge.Dst, agentNodeSuffix) {
765+
return true
766+
}
767+
}
768+
}
769+
}
770+
return false
771+
}
772+
773+
// Provision: agent ID and child resource ID are present
774+
return agent.Id == resourceAgentID
775+
}
776+
751777
type graphResource struct {
752778
Label string
753779
Depth uint

provisioner/terraform/resources_test.go

+162-1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,150 @@ func TestConvertResources(t *testing.T) {
219219
}},
220220
}},
221221
},
222+
"multiple-agents-multiple-apps": {
223+
resources: []*proto.Resource{{
224+
Name: "dev1",
225+
Type: "null_resource",
226+
Agents: []*proto.Agent{{
227+
Name: "dev1",
228+
OperatingSystem: "linux",
229+
Architecture: "amd64",
230+
Apps: []*proto.App{
231+
{
232+
Slug: "app1",
233+
DisplayName: "app1",
234+
// Subdomain defaults to false if unspecified.
235+
Subdomain: false,
236+
},
237+
{
238+
Slug: "app2",
239+
DisplayName: "app2",
240+
Subdomain: true,
241+
Healthcheck: &proto.Healthcheck{
242+
Url: "http://localhost:13337/healthz",
243+
Interval: 5,
244+
Threshold: 6,
245+
},
246+
},
247+
},
248+
Auth: &proto.Agent_Token{},
249+
ConnectionTimeoutSeconds: 120,
250+
DisplayApps: &displayApps,
251+
}},
252+
}, {
253+
Name: "dev2",
254+
Type: "null_resource",
255+
Agents: []*proto.Agent{{
256+
Name: "dev2",
257+
OperatingSystem: "linux",
258+
Architecture: "amd64",
259+
Apps: []*proto.App{
260+
{
261+
Slug: "app3",
262+
DisplayName: "app3",
263+
Subdomain: false,
264+
},
265+
},
266+
Auth: &proto.Agent_Token{},
267+
ConnectionTimeoutSeconds: 120,
268+
DisplayApps: &displayApps,
269+
}},
270+
}},
271+
},
272+
"multiple-agents-multiple-envs": {
273+
resources: []*proto.Resource{{
274+
Name: "dev1",
275+
Type: "null_resource",
276+
Agents: []*proto.Agent{{
277+
Name: "dev1",
278+
OperatingSystem: "linux",
279+
Architecture: "amd64",
280+
ExtraEnvs: []*proto.Env{
281+
{
282+
Name: "ENV_1",
283+
Value: "Env 1",
284+
},
285+
{
286+
Name: "ENV_2",
287+
Value: "Env 2",
288+
},
289+
},
290+
Auth: &proto.Agent_Token{},
291+
ConnectionTimeoutSeconds: 120,
292+
DisplayApps: &displayApps,
293+
}},
294+
}, {
295+
Name: "dev2",
296+
Type: "null_resource",
297+
Agents: []*proto.Agent{{
298+
Name: "dev2",
299+
OperatingSystem: "linux",
300+
Architecture: "amd64",
301+
ExtraEnvs: []*proto.Env{
302+
{
303+
Name: "ENV_3",
304+
Value: "Env 3",
305+
},
306+
},
307+
Auth: &proto.Agent_Token{},
308+
ConnectionTimeoutSeconds: 120,
309+
DisplayApps: &displayApps,
310+
}},
311+
}, {
312+
Name: "env1",
313+
Type: "coder_env",
314+
}, {
315+
Name: "env2",
316+
Type: "coder_env",
317+
}, {
318+
Name: "env3",
319+
Type: "coder_env",
320+
}},
321+
},
322+
"multiple-agents-multiple-scripts": {
323+
resources: []*proto.Resource{{
324+
Name: "dev1",
325+
Type: "null_resource",
326+
Agents: []*proto.Agent{{
327+
Name: "dev1",
328+
OperatingSystem: "linux",
329+
Architecture: "amd64",
330+
Scripts: []*proto.Script{
331+
{
332+
DisplayName: "Foobar Script 1",
333+
Script: "echo foobar 1",
334+
RunOnStart: true,
335+
},
336+
{
337+
DisplayName: "Foobar Script 2",
338+
Script: "echo foobar 2",
339+
RunOnStart: true,
340+
},
341+
},
342+
Auth: &proto.Agent_Token{},
343+
ConnectionTimeoutSeconds: 120,
344+
DisplayApps: &displayApps,
345+
}},
346+
}, {
347+
Name: "dev2",
348+
Type: "null_resource",
349+
Agents: []*proto.Agent{{
350+
Name: "dev2",
351+
OperatingSystem: "linux",
352+
Architecture: "amd64",
353+
Scripts: []*proto.Script{
354+
{
355+
DisplayName: "Foobar Script 3",
356+
Script: "echo foobar 3",
357+
RunOnStart: true,
358+
},
359+
},
360+
Auth: &proto.Agent_Token{},
361+
ConnectionTimeoutSeconds: 120,
362+
DisplayApps: &displayApps,
363+
}},
364+
}},
365+
},
222366
// Tests fetching metadata about workspace resources.
223367
"resource-metadata": {
224368
resources: []*proto.Resource{{
@@ -565,6 +709,18 @@ func TestConvertResources(t *testing.T) {
565709
sortResources(state.Resources)
566710
sortExternalAuthProviders(state.ExternalAuthProviders)
567711

712+
for _, resource := range state.Resources {
713+
for _, agent := range resource.Agents {
714+
agent.Id = ""
715+
if agent.GetToken() != "" {
716+
agent.Auth = &proto.Agent_Token{}
717+
}
718+
if agent.GetInstanceId() != "" {
719+
agent.Auth = &proto.Agent_InstanceId{}
720+
}
721+
}
722+
}
723+
568724
expectedNoMetadata := make([]*proto.Resource, 0)
569725
for _, resource := range expected.resources {
570726
resourceCopy, _ := protobuf.Clone(resource).(*proto.Resource)
@@ -642,7 +798,6 @@ func TestConvertResources(t *testing.T) {
642798
var resourcesMap []map[string]interface{}
643799
err = json.Unmarshal(data, &resourcesMap)
644800
require.NoError(t, err)
645-
646801
require.Equal(t, expectedMap, resourcesMap)
647802
require.ElementsMatch(t, expected.externalAuthProviders, state.ExternalAuthProviders)
648803
})
@@ -911,6 +1066,12 @@ func sortResources(resources []*proto.Resource) {
9111066
sort.Slice(agent.Apps, func(i, j int) bool {
9121067
return agent.Apps[i].Slug < agent.Apps[j].Slug
9131068
})
1069+
sort.Slice(agent.ExtraEnvs, func(i, j int) bool {
1070+
return agent.ExtraEnvs[i].Name < agent.ExtraEnvs[j].Name
1071+
})
1072+
sort.Slice(agent.Scripts, func(i, j int) bool {
1073+
return agent.Scripts[i].DisplayName < agent.Scripts[j].DisplayName
1074+
})
9141075
}
9151076
sort.Slice(resource.Agents, func(i, j int) bool {
9161077
return resource.Agents[i].Name < resource.Agents[j].Name
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "0.22.0"
6+
}
7+
}
8+
}
9+
10+
resource "coder_agent" "dev1" {
11+
os = "linux"
12+
arch = "amd64"
13+
}
14+
15+
resource "coder_agent" "dev2" {
16+
os = "linux"
17+
arch = "amd64"
18+
}
19+
20+
# app1 is for testing subdomain default.
21+
resource "coder_app" "app1" {
22+
agent_id = coder_agent.dev1.id
23+
slug = "app1"
24+
# subdomain should default to false.
25+
# subdomain = false
26+
}
27+
28+
# app2 tests that subdomaincan be true, and that healthchecks work.
29+
resource "coder_app" "app2" {
30+
agent_id = coder_agent.dev1.id
31+
slug = "app2"
32+
subdomain = true
33+
healthcheck {
34+
url = "http://localhost:13337/healthz"
35+
interval = 5
36+
threshold = 6
37+
}
38+
}
39+
40+
# app3 tests that subdomain can explicitly be false.
41+
resource "coder_app" "app3" {
42+
agent_id = coder_agent.dev2.id
43+
slug = "app3"
44+
subdomain = false
45+
}
46+
47+
resource "null_resource" "dev1" {
48+
depends_on = [
49+
coder_agent.dev1
50+
]
51+
}
52+
53+
resource "null_resource" "dev2" {
54+
depends_on = [
55+
coder_agent.dev2
56+
]
57+
}

provisioner/terraform/testdata/multiple-agents-multiple-apps/multiple-agents-multiple-apps.tfplan.dot

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)