fix: allow mapped resources in our terraform provider (#6242)

Fixes #6234.
This commit is contained in:
Kyle Carberry 2023-02-16 13:04:40 -06:00 committed by GitHub
parent 2c309194e9
commit d5af536ea2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 831 additions and 307 deletions

View File

@ -90,7 +90,7 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
// Indexes Terraform resources by their label.
// The label is what "terraform graph" uses to reference nodes.
tfResourceByLabel := map[string]*tfjson.StateResource{}
tfResourcesByLabel := map[string][]*tfjson.StateResource{}
var findTerraformResources func(mod *tfjson.StateModule)
findTerraformResources = func(mod *tfjson.StateModule) {
for _, module := range mod.ChildModules {
@ -98,8 +98,10 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
}
for _, resource := range mod.Resources {
label := convertAddressToLabel(resource.Address)
// index by label
tfResourceByLabel[label] = resource
if tfResourcesByLabel[label] == nil {
tfResourcesByLabel[label] = []*tfjson.StateResource{}
}
tfResourcesByLabel[label] = append(tfResourcesByLabel[label], resource)
}
}
for _, module := range modules {
@ -108,212 +110,218 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
// Find all agents!
agentNames := map[string]struct{}{}
for _, tfResource := range tfResourceByLabel {
if tfResource.Type != "coder_agent" {
continue
}
var attrs agentAttributes
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode agent attributes: %w", err)
}
if _, ok := agentNames[tfResource.Name]; ok {
return nil, nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
}
agentNames[tfResource.Name] = struct{}{}
// Handling for provider pre-v0.6.10.
loginBeforeReady := true
if _, ok := tfResource.AttributeValues["login_before_ready"]; ok {
loginBeforeReady = attrs.LoginBeforeReady
}
agent := &proto.Agent{
Name: tfResource.Name,
Id: attrs.ID,
Env: attrs.Env,
StartupScript: attrs.StartupScript,
OperatingSystem: attrs.OperatingSystem,
Architecture: attrs.Architecture,
Directory: attrs.Directory,
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
TroubleshootingUrl: attrs.TroubleshootingURL,
MotdFile: attrs.MOTDFile,
LoginBeforeReady: loginBeforeReady,
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
}
switch attrs.Auth {
case "token":
agent.Auth = &proto.Agent_Token{
Token: attrs.Token,
}
default:
// If token authentication isn't specified,
// assume instance auth. It's our only other
// authentication type!
agent.Auth = &proto.Agent_InstanceId{}
}
// The label is used to find the graph node!
agentLabel := convertAddressToLabel(tfResource.Address)
var agentNode *gographviz.Node
for _, node := range graph.Nodes.Lookup {
// The node attributes surround the label with quotes.
if strings.Trim(node.Attrs["label"], `"`) != agentLabel {
for _, tfResources := range tfResourcesByLabel {
for _, tfResource := range tfResources {
if tfResource.Type != "coder_agent" {
continue
}
agentNode = node
break
}
if agentNode == nil {
return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
}
var attrs agentAttributes
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode agent attributes: %w", err)
}
if _, ok := agentNames[tfResource.Name]; ok {
return nil, nil, xerrors.Errorf("duplicate agent name: %s", tfResource.Name)
}
agentNames[tfResource.Name] = struct{}{}
// Handling for provider pre-v0.6.10.
loginBeforeReady := true
if _, ok := tfResource.AttributeValues["login_before_ready"]; ok {
loginBeforeReady = attrs.LoginBeforeReady
}
agent := &proto.Agent{
Name: tfResource.Name,
Id: attrs.ID,
Env: attrs.Env,
StartupScript: attrs.StartupScript,
OperatingSystem: attrs.OperatingSystem,
Architecture: attrs.Architecture,
Directory: attrs.Directory,
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
TroubleshootingUrl: attrs.TroubleshootingURL,
MotdFile: attrs.MOTDFile,
LoginBeforeReady: loginBeforeReady,
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
}
switch attrs.Auth {
case "token":
agent.Auth = &proto.Agent_Token{
Token: attrs.Token,
}
default:
// If token authentication isn't specified,
// assume instance auth. It's our only other
// authentication type!
agent.Auth = &proto.Agent_InstanceId{}
}
// The label is used to find the graph node!
agentLabel := convertAddressToLabel(tfResource.Address)
var agentNode *gographviz.Node
for _, node := range graph.Nodes.Lookup {
// The node attributes surround the label with quotes.
if strings.Trim(node.Attrs["label"], `"`) != agentLabel {
continue
}
agentNode = node
break
}
if agentNode == nil {
return nil, nil, xerrors.Errorf("couldn't find node on graph: %q", agentLabel)
}
var agentResource *graphResource
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, agentNode.Name, 0, true) {
if agentResource == nil {
// Default to the first resource because we have nothing to compare!
agentResource = resource
continue
}
if resource.Depth < agentResource.Depth {
// There's a closer resource!
agentResource = resource
continue
}
if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label {
agentResource = resource
continue
}
}
var agentResource *graphResource
for _, resource := range findResourcesInGraph(graph, tfResourceByLabel, agentNode.Name, 0, true) {
if agentResource == nil {
// Default to the first resource because we have nothing to compare!
agentResource = resource
continue
}
if resource.Depth < agentResource.Depth {
// There's a closer resource!
agentResource = resource
continue
}
if resource.Depth == agentResource.Depth && resource.Label < agentResource.Label {
agentResource = resource
continue
}
}
if agentResource == nil {
continue
agents, exists := resourceAgents[agentResource.Label]
if !exists {
agents = make([]*proto.Agent, 0)
}
agents = append(agents, agent)
resourceAgents[agentResource.Label] = agents
}
agents, exists := resourceAgents[agentResource.Label]
if !exists {
agents = make([]*proto.Agent, 0)
}
agents = append(agents, agent)
resourceAgents[agentResource.Label] = agents
}
// Manually associate agents with instance IDs.
for _, resource := range tfResourceByLabel {
if resource.Type != "coder_agent_instance" {
continue
}
agentIDRaw, valid := resource.AttributeValues["agent_id"]
if !valid {
continue
}
agentID, valid := agentIDRaw.(string)
if !valid {
continue
}
instanceIDRaw, valid := resource.AttributeValues["instance_id"]
if !valid {
continue
}
instanceID, valid := instanceIDRaw.(string)
if !valid {
continue
}
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_agent_instance" {
continue
}
agentIDRaw, valid := resource.AttributeValues["agent_id"]
if !valid {
continue
}
agentID, valid := agentIDRaw.(string)
if !valid {
continue
}
instanceIDRaw, valid := resource.AttributeValues["instance_id"]
if !valid {
continue
}
instanceID, valid := instanceIDRaw.(string)
if !valid {
continue
}
for _, agents := range resourceAgents {
for _, agent := range agents {
if agent.Id != agentID {
continue
for _, agents := range resourceAgents {
for _, agent := range agents {
if agent.Id != agentID {
continue
}
// Only apply the instance ID if the agent authentication
// type is set to do so. A user ran into a bug where they
// had the instance ID block, but auth was set to "token". See:
// https://github.com/coder/coder/issues/4551#issuecomment-1336293468
switch t := agent.Auth.(type) {
case *proto.Agent_Token:
continue
case *proto.Agent_InstanceId:
t.InstanceId = instanceID
}
break
}
// Only apply the instance ID if the agent authentication
// type is set to do so. A user ran into a bug where they
// had the instance ID block, but auth was set to "token". See:
// https://github.com/coder/coder/issues/4551#issuecomment-1336293468
switch t := agent.Auth.(type) {
case *proto.Agent_Token:
continue
case *proto.Agent_InstanceId:
t.InstanceId = instanceID
}
break
}
}
}
// Associate Apps with agents.
appSlugs := make(map[string]struct{})
for _, resource := range tfResourceByLabel {
if resource.Type != "coder_app" {
continue
}
var attrs agentAppAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode app attributes: %w", err)
}
// Default to the resource name if none is set!
if attrs.Slug == "" {
attrs.Slug = resource.Name
}
if attrs.DisplayName == "" {
if attrs.Name != "" {
// Name is deprecated but still accepted.
attrs.DisplayName = attrs.Name
} else {
attrs.DisplayName = attrs.Slug
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_app" {
continue
}
}
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
return nil, nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
}
if _, exists := appSlugs[attrs.Slug]; exists {
return nil, nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
}
appSlugs[attrs.Slug] = struct{}{}
var healthcheck *proto.Healthcheck
if len(attrs.Healthcheck) != 0 {
healthcheck = &proto.Healthcheck{
Url: attrs.Healthcheck[0].URL,
Interval: attrs.Healthcheck[0].Interval,
Threshold: attrs.Healthcheck[0].Threshold,
var attrs agentAppAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode app attributes: %w", err)
}
}
sharingLevel := proto.AppSharingLevel_OWNER
switch strings.ToLower(attrs.Share) {
case "owner":
sharingLevel = proto.AppSharingLevel_OWNER
case "authenticated":
sharingLevel = proto.AppSharingLevel_AUTHENTICATED
case "public":
sharingLevel = proto.AppSharingLevel_PUBLIC
}
for _, agents := range resourceAgents {
for _, agent := range agents {
// Find agents with the matching ID and associate them!
if agent.Id != attrs.AgentID {
continue
// Default to the resource name if none is set!
if attrs.Slug == "" {
attrs.Slug = resource.Name
}
if attrs.DisplayName == "" {
if attrs.Name != "" {
// Name is deprecated but still accepted.
attrs.DisplayName = attrs.Name
} else {
attrs.DisplayName = attrs.Slug
}
}
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
return nil, nil, xerrors.Errorf("invalid app slug %q, please update your coder/coder provider to the latest version and specify the slug property on each coder_app", attrs.Slug)
}
if _, exists := appSlugs[attrs.Slug]; exists {
return nil, nil, xerrors.Errorf("duplicate app slug, they must be unique per template: %q", attrs.Slug)
}
appSlugs[attrs.Slug] = struct{}{}
var healthcheck *proto.Healthcheck
if len(attrs.Healthcheck) != 0 {
healthcheck = &proto.Healthcheck{
Url: attrs.Healthcheck[0].URL,
Interval: attrs.Healthcheck[0].Interval,
Threshold: attrs.Healthcheck[0].Threshold,
}
}
sharingLevel := proto.AppSharingLevel_OWNER
switch strings.ToLower(attrs.Share) {
case "owner":
sharingLevel = proto.AppSharingLevel_OWNER
case "authenticated":
sharingLevel = proto.AppSharingLevel_AUTHENTICATED
case "public":
sharingLevel = proto.AppSharingLevel_PUBLIC
}
for _, agents := range resourceAgents {
for _, agent := range agents {
// Find agents with the matching ID and associate them!
if agent.Id != attrs.AgentID {
continue
}
agent.Apps = append(agent.Apps, &proto.App{
Slug: attrs.Slug,
DisplayName: attrs.DisplayName,
Command: attrs.Command,
External: attrs.External,
Url: attrs.URL,
Icon: attrs.Icon,
Subdomain: attrs.Subdomain,
SharingLevel: sharingLevel,
Healthcheck: healthcheck,
})
}
agent.Apps = append(agent.Apps, &proto.App{
Slug: attrs.Slug,
DisplayName: attrs.DisplayName,
Command: attrs.Command,
External: attrs.External,
Url: attrs.URL,
Icon: attrs.Icon,
Subdomain: attrs.Subdomain,
SharingLevel: sharingLevel,
Healthcheck: healthcheck,
})
}
}
}
@ -324,130 +332,136 @@ func ConvertResourcesAndParameters(modules []*tfjson.StateModule, rawGraph strin
resourceIcon := map[string]string{}
resourceCost := map[string]int32{}
for _, resource := range tfResourceByLabel {
if resource.Type != "coder_metadata" {
continue
}
var attrs metadataAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode metadata attributes: %w", err)
}
resourceLabel := convertAddressToLabel(resource.Address)
var attachedNode *gographviz.Node
for _, node := range graph.Nodes.Lookup {
// The node attributes surround the label with quotes.
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_metadata" {
continue
}
attachedNode = node
break
}
if attachedNode == nil {
continue
}
var attachedResource *graphResource
for _, resource := range findResourcesInGraph(graph, tfResourceByLabel, attachedNode.Name, 0, false) {
var attrs metadataAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, nil, xerrors.Errorf("decode metadata attributes: %w", err)
}
resourceLabel := convertAddressToLabel(resource.Address)
var attachedNode *gographviz.Node
for _, node := range graph.Nodes.Lookup {
// The node attributes surround the label with quotes.
if strings.Trim(node.Attrs["label"], `"`) != resourceLabel {
continue
}
attachedNode = node
break
}
if attachedNode == nil {
continue
}
var attachedResource *graphResource
for _, resource := range findResourcesInGraph(graph, tfResourcesByLabel, attachedNode.Name, 0, false) {
if attachedResource == nil {
// Default to the first resource because we have nothing to compare!
attachedResource = resource
continue
}
if resource.Depth < attachedResource.Depth {
// There's a closer resource!
attachedResource = resource
continue
}
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
attachedResource = resource
continue
}
}
if attachedResource == nil {
// Default to the first resource because we have nothing to compare!
attachedResource = resource
continue
}
if resource.Depth < attachedResource.Depth {
// There's a closer resource!
attachedResource = resource
continue
}
if resource.Depth == attachedResource.Depth && resource.Label < attachedResource.Label {
attachedResource = resource
continue
}
}
if attachedResource == nil {
continue
}
targetLabel := attachedResource.Label
targetLabel := attachedResource.Label
resourceHidden[targetLabel] = attrs.Hide
resourceIcon[targetLabel] = attrs.Icon
resourceCost[targetLabel] = attrs.DailyCost
for _, item := range attrs.Items {
resourceMetadata[targetLabel] = append(resourceMetadata[targetLabel],
&proto.Resource_Metadata{
Key: item.Key,
Value: item.Value,
Sensitive: item.Sensitive,
IsNull: item.IsNull,
})
resourceHidden[targetLabel] = attrs.Hide
resourceIcon[targetLabel] = attrs.Icon
resourceCost[targetLabel] = attrs.DailyCost
for _, item := range attrs.Items {
resourceMetadata[targetLabel] = append(resourceMetadata[targetLabel],
&proto.Resource_Metadata{
Key: item.Key,
Value: item.Value,
Sensitive: item.Sensitive,
IsNull: item.IsNull,
})
}
}
}
for _, resource := range tfResourceByLabel {
if resource.Mode == tfjson.DataResourceMode {
continue
}
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
continue
}
label := convertAddressToLabel(resource.Address)
for _, tfResources := range tfResourcesByLabel {
for _, resource := range tfResources {
if resource.Mode == tfjson.DataResourceMode {
continue
}
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
continue
}
label := convertAddressToLabel(resource.Address)
agents, exists := resourceAgents[label]
if exists {
applyAutomaticInstanceID(resource, agents)
}
agents, exists := resourceAgents[label]
if exists {
applyAutomaticInstanceID(resource, agents)
}
resources = append(resources, &proto.Resource{
Name: resource.Name,
Type: resource.Type,
Agents: agents,
Metadata: resourceMetadata[label],
Hide: resourceHidden[label],
Icon: resourceIcon[label],
DailyCost: resourceCost[label],
InstanceType: applyInstanceType(resource),
})
resources = append(resources, &proto.Resource{
Name: resource.Name,
Type: resource.Type,
Agents: agents,
Metadata: resourceMetadata[label],
Hide: resourceHidden[label],
Icon: resourceIcon[label],
DailyCost: resourceCost[label],
InstanceType: applyInstanceType(resource),
})
}
}
parameters := make([]*proto.RichParameter, 0)
for _, resource := range tfResourceByLabel {
if resource.Type != "coder_parameter" {
continue
}
var param provider.Parameter
err = mapstructure.Decode(resource.AttributeValues, &param)
if err != nil {
return nil, nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
}
protoParam := &proto.RichParameter{
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.Default,
Icon: param.Icon,
}
if len(param.Validation) == 1 {
protoParam.ValidationRegex = param.Validation[0].Regex
protoParam.ValidationError = param.Validation[0].Error
protoParam.ValidationMax = int32(param.Validation[0].Max)
protoParam.ValidationMin = int32(param.Validation[0].Min)
protoParam.ValidationMonotonic = param.Validation[0].Monotonic
}
if len(param.Option) > 0 {
protoParam.Options = make([]*proto.RichParameterOption, 0, len(param.Option))
for _, option := range param.Option {
protoParam.Options = append(protoParam.Options, &proto.RichParameterOption{
Name: option.Name,
Description: option.Description,
Value: option.Value,
Icon: option.Icon,
})
for _, tfResources := range tfResourcesByLabel {
for _, resource := range tfResources {
if resource.Type != "coder_parameter" {
continue
}
var param provider.Parameter
err = mapstructure.Decode(resource.AttributeValues, &param)
if err != nil {
return nil, nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
}
protoParam := &proto.RichParameter{
Name: param.Name,
Description: param.Description,
Type: param.Type,
Mutable: param.Mutable,
DefaultValue: param.Default,
Icon: param.Icon,
}
if len(param.Validation) == 1 {
protoParam.ValidationRegex = param.Validation[0].Regex
protoParam.ValidationError = param.Validation[0].Error
protoParam.ValidationMax = int32(param.Validation[0].Max)
protoParam.ValidationMin = int32(param.Validation[0].Min)
protoParam.ValidationMonotonic = param.Validation[0].Monotonic
}
if len(param.Option) > 0 {
protoParam.Options = make([]*proto.RichParameterOption, 0, len(param.Option))
for _, option := range param.Option {
protoParam.Options = append(protoParam.Options, &proto.RichParameterOption{
Name: option.Name,
Description: option.Description,
Value: option.Value,
Icon: option.Icon,
})
}
}
parameters = append(parameters, protoParam)
}
parameters = append(parameters, protoParam)
}
return resources, parameters, nil
@ -536,7 +550,7 @@ func applyAutomaticInstanceID(resource *tfjson.StateResource, agents []*proto.Ag
// findResourcesInGraph traverses directionally in a graph until a resource is found,
// then it stores the depth it was found at, and continues working up the tree.
// nolint:revive
func findResourcesInGraph(graph *gographviz.Graph, tfResourceByLabel map[string]*tfjson.StateResource, nodeName string, currentDepth uint, up bool) []*graphResource {
func findResourcesInGraph(graph *gographviz.Graph, tfResourcesByLabel map[string][]*tfjson.StateResource, nodeName string, currentDepth uint, up bool) []*graphResource {
graphResources := make([]*graphResource, 0)
mapping := graph.Edges.DstToSrcs
if !up {
@ -545,29 +559,31 @@ func findResourcesInGraph(graph *gographviz.Graph, tfResourceByLabel map[string]
for destination := range mapping[nodeName] {
destinationNode := graph.Nodes.Lookup[destination]
// Work our way up the tree!
graphResources = append(graphResources, findResourcesInGraph(graph, tfResourceByLabel, destinationNode.Name, currentDepth+1, up)...)
graphResources = append(graphResources, findResourcesInGraph(graph, tfResourcesByLabel, destinationNode.Name, currentDepth+1, up)...)
destinationLabel, exists := destinationNode.Attrs["label"]
if !exists {
continue
}
destinationLabel = strings.Trim(destinationLabel, `"`)
resource, exists := tfResourceByLabel[destinationLabel]
resources, exists := tfResourcesByLabel[destinationLabel]
if !exists {
continue
}
// Data sources cannot be associated with agents for now!
if resource.Mode != tfjson.ManagedResourceMode {
continue
for _, resource := range resources {
// Data sources cannot be associated with agents for now!
if resource.Mode != tfjson.ManagedResourceMode {
continue
}
// Don't associate Coder resources with other Coder resources!
if strings.HasPrefix(resource.Type, "coder_") {
continue
}
graphResources = append(graphResources, &graphResource{
Label: destinationLabel,
Depth: currentDepth,
})
}
// Don't associate Coder resources with other Coder resources!
if strings.HasPrefix(resource.Type, "coder_") {
continue
}
graphResources = append(graphResources, &graphResource{
Label: destinationLabel,
Depth: currentDepth,
})
}
return graphResources

View File

@ -170,6 +170,30 @@ func TestConvertResources(t *testing.T) {
}},
}},
},
"mapped-apps": {
resources: []*proto.Resource{{
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "linux",
Architecture: "amd64",
Apps: []*proto.App{
{
Slug: "app1",
DisplayName: "app1",
},
{
Slug: "app2",
DisplayName: "app2",
},
},
Auth: &proto.Agent_Token{},
LoginBeforeReady: true,
ConnectionTimeoutSeconds: 120,
}},
}},
},
// Tests fetching metadata about workspace resources.
"resource-metadata": {
resources: []*proto.Resource{{

View File

@ -0,0 +1,38 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "0.6.1"
}
}
}
resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
}
locals {
apps_map = {
"app1" = {
name = "app1"
}
"app2" = {
name = "app2"
}
}
}
resource "coder_app" "apps" {
for_each = local.apps_map
agent_id = coder_agent.dev.id
slug = each.key
display_name = each.value.name
}
resource "null_resource" "dev" {
depends_on = [
coder_agent.dev
]
}

View File

@ -0,0 +1,21 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"]
"[root] coder_app.apps (expand)" [label = "coder_app.apps", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] coder_app.apps (expand)" -> "[root] coder_agent.dev (expand)"
"[root] coder_app.apps (expand)" -> "[root] local.apps_map (expand)"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.apps (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}

View File

@ -0,0 +1,298 @@
{
"format_version": "1.1",
"terraform_version": "1.3.7",
"planned_values": {
"root_module": {
"resources": [
{
"address": "coder_agent.dev",
"mode": "managed",
"type": "coder_agent",
"name": "dev",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"os": "linux",
"startup_script": null,
"troubleshooting_url": null
},
"sensitive_values": {}
},
{
"address": "coder_app.apps[\"app1\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app1",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"command": null,
"display_name": "app1",
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
"sensitive_values": {
"healthcheck": []
}
},
{
"address": "coder_app.apps[\"app2\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app2",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"command": null,
"display_name": "app2",
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": null,
"url": null
},
"sensitive_values": {
"healthcheck": []
}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"triggers": null
},
"sensitive_values": {}
}
]
}
},
"resource_changes": [
{
"address": "coder_agent.dev",
"mode": "managed",
"type": "coder_agent",
"name": "dev",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"os": "linux",
"startup_script": null,
"troubleshooting_url": null
},
"after_unknown": {
"id": true,
"init_script": true,
"token": true
},
"before_sensitive": false,
"after_sensitive": {
"token": true
}
}
},
{
"address": "coder_app.apps[\"app1\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app1",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"command": null,
"display_name": "app1",
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
"after_unknown": {
"agent_id": true,
"healthcheck": [],
"id": true
},
"before_sensitive": false,
"after_sensitive": {
"healthcheck": []
}
}
},
{
"address": "coder_app.apps[\"app2\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app2",
"provider_name": "registry.terraform.io/coder/coder",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"command": null,
"display_name": "app2",
"healthcheck": [],
"icon": null,
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": null,
"url": null
},
"after_unknown": {
"agent_id": true,
"healthcheck": [],
"id": true
},
"before_sensitive": false,
"after_sensitive": {
"healthcheck": []
}
}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"triggers": null
},
"after_unknown": {
"id": true
},
"before_sensitive": false,
"after_sensitive": {}
}
}
],
"configuration": {
"provider_config": {
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "0.6.1"
},
"null": {
"name": "null",
"full_name": "registry.terraform.io/hashicorp/null"
}
},
"root_module": {
"resources": [
{
"address": "coder_agent.dev",
"mode": "managed",
"type": "coder_agent",
"name": "dev",
"provider_config_key": "coder",
"expressions": {
"arch": {
"constant_value": "amd64"
},
"os": {
"constant_value": "linux"
}
},
"schema_version": 0
},
{
"address": "coder_app.apps",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"provider_config_key": "coder",
"expressions": {
"agent_id": {
"references": [
"coder_agent.dev.id",
"coder_agent.dev"
]
},
"display_name": {
"references": [
"each.value.name",
"each.value"
]
},
"slug": {
"references": [
"each.key"
]
}
},
"schema_version": 0,
"for_each_expression": {
"references": [
"local.apps_map"
]
}
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_config_key": "null",
"schema_version": 0,
"depends_on": [
"coder_agent.dev"
]
}
]
}
},
"relevant_attributes": [
{
"resource": "coder_agent.dev",
"attribute": [
"id"
]
}
]
}

