mirror of https://github.com/coder/coder.git
699 lines
22 KiB
Go
699 lines
22 KiB
Go
package terraform
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/awalterschulze/gographviz"
|
|
tfjson "github.com/hashicorp/terraform-json"
|
|
"github.com/mitchellh/mapstructure"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/terraform-provider-coder/provider"
|
|
|
|
"github.com/coder/coder/provisioner"
|
|
"github.com/coder/coder/provisionersdk/proto"
|
|
)
|
|
|
|
type agentMetadata struct {
|
|
Key string `mapstructure:"key"`
|
|
DisplayName string `mapstructure:"display_name"`
|
|
Script string `mapstructure:"script"`
|
|
Interval int64 `mapstructure:"interval"`
|
|
Timeout int64 `mapstructure:"timeout"`
|
|
}
|
|
|
|
// A mapping of attributes on the "coder_agent" resource.
|
|
type agentAttributes struct {
|
|
Auth string `mapstructure:"auth"`
|
|
OperatingSystem string `mapstructure:"os"`
|
|
Architecture string `mapstructure:"arch"`
|
|
Directory string `mapstructure:"dir"`
|
|
ID string `mapstructure:"id"`
|
|
Token string `mapstructure:"token"`
|
|
Env map[string]string `mapstructure:"env"`
|
|
StartupScript string `mapstructure:"startup_script"`
|
|
ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"`
|
|
TroubleshootingURL string `mapstructure:"troubleshooting_url"`
|
|
MOTDFile string `mapstructure:"motd_file"`
|
|
LoginBeforeReady bool `mapstructure:"login_before_ready"`
|
|
StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"`
|
|
ShutdownScript string `mapstructure:"shutdown_script"`
|
|
ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"`
|
|
Metadata []agentMetadata `mapstructure:"metadata"`
|
|
}
|
|
|
|
// A mapping of attributes on the "coder_app" resource.
|
|
type agentAppAttributes struct {
|
|
AgentID string `mapstructure:"agent_id"`
|
|
// Slug is required in terraform, but to avoid breaking existing users we
|
|
// will default to the resource name if it is not specified.
|
|
Slug string `mapstructure:"slug"`
|
|
DisplayName string `mapstructure:"display_name"`
|
|
// Name is deprecated in favor of DisplayName.
|
|
Name string `mapstructure:"name"`
|
|
Icon string `mapstructure:"icon"`
|
|
URL string `mapstructure:"url"`
|
|
External bool `mapstructure:"external"`
|
|
Command string `mapstructure:"command"`
|
|
Share string `mapstructure:"share"`
|
|
Subdomain bool `mapstructure:"subdomain"`
|
|
Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"`
|
|
}
|
|
|
|
// A mapping of attributes on the "healthcheck" resource.
|
|
type appHealthcheckAttributes struct {
|
|
URL string `mapstructure:"url"`
|
|
Interval int32 `mapstructure:"interval"`
|
|
Threshold int32 `mapstructure:"threshold"`
|
|
}
|
|
|
|
// A mapping of attributes on the "coder_metadata" resource.
|
|
type resourceMetadataAttributes struct {
|
|
ResourceID string `mapstructure:"resource_id"`
|
|
Hide bool `mapstructure:"hide"`
|
|
Icon string `mapstructure:"icon"`
|
|
DailyCost int32 `mapstructure:"daily_cost"`
|
|
Items []resourceMetadataItem `mapstructure:"item"`
|
|
}
|
|
|
|
type resourceMetadataItem struct {
|
|
Key string `mapstructure:"key"`
|
|
Value string `mapstructure:"value"`
|
|
Sensitive bool `mapstructure:"sensitive"`
|
|
IsNull bool `mapstructure:"is_null"`
|
|
}
|
|
|
|
type State struct {
|
|
Resources []*proto.Resource
|
|
Parameters []*proto.RichParameter
|
|
GitAuthProviders []string
|
|
}
|
|
|
|
// ConvertState consumes Terraform state and a GraphViz representation
|
|
// produced by `terraform graph` to produce resources consumable by Coder.
|
|
// nolint:gocyclo
|
|
func ConvertState(modules []*tfjson.StateModule, rawGraph string, rawParameterNames []string) (*State, error) {
|
|
parsedGraph, err := gographviz.ParseString(rawGraph)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("parse graph: %w", err)
|
|
}
|
|
graph, err := gographviz.NewAnalysedGraph(parsedGraph)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("analyze graph: %w", err)
|
|
}
|
|
|
|
resources := make([]*proto.Resource, 0)
|
|
resourceAgents := map[string][]*proto.Agent{}
|
|
|
|
// Indexes Terraform resources by their label.
|
|
// The label is what "terraform graph" uses to reference nodes.
|
|
tfResourcesByLabel := map[string]map[string]*tfjson.StateResource{}
|
|
|
|
// Extra array to preserve the order of rich parameters.
|
|
tfResourcesRichParameters := make([]*tfjson.StateResource, 0)
|
|
|
|
var findTerraformResources func(mod *tfjson.StateModule)
|
|
findTerraformResources = func(mod *tfjson.StateModule) {
|
|
for _, module := range mod.ChildModules {
|
|
findTerraformResources(module)
|
|
}
|
|
for _, resource := range mod.Resources {
|
|
if resource.Type == "coder_parameter" {
|
|
tfResourcesRichParameters = append(tfResourcesRichParameters, resource)
|
|
}
|
|
|
|
label := convertAddressToLabel(resource.Address)
|
|
if tfResourcesByLabel[label] == nil {
|
|
tfResourcesByLabel[label] = map[string]*tfjson.StateResource{}
|
|
}
|
|
tfResourcesByLabel[label][resource.Address] = resource
|
|
}
|
|
}
|
|
for _, module := range modules {
|
|
findTerraformResources(module)
|
|
}
|
|
|
|
// Find all agents!
|
|
agentNames := map[string]struct{}{}
|
|
for _, tfResources := range tfResourcesByLabel {
|
|
for _, tfResource := range tfResources {
|
|
if tfResource.Type != "coder_agent" {
|
|
continue
|
|
}
|
|
var attrs agentAttributes
|
|
err = mapstructure.Decode(tfResource.AttributeValues, &attrs)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("decode agent attributes: %w", err)
|
|
}
|
|
|
|
if _, ok := agentNames[tfResource.Name]; ok {
|
|
return 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
|
|
}
|
|
|
|
var metadata []*proto.Agent_Metadata
|
|
for _, item := range attrs.Metadata {
|
|
metadata = append(metadata, &proto.Agent_Metadata{
|
|
Key: item.Key,
|
|
DisplayName: item.DisplayName,
|
|
Script: item.Script,
|
|
Interval: item.Interval,
|
|
Timeout: item.Timeout,
|
|
})
|
|
}
|
|
|
|
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,
|
|
ShutdownScript: attrs.ShutdownScript,
|
|
ShutdownScriptTimeoutSeconds: attrs.ShutdownScriptTimeoutSeconds,
|
|
Metadata: metadata,
|
|
}
|
|
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, 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
|
|
}
|
|
}
|
|
|
|
if agentResource == nil {
|
|
continue
|
|
}
|
|
|
|
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 _, 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
|
|
}
|
|
// 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 _, resources := range tfResourcesByLabel {
|
|
for _, resource := range resources {
|
|
if resource.Type != "coder_app" {
|
|
continue
|
|
}
|
|
|
|
var attrs agentAppAttributes
|
|
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
|
if err != nil {
|
|
return 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
|
|
}
|
|
}
|
|
|
|
if !provisioner.AppSlugRegex.MatchString(attrs.Slug) {
|
|
return 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, 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,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Associate metadata blocks with resources.
|
|
resourceMetadata := map[string][]*proto.Resource_Metadata{}
|
|
resourceHidden := map[string]bool{}
|
|
resourceIcon := map[string]string{}
|
|
resourceCost := map[string]int32{}
|
|
|
|
for _, resources := range tfResourcesByLabel {
|
|
for _, resource := range resources {
|
|
if resource.Type != "coder_metadata" {
|
|
continue
|
|
}
|
|
|
|
var attrs resourceMetadataAttributes
|
|
err = mapstructure.Decode(resource.AttributeValues, &attrs)
|
|
if err != nil {
|
|
return 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 {
|
|
continue
|
|
}
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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 orderedRichParametersResources(tfResourcesRichParameters, rawParameterNames) {
|
|
var param provider.Parameter
|
|
err = mapstructure.Decode(resource.AttributeValues, ¶m)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("decode map values for coder_parameter.%s: %w", resource.Name, err)
|
|
}
|
|
protoParam := &proto.RichParameter{
|
|
Name: param.Name,
|
|
DisplayName: param.DisplayName,
|
|
Description: param.Description,
|
|
Type: param.Type,
|
|
Mutable: param.Mutable,
|
|
DefaultValue: param.Default,
|
|
Icon: param.Icon,
|
|
Required: !param.Optional,
|
|
LegacyVariableName: param.LegacyVariableName,
|
|
}
|
|
if len(param.Validation) == 1 {
|
|
protoParam.ValidationRegex = param.Validation[0].Regex
|
|
protoParam.ValidationError = param.Validation[0].Error
|
|
|
|
validationAttributeValues, ok := resource.AttributeValues["validation"]
|
|
if ok {
|
|
validationAttributeValuesArr, ok := validationAttributeValues.([]interface{})
|
|
if ok {
|
|
validationAttributeValuesMapStr, ok := validationAttributeValuesArr[0].(map[string]interface{})
|
|
if ok {
|
|
// Backward compatibility with terraform-coder-plugin < v0.8.2:
|
|
// * "min_disabled" and "max_disabled" are not available yet
|
|
// * "min" and "max" are required to be specified together
|
|
if _, ok = validationAttributeValuesMapStr["min_disabled"]; !ok {
|
|
if param.Validation[0].Min != 0 || param.Validation[0].Max != 0 {
|
|
param.Validation[0].MinDisabled = false
|
|
param.Validation[0].MaxDisabled = false
|
|
} else {
|
|
param.Validation[0].MinDisabled = true
|
|
param.Validation[0].MaxDisabled = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !param.Validation[0].MaxDisabled {
|
|
protoParam.ValidationMax = PtrInt32(param.Validation[0].Max)
|
|
}
|
|
if !param.Validation[0].MinDisabled {
|
|
protoParam.ValidationMin = PtrInt32(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)
|
|
}
|
|
|
|
// A map is used to ensure we don't have duplicates!
|
|
gitAuthProvidersMap := map[string]struct{}{}
|
|
for _, tfResources := range tfResourcesByLabel {
|
|
for _, resource := range tfResources {
|
|
if resource.Type != "coder_git_auth" {
|
|
continue
|
|
}
|
|
id, ok := resource.AttributeValues["id"].(string)
|
|
if !ok {
|
|
return nil, xerrors.Errorf("git auth id is not a string")
|
|
}
|
|
gitAuthProvidersMap[id] = struct{}{}
|
|
}
|
|
}
|
|
gitAuthProviders := make([]string, 0, len(gitAuthProvidersMap))
|
|
for id := range gitAuthProvidersMap {
|
|
gitAuthProviders = append(gitAuthProviders, id)
|
|
}
|
|
|
|
return &State{
|
|
Resources: resources,
|
|
Parameters: parameters,
|
|
GitAuthProviders: gitAuthProviders,
|
|
}, nil
|
|
}
|
|
|
|
func PtrInt32(number int) *int32 {
|
|
n := int32(number)
|
|
return &n
|
|
}
|
|
|
|
// convertAddressToLabel returns the Terraform address without the count
|
|
// specifier.
|
|
// eg. "module.ec2_dev.ec2_instance.dev[0]" becomes "module.ec2_dev.ec2_instance.dev"
|
|
func convertAddressToLabel(address string) string {
|
|
cut, _, _ := strings.Cut(address, "[")
|
|
return cut
|
|
}
|
|
|
|
type graphResource struct {
|
|
Label string
|
|
Depth uint
|
|
}
|
|
|
|
// applyInstanceType sets the instance type on an agent if it matches
|
|
// one of the special resource types that we track.
|
|
func applyInstanceType(resource *tfjson.StateResource) string {
|
|
key, isValid := map[string]string{
|
|
"google_compute_instance": "machine_type",
|
|
"aws_instance": "instance_type",
|
|
"aws_spot_instance_request": "instance_type",
|
|
"azurerm_linux_virtual_machine": "size",
|
|
"azurerm_windows_virtual_machine": "size",
|
|
}[resource.Type]
|
|
if !isValid {
|
|
return ""
|
|
}
|
|
|
|
instanceTypeRaw, isValid := resource.AttributeValues[key]
|
|
if !isValid {
|
|
return ""
|
|
}
|
|
instanceType, isValid := instanceTypeRaw.(string)
|
|
if !isValid {
|
|
return ""
|
|
}
|
|
return instanceType
|
|
}
|
|
|
|
// applyAutomaticInstanceID checks if the resource is one of a set of *magical* IDs
|
|
// that automatically index their identifier for automatic authentication.
|
|
func applyAutomaticInstanceID(resource *tfjson.StateResource, agents []*proto.Agent) {
|
|
// These resource types are for automatically associating an instance ID
|
|
// with an agent for authentication.
|
|
key, isValid := map[string]string{
|
|
"google_compute_instance": "instance_id",
|
|
"aws_instance": "id",
|
|
"aws_spot_instance_request": "spot_instance_id",
|
|
"azurerm_linux_virtual_machine": "virtual_machine_id",
|
|
"azurerm_windows_virtual_machine": "virtual_machine_id",
|
|
}[resource.Type]
|
|
if !isValid {
|
|
return
|
|
}
|
|
|
|
// The resource type doesn't support
|
|
// automatically setting the instance ID.
|
|
instanceIDRaw, isValid := resource.AttributeValues[key]
|
|
if !isValid {
|
|
return
|
|
}
|
|
instanceID, isValid := instanceIDRaw.(string)
|
|
if !isValid {
|
|
return
|
|
}
|
|
for _, agent := range agents {
|
|
// Didn't use instance identity.
|
|
if agent.GetToken() != "" {
|
|
continue
|
|
}
|
|
if agent.GetInstanceId() != "" {
|
|
// If an instance ID is manually specified, do not override!
|
|
continue
|
|
}
|
|
|
|
agent.Auth = &proto.Agent_InstanceId{
|
|
InstanceId: instanceID,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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, tfResourcesByLabel map[string]map[string]*tfjson.StateResource, nodeName string, currentDepth uint, up bool) []*graphResource {
|
|
graphResources := make([]*graphResource, 0)
|
|
mapping := graph.Edges.DstToSrcs
|
|
if !up {
|
|
mapping = graph.Edges.SrcToDsts
|
|
}
|
|
for destination := range mapping[nodeName] {
|
|
destinationNode := graph.Nodes.Lookup[destination]
|
|
// Work our way up the tree!
|
|
graphResources = append(graphResources, findResourcesInGraph(graph, tfResourcesByLabel, destinationNode.Name, currentDepth+1, up)...)
|
|
|
|
destinationLabel, exists := destinationNode.Attrs["label"]
|
|
if !exists {
|
|
continue
|
|
}
|
|
destinationLabel = strings.Trim(destinationLabel, `"`)
|
|
resources, exists := tfResourcesByLabel[destinationLabel]
|
|
if !exists {
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
return graphResources
|
|
}
|
|
|
|
func orderedRichParametersResources(tfResourcesRichParameters []*tfjson.StateResource, orderedNames []string) []*tfjson.StateResource {
|
|
if len(orderedNames) == 0 {
|
|
return tfResourcesRichParameters
|
|
}
|
|
|
|
ordered := make([]*tfjson.StateResource, len(orderedNames))
|
|
for i, name := range orderedNames {
|
|
for _, resource := range tfResourcesRichParameters {
|
|
if resource.Name == name {
|
|
ordered[i] = resource
|
|
}
|
|
}
|
|
}
|
|
return ordered
|
|
}
|