View File

@ -0,0 +1,21 @@
digraph {
compound = "true"
newrank = "true"
subgraph "root" {
"[root] coder_agent.dev (expand)" [label = "coder_agent.dev", shape = "box"]
"[root] coder_app.apps (expand)" [label = "coder_app.apps", shape = "box"]
"[root] null_resource.dev (expand)" [label = "null_resource.dev", shape = "box"]
"[root] provider[\"registry.terraform.io/coder/coder\"]" [label = "provider[\"registry.terraform.io/coder/coder\"]", shape = "diamond"]
"[root] provider[\"registry.terraform.io/hashicorp/null\"]" [label = "provider[\"registry.terraform.io/hashicorp/null\"]", shape = "diamond"]
"[root] coder_agent.dev (expand)" -> "[root] provider[\"registry.terraform.io/coder/coder\"]"
"[root] coder_app.apps (expand)" -> "[root] coder_agent.dev (expand)"
"[root] coder_app.apps (expand)" -> "[root] local.apps_map (expand)"
"[root] null_resource.dev (expand)" -> "[root] coder_agent.dev (expand)"
"[root] null_resource.dev (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"]"
"[root] provider[\"registry.terraform.io/coder/coder\"] (close)" -> "[root] coder_app.apps (expand)"
"[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)" -> "[root] null_resource.dev (expand)"
"[root] root" -> "[root] provider[\"registry.terraform.io/coder/coder\"] (close)"
"[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/null\"] (close)"
}
}

View File

@ -0,0 +1,106 @@
{
"format_version": "1.0",
"terraform_version": "1.3.7",
"values": {
"root_module": {
"resources": [
{
"address": "coder_agent.dev",
"mode": "managed",
"type": "coder_agent",
"name": "dev",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"arch": "amd64",
"auth": "token",
"connection_timeout": 120,
"dir": null,
"env": null,
"id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
"init_script": "",
"os": "linux",
"startup_script": null,
"token": "47b62747-82ad-4792-960a-e120a88d2bac",
"troubleshooting_url": null
},
"sensitive_values": {}
},
{
"address": "coder_app.apps[\"app1\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app1",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
"command": null,
"display_name": "app1",
"healthcheck": [],
"icon": null,
"id": "75b7232a-320f-462f-91c0-5542e655dce9",
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app1",
"subdomain": null,
"url": null
},
"sensitive_values": {
"healthcheck": []
},
"depends_on": [
"coder_agent.dev"
]
},
{
"address": "coder_app.apps[\"app2\"]",
"mode": "managed",
"type": "coder_app",
"name": "apps",
"index": "app2",
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"agent_id": "9607dca1-bb3e-4606-849c-69d073b3b7d1",
"command": null,
"display_name": "app2",
"healthcheck": [],
"icon": null,
"id": "30e8a682-a440-48a8-847d-525baa05783f",
"name": null,
"relative_path": null,
"share": "owner",
"slug": "app2",
"subdomain": null,
"url": null
},
"sensitive_values": {
"healthcheck": []
},
"depends_on": [
"coder_agent.dev"
]
},
{
"address": "null_resource.dev",
"mode": "managed",
"type": "null_resource",
"name": "dev",
"provider_name": "registry.terraform.io/hashicorp/null",
"schema_version": 0,
"values": {
"id": "6276937382685771643",
"triggers": null
},
"sensitive_values": {},
"depends_on": [
"coder_agent.dev"
]
}
]
}
}
}