diff --git a/provisioner/terraform/convertstate.go b/provisioner/terraform/convertstate.go new file mode 100644 index 0000000000000..3026fbe20cd01 --- /dev/null +++ b/provisioner/terraform/convertstate.go @@ -0,0 +1,1041 @@ +package terraform + +import ( + "context" + "fmt" + "math" + "strings" + + "github.com/awalterschulze/gographviz" + "github.com/google/uuid" + tfjson "github.com/hashicorp/terraform-json" + "github.com/mitchellh/mapstructure" + "golang.org/x/xerrors" + + "cdr.dev/slog" + + "github.com/coder/terraform-provider-coder/v2/provider" + + "github.com/coder/coder/v2/coderd/util/slice" + stringutil "github.com/coder/coder/v2/coderd/util/strings" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisioner" + "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/coder/v2/provisionersdk/proto" +) + +// ConvertState consumes Terraform state and a GraphViz representation +// produced by `terraform graph` to produce resources consumable by Coder. +func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph string, logger slog.Logger) (*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) + } + + converter := newConverter(ctx, logger, graph) + + // Index all resources from the state. + converter.indexTerraformResources(modules) + + // Process agents first, as they are referenced by other resources. + if err := converter.processAgents(); err != nil { + return nil, err + } + + // Associate agent instance IDs. + converter.processAgentInstances() + + // Associate agent-related resources. + if err := converter.processAgentApps(); err != nil { + return nil, err + } + if err := converter.processAgentEnvs(); err != nil { + return nil, err + } + if err := converter.processAgentScripts(); err != nil { + return nil, err + } + if err := converter.processAgentDevcontainers(); err != nil { + return nil, err + } + + // Process resource metadata. + if err := converter.processResourceMetadata(); err != nil { + return nil, err + } + + // Build the final resource list. + converter.buildResources() + + // Process parameters and presets. + parameters, err := converter.processParameters() + if err != nil { + return nil, err + } + presets, err := converter.processPresets(parameters) + if err != nil { + return nil, err + } + + // Process AI tasks. + aiTasks, err := converter.processAITasks() + if err != nil { + return nil, err + } + + // Process external auth providers. + externalAuthProviders, err := converter.processExternalAuthProviders() + if err != nil { + return nil, err + } + + return &State{ + Resources: converter.resources, + Parameters: parameters, + Presets: presets, + ExternalAuthProviders: externalAuthProviders, + HasAITasks: hasAITaskResources(converter.graph), + AITasks: aiTasks, + HasExternalAgents: hasExternalAgentResources(converter.graph), + }, nil +} + +// stateConverter holds intermediate state for converting Terraform state to Coder resources. +type stateConverter struct { + ctx context.Context + logger slog.Logger + graph *gographviz.Graph + + // Indexed Terraform resources by their label (what "terraform graph" uses to reference nodes). + tfResourcesByLabel map[string]map[string]*tfjson.StateResource + + // Categorized resources for processing. + tfResourcesRichParameters []*tfjson.StateResource + tfResourcesPresets []*tfjson.StateResource + tfResourcesAITasks []*tfjson.StateResource + + // Output state. + resources []*proto.Resource + resourceAgents map[string][]*proto.Agent + + // Resource metadata collected during processing. + resourceMetadata map[string][]*proto.Resource_Metadata + resourceHidden map[string]bool + resourceIcon map[string]string + resourceCost map[string]int32 +} + +func newConverter(ctx context.Context, logger slog.Logger, graph *gographviz.Graph) *stateConverter { + return &stateConverter{ + ctx: ctx, + logger: logger, + graph: graph, + tfResourcesByLabel: make(map[string]map[string]*tfjson.StateResource), + tfResourcesRichParameters: make([]*tfjson.StateResource, 0), + tfResourcesPresets: make([]*tfjson.StateResource, 0), + tfResourcesAITasks: make([]*tfjson.StateResource, 0), + resources: make([]*proto.Resource, 0), + resourceAgents: make(map[string][]*proto.Agent), + } +} + +// indexTerraformResources recursively indexes all Terraform resources by label and categorizes special types. +func (c *stateConverter) indexTerraformResources(modules []*tfjson.StateModule) { + var indexModule func(mod *tfjson.StateModule) + indexModule = func(mod *tfjson.StateModule) { + for _, module := range mod.ChildModules { + indexModule(module) + } + for _, resource := range mod.Resources { + // Categorize special resource types. + switch resource.Type { + case "coder_parameter": + c.tfResourcesRichParameters = append(c.tfResourcesRichParameters, resource) + case "coder_workspace_preset": + c.tfResourcesPresets = append(c.tfResourcesPresets, resource) + case "coder_ai_task": + c.tfResourcesAITasks = append(c.tfResourcesAITasks, resource) + } + + // Index by label for graph lookups. + label := convertAddressToLabel(resource.Address) + if c.tfResourcesByLabel[label] == nil { + c.tfResourcesByLabel[label] = make(map[string]*tfjson.StateResource) + } + c.tfResourcesByLabel[label][resource.Address] = resource + } + } + + for _, module := range modules { + indexModule(module) + } +} + +// processAgents finds all coder_agent resources, validates them, and associates them with their parent resources via the graph. +func (c *stateConverter) processAgents() error { + agentNames := make(map[string]struct{}) + for _, tfResources := range c.tfResourcesByLabel { + for _, tfResource := range tfResources { + if tfResource.Type != "coder_agent" { + continue + } + var attrs agentAttributes + err := mapstructure.Decode(tfResource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode agent attributes: %w", err) + } + + // Similar logic is duplicated in terraform/resources.go. + if tfResource.Name == "" { + return xerrors.Errorf("agent name cannot be empty") + } + // In 2025-02 we removed support for underscores in agent names. To + // provide a nicer error message, we check the regex first and check + // for underscores if it fails. + if !provisioner.AgentNameRegex.MatchString(tfResource.Name) { + if strings.Contains(tfResource.Name, "_") { + return xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", tfResource.Name, provisioner.AgentNameRegex.String()) + } + return xerrors.Errorf("agent name %q does not match regex %q", tfResource.Name, provisioner.AgentNameRegex.String()) + } + // Agent names must be case-insensitive-unique, to be unambiguous in + // `coder_app`s and CoderVPN DNS names. + if _, ok := agentNames[strings.ToLower(tfResource.Name)]; ok { + return xerrors.Errorf("duplicate agent name: %s", tfResource.Name) + } + agentNames[strings.ToLower(tfResource.Name)] = struct{}{} + + // Handling for deprecated attributes. login_before_ready was replaced + // by startup_script_behavior, but we still need to support it for + // backwards compatibility. + startupScriptBehavior := string(codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking) + if attrs.StartupScriptBehavior != "" { + startupScriptBehavior = attrs.StartupScriptBehavior + } else { + // Handling for provider pre-v0.6.10 (because login_before_ready + // defaulted to true, we must check for its presence). + if _, ok := tfResource.AttributeValues["login_before_ready"]; ok && !attrs.LoginBeforeReady { + startupScriptBehavior = string(codersdk.WorkspaceAgentStartupScriptBehaviorBlocking) + } + } + + 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, + Order: item.Order, + }) + } + + // If a user doesn't specify 'display_apps' then they default + // into all apps except VSCode Insiders. + displayApps := provisionersdk.DefaultDisplayApps() + + if len(attrs.DisplayApps) != 0 { + displayApps = &proto.DisplayApps{ + Vscode: attrs.DisplayApps[0].VSCode, + VscodeInsiders: attrs.DisplayApps[0].VSCodeInsiders, + WebTerminal: attrs.DisplayApps[0].WebTerminal, + PortForwardingHelper: attrs.DisplayApps[0].PortForwardingHelper, + SshHelper: attrs.DisplayApps[0].SSHHelper, + } + } + + resourcesMonitoring := &proto.ResourcesMonitoring{ + Volumes: make([]*proto.VolumeResourceMonitor, 0), + } + + for _, resource := range attrs.ResourcesMonitoring { + for _, memoryResource := range resource.Memory { + resourcesMonitoring.Memory = &proto.MemoryResourceMonitor{ + Enabled: memoryResource.Enabled, + Threshold: memoryResource.Threshold, + } + } + } + + for _, resource := range attrs.ResourcesMonitoring { + for _, volume := range resource.Volumes { + resourcesMonitoring.Volumes = append(resourcesMonitoring.Volumes, &proto.VolumeResourceMonitor{ + Path: volume.Path, + Enabled: volume.Enabled, + Threshold: volume.Threshold, + }) + } + } + + agent := &proto.Agent{ + Name: tfResource.Name, + Id: attrs.ID, + Env: attrs.Env, + OperatingSystem: attrs.OperatingSystem, + Architecture: attrs.Architecture, + Directory: attrs.Directory, + ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds, + TroubleshootingUrl: attrs.TroubleshootingURL, + MotdFile: attrs.MOTDFile, + ResourcesMonitoring: resourcesMonitoring, + Metadata: metadata, + DisplayApps: displayApps, + Order: attrs.Order, + ApiKeyScope: attrs.APIKeyScope, + } + // Support the legacy script attributes in the agent! + if attrs.StartupScript != "" { + agent.Scripts = append(agent.Scripts, &proto.Script{ + // This is ▶️ + Icon: "/emojis/25b6-fe0f.png", + LogPath: "coder-startup-script.log", + DisplayName: "Startup Script", + Script: attrs.StartupScript, + StartBlocksLogin: startupScriptBehavior == string(codersdk.WorkspaceAgentStartupScriptBehaviorBlocking), + RunOnStart: true, + }) + } + if attrs.ShutdownScript != "" { + agent.Scripts = append(agent.Scripts, &proto.Script{ + // This is ◀️ + Icon: "/emojis/25c0.png", + LogPath: "coder-shutdown-script.log", + DisplayName: "Shutdown Script", + Script: attrs.ShutdownScript, + RunOnStop: true, + }) + } + 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 c.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 xerrors.Errorf("couldn't find node on graph: %q", agentLabel) + } + + var agentResource *graphResource + for _, resource := range findResourcesInGraph(c.graph, c.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 := c.resourceAgents[agentResource.Label] + if !exists { + agents = make([]*proto.Agent, 0, 1) + } + agents = append(agents, agent) + c.resourceAgents[agentResource.Label] = agents + } + } + return nil +} + +// processAgentInstances associates instance IDs with agents that use instance-based authentication. +func (c *stateConverter) processAgentInstances() { + for _, resources := range c.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 c.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 + } + } + } + } +} + +// processAgentApps associates coder_app resources with their agents. +func (c *stateConverter) processAgentApps() error { + appSlugs := make(map[string]struct{}) + for _, resources := range c.tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_app" { + continue + } + + var attrs agentAppAttributes + err := mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode app attributes: %w", err) + } + + // Default to the resource name if none is set! + if attrs.Slug == "" { + attrs.Slug = resource.Name + } + // Similar logic is duplicated in terraform/resources.go. + if attrs.DisplayName == "" { + if attrs.Name != "" { + // Name is deprecated but still accepted. + attrs.DisplayName = attrs.Name + } else { + attrs.DisplayName = attrs.Slug + } + } + + // Contrary to agent names above, app slugs were never permitted to + // contain uppercase letters or underscores. + if !provisioner.AppSlugRegex.MatchString(attrs.Slug) { + return xerrors.Errorf("app slug %q does not match regex %q", attrs.Slug, provisioner.AppSlugRegex.String()) + } + + if _, exists := appSlugs[attrs.Slug]; exists { + return 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 + } + + openIn := proto.AppOpenIn_SLIM_WINDOW + switch strings.ToLower(attrs.OpenIn) { + case "slim-window": + openIn = proto.AppOpenIn_SLIM_WINDOW + case "tab": + openIn = proto.AppOpenIn_TAB + } + + for _, agents := range c.resourceAgents { + for _, agent := range agents { + // Find agents with the matching ID and associate them! + + if !dependsOnAgent(c.graph, agent, attrs.AgentID, resource) { + continue + } + + id := attrs.ID + if id == "" { + // This should never happen since the "id" attribute is set on creation: + // https://github.com/coder/terraform-provider-coder/blob/cfa101df4635e405e66094fa7779f9a89d92f400/provider/app.go#L37 + c.logger.Warn(c.ctx, "coder_app's id was unexpectedly empty", slog.F("name", attrs.Name)) + + id = uuid.NewString() + } + + agent.Apps = append(agent.Apps, &proto.App{ + Id: id, + 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, + Order: attrs.Order, + Group: attrs.Group, + Hidden: attrs.Hidden, + OpenIn: openIn, + Tooltip: attrs.Tooltip, + }) + } + } + } + } + return nil +} + +// processAgentEnvs associates coder_env resources with their agents. +func (c *stateConverter) processAgentEnvs() error { + for _, resources := range c.tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_env" { + continue + } + var attrs agentEnvAttributes + err := mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode env attributes: %w", err) + } + for _, agents := range c.resourceAgents { + for _, agent := range agents { + // Find agents with the matching ID and associate them! + if !dependsOnAgent(c.graph, agent, attrs.AgentID, resource) { + continue + } + agent.ExtraEnvs = append(agent.ExtraEnvs, &proto.Env{ + Name: attrs.Name, + Value: attrs.Value, + }) + } + } + } + } + return nil +} + +// processAgentScripts associates coder_script resources with their agents. +func (c *stateConverter) processAgentScripts() error { + for _, resources := range c.tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_script" { + continue + } + var attrs agentScriptAttributes + err := mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode script attributes: %w", err) + } + for _, agents := range c.resourceAgents { + for _, agent := range agents { + // Find agents with the matching ID and associate them! + if !dependsOnAgent(c.graph, agent, attrs.AgentID, resource) { + continue + } + agent.Scripts = append(agent.Scripts, &proto.Script{ + DisplayName: attrs.DisplayName, + Icon: attrs.Icon, + Script: attrs.Script, + Cron: attrs.Cron, + LogPath: attrs.LogPath, + StartBlocksLogin: attrs.StartBlocksLogin, + RunOnStart: attrs.RunOnStart, + RunOnStop: attrs.RunOnStop, + TimeoutSeconds: attrs.TimeoutSeconds, + }) + } + } + } + } + return nil +} + +// processAgentDevcontainers associates coder_devcontainer resources with their agents. +func (c *stateConverter) processAgentDevcontainers() error { + for _, resources := range c.tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_devcontainer" { + continue + } + var attrs agentDevcontainerAttributes + err := mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode script attributes: %w", err) + } + for _, agents := range c.resourceAgents { + for _, agent := range agents { + // Find agents with the matching ID and associate them! + if !dependsOnAgent(c.graph, agent, attrs.AgentID, resource) { + continue + } + agent.Devcontainers = append(agent.Devcontainers, &proto.Devcontainer{ + Name: resource.Name, + WorkspaceFolder: attrs.WorkspaceFolder, + ConfigPath: attrs.ConfigPath, + }) + } + } + } + } + return nil +} + +// processResourceMetadata associates coder_metadata resources with their target resources and collects metadata. +func (c *stateConverter) processResourceMetadata() error { + resourceMetadata := make(map[string][]*proto.Resource_Metadata) + resourceHidden := make(map[string]bool) + resourceIcon := make(map[string]string) + resourceCost := make(map[string]int32) + + metadataTargetLabels := make(map[string]bool) + for _, resources := range c.tfResourcesByLabel { + for _, resource := range resources { + if resource.Type != "coder_metadata" { + continue + } + + var attrs resourceMetadataAttributes + err := mapstructure.Decode(resource.AttributeValues, &attrs) + if err != nil { + return xerrors.Errorf("decode metadata attributes: %w", err) + } + resourceLabel := convertAddressToLabel(resource.Address) + + var attachedNode *gographviz.Node + for _, node := range c.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(c.graph, c.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 + + if metadataTargetLabels[targetLabel] { + return xerrors.Errorf("duplicate metadata resource: %s", targetLabel) + } + metadataTargetLabels[targetLabel] = true + + 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, + }) + } + } + } + + // Store metadata for use in buildResources. + c.resourceMetadata = resourceMetadata + c.resourceHidden = resourceHidden + c.resourceIcon = resourceIcon + c.resourceCost = resourceCost + return nil +} + +// buildResources creates the final proto.Resource list from indexed resources and associated agents/metadata. +func (c *stateConverter) buildResources() { + for _, tfResources := range c.tfResourcesByLabel { + for _, resource := range tfResources { + if resource.Mode == tfjson.DataResourceMode { + continue + } + if resource.Type == "coder_script" || resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" { + continue + } + label := convertAddressToLabel(resource.Address) + modulePath, err := convertAddressToModulePath(resource.Address) + if err != nil { + // Module path recording was added primarily to keep track of + // modules in telemetry. We're adding this sentinel value so + // we can detect if there are any issues with the address + // parsing. + // + // We don't want to set modulePath to null here because, in + // the database, a null value in WorkspaceResource's ModulePath + // indicates "this resource was created before module paths + // were tracked." + modulePath = fmt.Sprintf("%s", ErrInvalidTerraformAddr) + c.logger.Error(c.ctx, "failed to parse Terraform address", slog.F("address", resource.Address)) + } + + agents, exists := c.resourceAgents[label] + if exists { + applyAutomaticInstanceID(resource, agents) + } + + c.resources = append(c.resources, &proto.Resource{ + Name: resource.Name, + Type: resource.Type, + Agents: agents, + Metadata: c.resourceMetadata[label], + Hide: c.resourceHidden[label], + Icon: c.resourceIcon[label], + DailyCost: c.resourceCost[label], + InstanceType: applyInstanceType(resource), + ModulePath: modulePath, + }) + } + } +} + +// processParameters converts coder_parameter resources to proto.RichParameter. +func (c *stateConverter) processParameters() ([]*proto.RichParameter, error) { + var duplicatedParamNames []string + parameters := make([]*proto.RichParameter, 0) + for _, resource := range c.tfResourcesRichParameters { + 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) + } + var defaultVal string + if param.Default != nil { + defaultVal = *param.Default + } + + pft, err := proto.FormType(param.FormType) + if err != nil { + return nil, xerrors.Errorf("decode form_type for coder_parameter.%s: %w", resource.Name, err) + } + + protoParam := &proto.RichParameter{ + Name: param.Name, + DisplayName: param.DisplayName, + Description: param.Description, + FormType: pft, + Type: param.Type, + Mutable: param.Mutable, + DefaultValue: defaultVal, + Icon: param.Icon, + Required: !param.Optional, + // #nosec G115 - Safe conversion as parameter order value is expected to be within int32 range + Order: int32(param.Order), + Ephemeral: param.Ephemeral, + } + 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, + }) + } + } + + // Check if this parameter duplicates an existing parameter. + formattedName := fmt.Sprintf("%q", protoParam.Name) + if !slice.Contains(duplicatedParamNames, formattedName) && + slice.ContainsCompare(parameters, protoParam, func(a, b *proto.RichParameter) bool { + return a.Name == b.Name + }) { + duplicatedParamNames = append(duplicatedParamNames, formattedName) + } + + parameters = append(parameters, protoParam) + } + + // Enforce that parameters be uniquely named. + if len(duplicatedParamNames) > 0 { + s := "" + if len(duplicatedParamNames) == 1 { + s = "s" + } + return nil, xerrors.Errorf( + "coder_parameter names must be unique but %s appear%s multiple times", + stringutil.JoinWithConjunction(duplicatedParamNames), s, + ) + } + + return parameters, nil +} + +// processPresets converts coder_workspace_preset resources to proto.Preset. +func (c *stateConverter) processPresets(parameters []*proto.RichParameter) ([]*proto.Preset, error) { + var duplicatedPresetNames []string + presets := make([]*proto.Preset, 0) + for _, resource := range c.tfResourcesPresets { + var preset provider.WorkspacePreset + err := mapstructure.Decode(resource.AttributeValues, &preset) + if err != nil { + return nil, xerrors.Errorf("decode preset attributes: %w", err) + } + + var duplicatedPresetParameterNames []string + var nonExistentParameters []string + var presetParameters []*proto.PresetParameter + for name, value := range preset.Parameters { + presetParameter := &proto.PresetParameter{ + Name: name, + Value: value, + } + + formattedName := fmt.Sprintf("%q", name) + if !slice.Contains(duplicatedPresetParameterNames, formattedName) && + slice.ContainsCompare(presetParameters, presetParameter, func(a, b *proto.PresetParameter) bool { + return a.Name == b.Name + }) { + duplicatedPresetParameterNames = append(duplicatedPresetParameterNames, formattedName) + } + if !slice.ContainsCompare(parameters, &proto.RichParameter{Name: name}, func(a, b *proto.RichParameter) bool { + return a.Name == b.Name + }) { + nonExistentParameters = append(nonExistentParameters, name) + } + + presetParameters = append(presetParameters, presetParameter) + } + + if len(duplicatedPresetParameterNames) > 0 { + s := "" + if len(duplicatedPresetParameterNames) == 1 { + s = "s" + } + return nil, xerrors.Errorf( + "coder_workspace_preset parameters must be unique but %s appear%s multiple times", stringutil.JoinWithConjunction(duplicatedPresetParameterNames), s, + ) + } + + if len(nonExistentParameters) > 0 { + c.logger.Warn( + c.ctx, + "coder_workspace_preset defines preset values for at least one parameter that is not defined by the template", + slog.F("parameters", stringutil.JoinWithConjunction(nonExistentParameters)), + ) + } + + if len(preset.Prebuilds) != 1 { + c.logger.Warn( + c.ctx, + "coder_workspace_preset must have exactly one prebuild block", + ) + } + var prebuildInstances int32 + var expirationPolicy *proto.ExpirationPolicy + var scheduling *proto.Scheduling + if len(preset.Prebuilds) > 0 { + prebuildInstances = int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].Instances))) + if len(preset.Prebuilds[0].ExpirationPolicy) > 0 { + expirationPolicy = &proto.ExpirationPolicy{ + Ttl: int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].ExpirationPolicy[0].TTL))), + } + } + if len(preset.Prebuilds[0].Scheduling) > 0 { + scheduling = convertScheduling(preset.Prebuilds[0].Scheduling[0]) + } + } + protoPreset := &proto.Preset{ + Name: preset.Name, + Parameters: presetParameters, + Prebuild: &proto.Prebuild{ + Instances: prebuildInstances, + ExpirationPolicy: expirationPolicy, + Scheduling: scheduling, + }, + Default: preset.Default, + Description: preset.Description, + Icon: preset.Icon, + } + + if slice.Contains(duplicatedPresetNames, preset.Name) { + duplicatedPresetNames = append(duplicatedPresetNames, preset.Name) + } + presets = append(presets, protoPreset) + } + if len(duplicatedPresetNames) > 0 { + s := "" + if len(duplicatedPresetNames) == 1 { + s = "s" + } + return nil, xerrors.Errorf( + "coder_workspace_preset names must be unique but %s appear%s multiple times", + stringutil.JoinWithConjunction(duplicatedPresetNames), s, + ) + } + + // Validate that only one preset is marked as default. + var defaultPresets int + for _, preset := range presets { + if preset.Default { + defaultPresets++ + } + } + if defaultPresets > 1 { + return nil, xerrors.Errorf("a maximum of 1 coder_workspace_preset can be marked as default, but %d are set", defaultPresets) + } + + return presets, nil +} + +// processAITasks converts coder_ai_task resources to proto.AITask. +func (c *stateConverter) processAITasks() ([]*proto.AITask, error) { + // This will only pick up resources which will actually be created. + aiTasks := make([]*proto.AITask, 0, len(c.tfResourcesAITasks)) + for _, resource := range c.tfResourcesAITasks { + var task provider.AITask + err := mapstructure.Decode(resource.AttributeValues, &task) + if err != nil { + return nil, xerrors.Errorf("decode coder_ai_task attributes: %w", err) + } + + appID := task.AppID + if appID == "" && len(task.SidebarApp) > 0 { + appID = task.SidebarApp[0].ID + } + + aiTasks = append(aiTasks, &proto.AITask{ + Id: task.ID, + AppId: appID, + SidebarApp: &proto.AITaskSidebarApp{ + Id: appID, + }, + }) + } + + return aiTasks, nil +} + +// processExternalAuthProviders collects coder_external_auth resources. +func (c *stateConverter) processExternalAuthProviders() ([]*proto.ExternalAuthProviderResource, error) { + // A map is used to ensure we don't have duplicates! + externalAuthProvidersMap := make(map[string]*proto.ExternalAuthProviderResource) + for _, tfResources := range c.tfResourcesByLabel { + for _, resource := range tfResources { + // Checking for `coder_git_auth` is legacy! + if resource.Type != "coder_external_auth" && resource.Type != "coder_git_auth" { + continue + } + + id, ok := resource.AttributeValues["id"].(string) + if !ok { + return nil, xerrors.Errorf("external auth id is not a string") + } + optional := false + optionalAttribute, ok := resource.AttributeValues["optional"].(bool) + if ok { + optional = optionalAttribute + } + + externalAuthProvidersMap[id] = &proto.ExternalAuthProviderResource{ + Id: id, + Optional: optional, + } + } + } + externalAuthProviders := make([]*proto.ExternalAuthProviderResource, 0, len(externalAuthProvidersMap)) + for _, it := range externalAuthProvidersMap { + externalAuthProviders = append(externalAuthProviders, it) + } + + return externalAuthProviders, nil +} + +func (c *stateConverter) hasGraph() bool { + return c.graph != nil +} diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index a65615e5f233e..c0e55b2117088 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -206,7 +206,7 @@ func hasExternalAgentResources(graph *gographviz.Graph) bool { // ConvertState consumes Terraform state and a GraphViz representation // produced by `terraform graph` to produce resources consumable by Coder. // nolint:gocognit // This function makes more sense being large for now, until refactored. -func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph string, logger slog.Logger) (*State, error) { +func ConvertState2(ctx context.Context, modules []*tfjson.StateModule, rawGraph string, logger slog.Logger) (*State, error) { parsedGraph, err := gographviz.ParseString(rawGraph) if err != nil { return nil, xerrors.Errorf("parse graph: %w", err) diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index a2c5b536ac2db..021e6941a3989 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -2,16 +2,19 @@ package terraform_test import ( "context" + "crypto/sha256" "encoding/json" "fmt" "os" "path/filepath" "runtime" + "slices" "sort" "strings" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" tfjson "github.com/hashicorp/terraform-json" "github.com/stretchr/testify/require" protobuf "google.golang.org/protobuf/proto" @@ -30,6 +33,116 @@ func ctxAndLogger(t *testing.T) (context.Context, slog.Logger) { return context.Background(), testutil.Logger(t) } +// TestConvertStateGolden compares the output of ConvertState to a golden +// file to prevent regressions. If the logic changes, update the golden files +// accordingly. +// +// This was created to aid in refactoring `ConvertState`. +func TestConvertStateGolden(t *testing.T) { + t.Parallel() + + testResourceDirectories := filepath.Join("testdata", "resources") + entries, err := os.ReadDir(testResourceDirectories) + require.NoError(t, err) + + for _, testDirectory := range entries { + if !testDirectory.IsDir() { + continue + } + + testFiles, err := os.ReadDir(filepath.Join(testResourceDirectories, testDirectory.Name())) + require.NoError(t, err) + + // ConvertState works on both a plan file and a state file. + // The test should create a golden file for both. + for _, step := range []string{"plan", "state"} { + srcIdc := slices.IndexFunc(testFiles, func(entry os.DirEntry) bool { + return strings.HasSuffix(entry.Name(), fmt.Sprintf(".tf%s.json", step)) + }) + dotIdx := slices.IndexFunc(testFiles, func(entry os.DirEntry) bool { + return strings.HasSuffix(entry.Name(), fmt.Sprintf(".tf%s.dot", step)) + }) + + // If the directory is missing these files, we cannot run ConvertState + // on it. So it's skipped. + if srcIdc == -1 || dotIdx == -1 { + continue + } + + t.Run(step+"_"+testDirectory.Name(), func(t *testing.T) { + // Load the paths before t.Parallel() + testDirectoryPath := filepath.Join(testResourceDirectories, testDirectory.Name()) + planFile := filepath.Join(testDirectoryPath, testFiles[srcIdc].Name()) + dotFile := filepath.Join(testDirectoryPath, testFiles[dotIdx].Name()) + + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + logger := slogtest.Make(t, nil) + + // Gather plan + tfStepRaw, err := os.ReadFile(planFile) + require.NoError(t, err) + + var modules []*tfjson.StateModule + switch step { + case "plan": + var tfPlan tfjson.Plan + err = json.Unmarshal(tfStepRaw, &tfPlan) + require.NoError(t, err) + + modules = []*tfjson.StateModule{tfPlan.PlannedValues.RootModule} + if tfPlan.PriorState != nil { + modules = append(modules, tfPlan.PriorState.Values.RootModule) + } + case "state": + var tfState tfjson.State + err = json.Unmarshal(tfStepRaw, &tfState) + require.NoError(t, err) + modules = []*tfjson.StateModule{tfState.Values.RootModule} + default: + t.Fatalf("unknown step: %s", step) + } + + // Gather graph + dotFileRaw, err := os.ReadFile(dotFile) + require.NoError(t, err) + + // expectedOutput is `any` to support errors too. If `ConvertState` returns an + // error, that error is the golden file output. + var expectedOutput any + state, err := terraform.ConvertState(ctx, modules, string(dotFileRaw), logger) + if err == nil { + sortResources(state.Resources) + sortExternalAuthProviders(state.ExternalAuthProviders) + deterministicAppIDs(state.Resources) + expectedOutput = state + } else { + // Write the error to the file then. Track errors as much as valid paths. + expectedOutput = err.Error() + } + + expPath := filepath.Join(testDirectoryPath, fmt.Sprintf("converted_state.%s.golden", step)) + if *updateGoldenFiles { + gotBytes, err := json.MarshalIndent(expectedOutput, "", " ") + require.NoError(t, err, "marshaling converted state to JSON") + // Newline at end of file for git purposes + err = os.WriteFile(expPath, append(gotBytes, '\n'), 0o600) + require.NoError(t, err) + return + } + + gotBytes, err := json.Marshal(expectedOutput) + require.NoError(t, err, "marshaling converted state to JSON") + + expBytes, err := os.ReadFile(expPath) + require.NoError(t, err) + + require.JSONEq(t, string(expBytes), string(gotBytes), "converted state") + }) + } + } +} + func TestConvertResources(t *testing.T) { t.Parallel() // nolint:dogsled @@ -1669,3 +1782,18 @@ func sortExternalAuthProviders(providers []*proto.ExternalAuthProviderResource) return strings.Compare(providers[i].Id, providers[j].Id) == -1 }) } + +// deterministicAppIDs handles setting agent app ids to something deterministic. +// In plan files, ids are not present. In state files, they are. +// It is simpler for comparisons if we just set it to something deterministic. +func deterministicAppIDs(resources []*proto.Resource) { + for _, resource := range resources { + for _, agent := range resource.Agents { + for _, app := range agent.Apps { + data := sha256.Sum256([]byte(app.Slug + app.DisplayName)) + id, _ := uuid.FromBytes(data[:16]) + app.Id = id.String() + } + } + } +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.plan.golden b/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.plan.golden new file mode 100644 index 0000000000000..84ba18790acbe --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.plan.golden @@ -0,0 +1,21 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.state.golden b/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.state.golden new file mode 100644 index 0000000000000..7be30d4b4d5cd --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-app/converted_state.state.golden @@ -0,0 +1,22 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "id": "c4f032b8-97e4-42b0-aa2f-30a9e698f8d4", + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.plan.golden b/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.plan.golden new file mode 100644 index 0000000000000..687d4920b8bec --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.plan.golden @@ -0,0 +1,31 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + }, + { + "name": "b", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + { + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.state.golden b/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.state.golden new file mode 100644 index 0000000000000..10e510eac1c75 --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-multiple/converted_state.state.golden @@ -0,0 +1,33 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + }, + { + "name": "b", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "id": "89e6ab36-2e98-4d13-9b4c-69b7588b7e1d", + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd", + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.plan.golden b/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.plan.golden new file mode 100644 index 0000000000000..84ba18790acbe --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.plan.golden @@ -0,0 +1,21 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.state.golden b/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.state.golden new file mode 100644 index 0000000000000..4984e279fb851 --- /dev/null +++ b/provisioner/terraform/testdata/resources/ai-tasks-sidebar/converted_state.state.golden @@ -0,0 +1,22 @@ +{ + "Resources": [ + { + "name": "a", + "type": "coder_ai_task" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [ + { + "id": "89e6ab36-2e98-4d13-9b4c-69b7588b7e1d", + "sidebar_app": { + "id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + }, + "app_id": "5ece4674-dd35-4f16-88c8-82e40e72e2fd" + } + ], + "HasAITasks": true, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/calling-module/converted_state.plan.golden b/provisioner/terraform/testdata/resources/calling-module/converted_state.plan.golden new file mode 100644 index 0000000000000..ed13fb19fd719 --- /dev/null +++ b/provisioner/terraform/testdata/resources/calling-module/converted_state.plan.golden @@ -0,0 +1,34 @@ +{ + "Resources": [ + { + "name": "example", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ], + "module_path": "module.module" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/calling-module/converted_state.state.golden b/provisioner/terraform/testdata/resources/calling-module/converted_state.state.golden new file mode 100644 index 0000000000000..cefa9f257f7e2 --- /dev/null +++ b/provisioner/terraform/testdata/resources/calling-module/converted_state.state.golden @@ -0,0 +1,35 @@ +{ + "Resources": [ + { + "name": "example", + "type": "null_resource", + "agents": [ + { + "id": "8cb7c83a-eddb-45e9-a78c-4b50d0f10e5e", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "59bcf169-14fe-497d-9a97-709c1d837848" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ], + "module_path": "module.module" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/chaining-resources/converted_state.plan.golden b/provisioner/terraform/testdata/resources/chaining-resources/converted_state.plan.golden new file mode 100644 index 0000000000000..5314f549e7fdd --- /dev/null +++ b/provisioner/terraform/testdata/resources/chaining-resources/converted_state.plan.golden @@ -0,0 +1,37 @@ +{ + "Resources": [ + { + "name": "a", + "type": "null_resource" + }, + { + "name": "b", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/chaining-resources/converted_state.state.golden b/provisioner/terraform/testdata/resources/chaining-resources/converted_state.state.golden new file mode 100644 index 0000000000000..48879277d69f7 --- /dev/null +++ b/provisioner/terraform/testdata/resources/chaining-resources/converted_state.state.golden @@ -0,0 +1,38 @@ +{ + "Resources": [ + { + "name": "a", + "type": "null_resource" + }, + { + "name": "b", + "type": "null_resource", + "agents": [ + { + "id": "d9f5159f-58be-4035-b13c-8e9d988ea2fc", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "20b314d3-9acc-4ae7-8fd7-b8fcfc456e06" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.plan.golden b/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.plan.golden new file mode 100644 index 0000000000000..ee1553bc9b329 --- /dev/null +++ b/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.plan.golden @@ -0,0 +1,37 @@ +{ + "Resources": [ + { + "name": "first", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "second", + "type": "null_resource" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.state.golden b/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.state.golden new file mode 100644 index 0000000000000..6da4224355b3c --- /dev/null +++ b/provisioner/terraform/testdata/resources/conflicting-resources/converted_state.state.golden @@ -0,0 +1,38 @@ +{ + "Resources": [ + { + "name": "first", + "type": "null_resource", + "agents": [ + { + "id": "e78db244-3076-4c04-8ac3-5a55dae032e7", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "c0a7e7f5-2616-429e-ac69-a8c3d9bbbb5d" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "second", + "type": "null_resource" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/devcontainer/converted_state.plan.golden b/provisioner/terraform/testdata/resources/devcontainer/converted_state.plan.golden new file mode 100644 index 0000000000000..fded49faa9e15 --- /dev/null +++ b/provisioner/terraform/testdata/resources/devcontainer/converted_state.plan.golden @@ -0,0 +1,52 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "devcontainers": [ + { + "workspace_folder": "/workspace1", + "name": "dev1" + }, + { + "workspace_folder": "/workspace2", + "config_path": "/workspace2/.devcontainer/devcontainer.json", + "name": "dev2" + } + ], + "api_key_scope": "all" + } + ] + }, + { + "name": "dev1", + "type": "coder_devcontainer" + }, + { + "name": "dev2", + "type": "coder_devcontainer" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden b/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden new file mode 100644 index 0000000000000..fe89c7bcc76c2 --- /dev/null +++ b/provisioner/terraform/testdata/resources/devcontainer/converted_state.state.golden @@ -0,0 +1,53 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "eb1fa705-34c6-405b-a2ec-70e4efd1614e", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "e8663cf8-6991-40ca-b534-b9d48575cc4e" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "devcontainers": [ + { + "workspace_folder": "/workspace1", + "name": "dev1" + }, + { + "workspace_folder": "/workspace2", + "config_path": "/workspace2/.devcontainer/devcontainer.json", + "name": "dev2" + } + ], + "api_key_scope": "all" + } + ] + }, + { + "name": "dev1", + "type": "coder_devcontainer" + }, + { + "name": "dev2", + "type": "coder_devcontainer" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.plan.golden b/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.plan.golden new file mode 100644 index 0000000000000..cdce3f15b2ea5 --- /dev/null +++ b/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.plan.golden @@ -0,0 +1,28 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": {}, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.state.golden b/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.state.golden new file mode 100644 index 0000000000000..924814c69ada2 --- /dev/null +++ b/provisioner/terraform/testdata/resources/display-apps-disabled/converted_state.state.golden @@ -0,0 +1,29 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "149d8647-ec80-4a63-9aa5-2c82452e69a6", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "bd20db5f-7645-411f-b253-033e494e6c89" + }, + "connection_timeout_seconds": 120, + "display_apps": {}, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/display-apps/converted_state.plan.golden b/provisioner/terraform/testdata/resources/display-apps/converted_state.plan.golden new file mode 100644 index 0000000000000..d7fe5795eb0a1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/display-apps/converted_state.plan.golden @@ -0,0 +1,31 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode_insiders": true, + "web_terminal": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/display-apps/converted_state.state.golden b/provisioner/terraform/testdata/resources/display-apps/converted_state.state.golden new file mode 100644 index 0000000000000..63ef183e8925c --- /dev/null +++ b/provisioner/terraform/testdata/resources/display-apps/converted_state.state.golden @@ -0,0 +1,32 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "c49a0e36-fd67-4946-a75f-ff52b77e9f95", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "d9775224-6ecb-4c53-b24d-931555a7c86a" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode_insiders": true, + "web_terminal": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/external-agents/converted_state.plan.golden b/provisioner/terraform/testdata/resources/external-agents/converted_state.plan.golden new file mode 100644 index 0000000000000..2a806a7e08571 --- /dev/null +++ b/provisioner/terraform/testdata/resources/external-agents/converted_state.plan.golden @@ -0,0 +1,33 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "coder_external_agent", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": true +} diff --git a/provisioner/terraform/testdata/resources/external-agents/converted_state.state.golden b/provisioner/terraform/testdata/resources/external-agents/converted_state.state.golden new file mode 100644 index 0000000000000..da0af3790a2e1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/external-agents/converted_state.state.golden @@ -0,0 +1,34 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "coder_external_agent", + "agents": [ + { + "id": "15a35370-3b2e-4ee7-8b28-81cef0152d8b", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "d054c66b-cc5c-41ae-aa0c-2098a1075272" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": true +} diff --git a/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.plan.golden b/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.plan.golden new file mode 100644 index 0000000000000..91bc3bdf09da7 --- /dev/null +++ b/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.plan.golden @@ -0,0 +1,41 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [ + { + "id": "github" + }, + { + "id": "gitlab", + "optional": true + } + ], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.state.golden b/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.state.golden new file mode 100644 index 0000000000000..87a47db1206f1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/external-auth-providers/converted_state.state.golden @@ -0,0 +1,42 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "1682dc74-4f8a-49da-8c36-3df839f5c1f0", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "c018b99e-4370-409c-b81d-6305c5cd9078" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [ + { + "id": "github" + }, + { + "id": "gitlab", + "optional": true + } + ], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/instance-id/converted_state.plan.golden b/provisioner/terraform/testdata/resources/instance-id/converted_state.plan.golden new file mode 100644 index 0000000000000..954495aa0b11f --- /dev/null +++ b/provisioner/terraform/testdata/resources/instance-id/converted_state.plan.golden @@ -0,0 +1,33 @@ +{ + "Resources": [ + { + "name": "main", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "InstanceId": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/instance-id/converted_state.state.golden b/provisioner/terraform/testdata/resources/instance-id/converted_state.state.golden new file mode 100644 index 0000000000000..031e264526c5b --- /dev/null +++ b/provisioner/terraform/testdata/resources/instance-id/converted_state.state.golden @@ -0,0 +1,34 @@ +{ + "Resources": [ + { + "name": "main", + "type": "null_resource", + "agents": [ + { + "id": "8e130bb7-437f-4892-a2e4-ae892f95d824", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "InstanceId": "example" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.plan.golden b/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.plan.golden new file mode 100644 index 0000000000000..b9400c3917df2 --- /dev/null +++ b/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.plan.golden @@ -0,0 +1,85 @@ +{ + "Resources": [ + { + "name": "coder_workspace", + "type": "kubernetes_config_map" + }, + { + "name": "coder_workspace", + "type": "kubernetes_role" + }, + { + "name": "coder_workspace", + "type": "kubernetes_role_binding" + }, + { + "name": "coder_workspace", + "type": "kubernetes_secret" + }, + { + "name": "coder_workspace", + "type": "kubernetes_service_account" + }, + { + "name": "main", + "type": "kubernetes_pod", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "code-server", + "display_name": "code-server", + "url": "http://localhost:13337?folder=/home/coder", + "icon": "/icon/code.svg", + "open_in": 1, + "id": "73971185-3dea-f456-c568-4f285dbcdb52" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Startup Script", + "icon": "/emojis/25b6-fe0f.png", + "script": " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log \u0026\n", + "run_on_start": true, + "log_path": "coder-startup-script.log" + } + ], + "resources_monitoring": {} + } + ], + "metadata": [ + { + "key": "cpu", + "value": "1" + }, + { + "key": "memory", + "value": "1Gi" + }, + { + "key": "gpu", + "value": "1" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.state.golden b/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.state.golden new file mode 100644 index 0000000000000..d70291e74adcc --- /dev/null +++ b/provisioner/terraform/testdata/resources/kubernetes-metadata/converted_state.state.golden @@ -0,0 +1,86 @@ +{ + "Resources": [ + { + "name": "coder_workspace", + "type": "kubernetes_config_map" + }, + { + "name": "coder_workspace", + "type": "kubernetes_role" + }, + { + "name": "coder_workspace", + "type": "kubernetes_role_binding" + }, + { + "name": "coder_workspace", + "type": "kubernetes_secret" + }, + { + "name": "coder_workspace", + "type": "kubernetes_service_account" + }, + { + "name": "main", + "type": "kubernetes_pod", + "agents": [ + { + "id": "b65f06b5-8698-4e47-80fb-e78f9b920e3d", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "code-server", + "display_name": "code-server", + "url": "http://localhost:13337?folder=/home/coder", + "icon": "/icon/code.svg", + "open_in": 1, + "id": "73971185-3dea-f456-c568-4f285dbcdb52" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Startup Script", + "icon": "/emojis/25b6-fe0f.png", + "script": " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log \u0026\n", + "run_on_start": true, + "log_path": "coder-startup-script.log" + } + ], + "resources_monitoring": {} + } + ], + "metadata": [ + { + "key": "cpu", + "value": "1" + }, + { + "key": "memory", + "value": "1Gi" + }, + { + "key": "gpu", + "value": "1" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/mapped-apps/converted_state.plan.golden b/provisioner/terraform/testdata/resources/mapped-apps/converted_state.plan.golden new file mode 100644 index 0000000000000..b868351cd00c0 --- /dev/null +++ b/provisioner/terraform/testdata/resources/mapped-apps/converted_state.plan.golden @@ -0,0 +1,47 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/mapped-apps/converted_state.state.golden b/provisioner/terraform/testdata/resources/mapped-apps/converted_state.state.golden new file mode 100644 index 0000000000000..e932aa73dc4f4 --- /dev/null +++ b/provisioner/terraform/testdata/resources/mapped-apps/converted_state.state.golden @@ -0,0 +1,48 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "bac96c8e-acef-4e1c-820d-0933d6989874", + "name": "dev", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "d52f0d63-5b51-48b3-b342-fd48de4bf957" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.plan.golden new file mode 100644 index 0000000000000..5cfdb43ad5de9 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.plan.golden @@ -0,0 +1,84 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app3", + "display_name": "app3", + "open_in": 1, + "id": "a2714999-3f82-11a4-b8fe-3a11d88f3021" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.state.golden new file mode 100644 index 0000000000000..bf3722980dd25 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-apps/converted_state.state.golden @@ -0,0 +1,86 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "id": "b67999d7-9356-4d32-b3ed-f9ffd283cd5b", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "f736f6d7-6fce-47b6-9fe0-3c99ce17bd8f" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "id": "cb18360a-0bad-4371-a26d-50c30e1d33f7", + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app3", + "display_name": "app3", + "open_in": 1, + "id": "a2714999-3f82-11a4-b8fe-3a11d88f3021" + } + ], + "Auth": { + "Token": "5d1d447c-65b0-47ba-998b-1ba752db7d78" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.plan.golden new file mode 100644 index 0000000000000..75500696591e1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.plan.golden @@ -0,0 +1,84 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "extra_envs": [ + { + "name": "ENV_1", + "value": "Env 1" + }, + { + "name": "ENV_2", + "value": "Env 2" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "extra_envs": [ + { + "name": "ENV_3", + "value": "Env 3" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "env1", + "type": "coder_env" + }, + { + "name": "env2", + "type": "coder_env" + }, + { + "name": "env3", + "type": "coder_env" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.state.golden new file mode 100644 index 0000000000000..c041641367c19 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-envs/converted_state.state.golden @@ -0,0 +1,86 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "id": "fac6034b-1d42-4407-b266-265e35795241", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "1ef61ba1-3502-4e65-b934-8cc63b16877c" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "extra_envs": [ + { + "name": "ENV_1", + "value": "Env 1" + }, + { + "name": "ENV_2", + "value": "Env 2" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "id": "a02262af-b94b-4d6d-98ec-6e36b775e328", + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "3d5caada-8239-4074-8d90-6a28a11858f9" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "extra_envs": [ + { + "name": "ENV_3", + "value": "Env 3" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "env1", + "type": "coder_env" + }, + { + "name": "env2", + "type": "coder_env" + }, + { + "name": "env3", + "type": "coder_env" + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.plan.golden new file mode 100644 index 0000000000000..084a038a9bf37 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.plan.golden @@ -0,0 +1,91 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": { + "memory": { + "enabled": true, + "threshold": 80 + } + }, + "api_key_scope": "all" + }, + { + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": { + "memory": { + "enabled": true, + "threshold": 99 + }, + "volumes": [ + { + "path": "/volume2", + "threshold": 50 + }, + { + "path": "/volume1", + "enabled": true, + "threshold": 80 + } + ] + }, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.state.golden new file mode 100644 index 0000000000000..ded45301131cd --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-monitors/converted_state.state.golden @@ -0,0 +1,93 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "ca077115-5e6d-4ae5-9ca1-10d3b4f21ca8", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + } + ], + "Auth": { + "Token": "91e41276-344e-4664-a560-85f0ceb71a7e" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": { + "memory": { + "enabled": true, + "threshold": 80 + } + }, + "api_key_scope": "all" + }, + { + "id": "e3ce0177-ce0c-4136-af81-90d0751bf3de", + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "2ce64d1c-c57f-4b6b-af87-b693c5998182" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": { + "memory": { + "enabled": true, + "threshold": 99 + }, + "volumes": [ + { + "path": "/volume2", + "threshold": 50 + }, + { + "path": "/volume1", + "enabled": true, + "threshold": 80 + } + ] + }, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.plan.golden new file mode 100644 index 0000000000000..14f2b6ec314f1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.plan.golden @@ -0,0 +1,75 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Foobar Script 1", + "script": "echo foobar 1", + "run_on_start": true + }, + { + "display_name": "Foobar Script 2", + "script": "echo foobar 2", + "run_on_start": true + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Foobar Script 3", + "script": "echo foobar 3", + "run_on_start": true + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.state.golden new file mode 100644 index 0000000000000..9cfdd52317aab --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents-multiple-scripts/converted_state.state.golden @@ -0,0 +1,77 @@ +{ + "Resources": [ + { + "name": "dev1", + "type": "null_resource", + "agents": [ + { + "id": "9d9c16e7-5828-4ca4-9c9d-ba4b61d2b0db", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "2054bc44-b3d1-44e3-8f28-4ce327081ddb" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Foobar Script 1", + "script": "echo foobar 1", + "run_on_start": true + }, + { + "display_name": "Foobar Script 2", + "script": "echo foobar 2", + "run_on_start": true + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + }, + { + "name": "dev2", + "type": "null_resource", + "agents": [ + { + "id": "69cb645c-7a6a-4ad6-be86-dcaab810e7c1", + "name": "dev2", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "c3e73db7-a589-4364-bcf7-0224a9be5c70" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Foobar Script 3", + "script": "echo foobar 3", + "run_on_start": true + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-agents/converted_state.plan.golden new file mode 100644 index 0000000000000..9ad64531d747a --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents/converted_state.plan.golden @@ -0,0 +1,95 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "name": "dev2", + "operating_system": "darwin", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 1, + "motd_file": "/etc/motd", + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Shutdown Script", + "icon": "/emojis/25c0.png", + "script": "echo bye bye", + "run_on_stop": true, + "log_path": "coder-shutdown-script.log" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "name": "dev3", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "troubleshooting_url": "https://coder.com/troubleshoot", + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "name": "dev4", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-agents/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-agents/converted_state.state.golden new file mode 100644 index 0000000000000..7c8d16459485b --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-agents/converted_state.state.golden @@ -0,0 +1,99 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "d3113fa6-6ff3-4532-adc2-c7c51f418fca", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "ecd3c234-6923-4066-9c49-a4ab05f8b25b" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "id": "65036667-6670-4ae9-b081-9e47a659b2a3", + "name": "dev2", + "operating_system": "darwin", + "architecture": "amd64", + "Auth": { + "Token": "d18a13a0-bb95-4500-b789-b341be481710" + }, + "connection_timeout_seconds": 1, + "motd_file": "/etc/motd", + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "scripts": [ + { + "display_name": "Shutdown Script", + "icon": "/emojis/25c0.png", + "script": "echo bye bye", + "run_on_stop": true, + "log_path": "coder-shutdown-script.log" + } + ], + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "id": "ca951672-300e-4d31-859f-72ea307ef692", + "name": "dev3", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "4df063e4-150e-447d-b7fb-8de08f19feca" + }, + "connection_timeout_seconds": 120, + "troubleshooting_url": "https://coder.com/troubleshoot", + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + }, + { + "id": "40b28bed-7b37-4f70-8209-114f26eb09d8", + "name": "dev4", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "d8694897-083f-4a0c-8633-70107a9d45fb" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-apps/converted_state.plan.golden b/provisioner/terraform/testdata/resources/multiple-apps/converted_state.plan.golden new file mode 100644 index 0000000000000..703e01ac4061a --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-apps/converted_state.plan.golden @@ -0,0 +1,59 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + }, + { + "slug": "app3", + "display_name": "app3", + "open_in": 1, + "id": "a2714999-3f82-11a4-b8fe-3a11d88f3021" + } + ], + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/multiple-apps/converted_state.state.golden b/provisioner/terraform/testdata/resources/multiple-apps/converted_state.state.golden new file mode 100644 index 0000000000000..869c56d7974d6 --- /dev/null +++ b/provisioner/terraform/testdata/resources/multiple-apps/converted_state.state.golden @@ -0,0 +1,60 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "947c273b-8ec8-4d7e-9f5f-82d777dd7233", + "name": "dev1", + "operating_system": "linux", + "architecture": "amd64", + "apps": [ + { + "slug": "app1", + "display_name": "app1", + "open_in": 1, + "id": "634ec976-f595-9122-c51e-8da2e3c6e3ce" + }, + { + "slug": "app2", + "display_name": "app2", + "subdomain": true, + "healthcheck": { + "url": "http://localhost:13337/healthz", + "interval": 5, + "threshold": 6 + }, + "open_in": 1, + "id": "13922208-d2bc-196b-54cb-3fc084916309" + }, + { + "slug": "app3", + "display_name": "app3", + "open_in": 1, + "id": "a2714999-3f82-11a4-b8fe-3a11d88f3021" + } + ], + "Auth": { + "Token": "fcb257f7-62fe-48c9-a8fd-b0b80c9fb3c8" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.plan.golden b/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.plan.golden new file mode 100644 index 0000000000000..c1059056c6e4e --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.plan.golden @@ -0,0 +1 @@ +"a maximum of 1 coder_workspace_preset can be marked as default, but 2 are set" diff --git a/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.state.golden b/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.state.golden new file mode 100644 index 0000000000000..c1059056c6e4e --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets-multiple-defaults/converted_state.state.golden @@ -0,0 +1 @@ +"a maximum of 1 coder_workspace_preset can be marked as default, but 2 are set" diff --git a/provisioner/terraform/testdata/resources/presets-single-default/converted_state.plan.golden b/provisioner/terraform/testdata/resources/presets-single-default/converted_state.plan.golden new file mode 100644 index 0000000000000..2113065502811 --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets-single-default/converted_state.plan.golden @@ -0,0 +1,67 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "instance_type", + "description": "Instance type", + "type": "string", + "default_value": "t3.micro", + "form_type": 4 + } + ], + "Presets": [ + { + "name": "development", + "parameters": [ + { + "name": "instance_type", + "value": "t3.micro" + } + ], + "prebuild": { + "instances": 1 + }, + "default": true + }, + { + "name": "production", + "parameters": [ + { + "name": "instance_type", + "value": "t3.large" + } + ], + "prebuild": { + "instances": 2 + } + } + ], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/presets-single-default/converted_state.state.golden b/provisioner/terraform/testdata/resources/presets-single-default/converted_state.state.golden new file mode 100644 index 0000000000000..ecf470e46a67e --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets-single-default/converted_state.state.golden @@ -0,0 +1,68 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "5d66372f-a526-44ee-9eac-0c16bcc57aa2", + "name": "dev", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "70ab06e5-ef86-4ac2-a1d9-58c8ad85d379" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "instance_type", + "description": "Instance type", + "type": "string", + "default_value": "t3.micro", + "form_type": 4 + } + ], + "Presets": [ + { + "name": "development", + "parameters": [ + { + "name": "instance_type", + "value": "t3.micro" + } + ], + "prebuild": { + "instances": 1 + }, + "default": true + }, + { + "name": "production", + "parameters": [ + { + "name": "instance_type", + "value": "t3.large" + } + ], + "prebuild": { + "instances": 2 + } + } + ], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/presets/converted_state.plan.golden b/provisioner/terraform/testdata/resources/presets/converted_state.plan.golden new file mode 100644 index 0000000000000..ecfa791e257d3 --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets/converted_state.plan.golden @@ -0,0 +1,102 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "First parameter from child module", + "description": "First parameter from child module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from child module", + "description": "Second parameter from child module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "First parameter from module", + "description": "First parameter from module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from module", + "description": "Second parameter from module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "form_type": 4 + } + ], + "Presets": [ + { + "name": "My First Project", + "parameters": [ + { + "name": "Sample", + "value": "A1B2C3" + } + ], + "prebuild": { + "instances": 4, + "expiration_policy": { + "ttl": 86400 + }, + "scheduling": { + "timezone": "America/Los_Angeles", + "schedule": [ + { + "cron": "* 8-18 * * 1-5", + "instances": 3 + }, + { + "cron": "* 8-14 * * 6", + "instances": 1 + } + ] + } + } + } + ], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/presets/converted_state.state.golden b/provisioner/terraform/testdata/resources/presets/converted_state.state.golden new file mode 100644 index 0000000000000..a1b67adb76f4e --- /dev/null +++ b/provisioner/terraform/testdata/resources/presets/converted_state.state.golden @@ -0,0 +1,103 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "8cfc2f0d-5cd6-4631-acfa-c3690ae5557c", + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "abc9d31e-d1d6-4f2c-9e35-005ebe39aeec" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "First parameter from child module", + "description": "First parameter from child module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from child module", + "description": "Second parameter from child module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "First parameter from module", + "description": "First parameter from module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from module", + "description": "Second parameter from module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "form_type": 4 + } + ], + "Presets": [ + { + "name": "My First Project", + "parameters": [ + { + "name": "Sample", + "value": "A1B2C3" + } + ], + "prebuild": { + "instances": 4, + "expiration_policy": { + "ttl": 86400 + }, + "scheduling": { + "timezone": "America/Los_Angeles", + "schedule": [ + { + "cron": "* 8-18 * * 1-5", + "instances": 3 + }, + { + "cron": "* 8-14 * * 6", + "instances": 1 + } + ] + } + } + } + ], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.plan.golden b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.plan.golden new file mode 100644 index 0000000000000..8731a0c260de1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.plan.golden @@ -0,0 +1 @@ +"duplicate metadata resource: null_resource.about" diff --git a/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.state.golden b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.state.golden new file mode 100644 index 0000000000000..8731a0c260de1 --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-metadata-duplicate/converted_state.state.golden @@ -0,0 +1 @@ +"duplicate metadata resource: null_resource.about" diff --git a/provisioner/terraform/testdata/resources/resource-metadata/converted_state.plan.golden b/provisioner/terraform/testdata/resources/resource-metadata/converted_state.plan.golden new file mode 100644 index 0000000000000..2a351e856ef7d --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-metadata/converted_state.plan.golden @@ -0,0 +1,63 @@ +{ + "Resources": [ + { + "name": "about", + "type": "null_resource", + "agents": [ + { + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "metadata": [ + { + "key": "process_count", + "display_name": "Process Count", + "script": "ps -ef | wc -l", + "interval": 5, + "timeout": 1, + "order": 7 + } + ], + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ], + "metadata": [ + { + "key": "hello", + "value": "world" + }, + { + "key": "null" + }, + { + "key": "empty" + }, + { + "key": "secret", + "value": "squirrel", + "sensitive": true + } + ], + "hide": true, + "icon": "/icon/server.svg", + "daily_cost": 29 + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/resource-metadata/converted_state.state.golden b/provisioner/terraform/testdata/resources/resource-metadata/converted_state.state.golden new file mode 100644 index 0000000000000..3f0578713e01a --- /dev/null +++ b/provisioner/terraform/testdata/resources/resource-metadata/converted_state.state.golden @@ -0,0 +1,65 @@ +{ + "Resources": [ + { + "name": "about", + "type": "null_resource", + "agents": [ + { + "id": "9a5911cd-2335-4050-aba8-4c26ba1ca704", + "name": "main", + "operating_system": "linux", + "architecture": "amd64", + "Auth": { + "Token": "2b4471d9-1281-45bf-8be2-9b182beb9285" + }, + "connection_timeout_seconds": 120, + "metadata": [ + { + "key": "process_count", + "display_name": "Process Count", + "script": "ps -ef | wc -l", + "interval": 5, + "timeout": 1, + "order": 7 + } + ], + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ], + "metadata": [ + { + "key": "hello", + "value": "world" + }, + { + "key": "null", + "is_null": true + }, + { + "key": "empty" + }, + { + "key": "secret", + "value": "squirrel", + "sensitive": true + } + ], + "hide": true, + "icon": "/icon/server.svg", + "daily_cost": 29 + } + ], + "Parameters": [], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.plan.golden b/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.plan.golden new file mode 100644 index 0000000000000..5a76d1778b382 --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.plan.golden @@ -0,0 +1,49 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "Example", + "type": "string", + "required": true, + "order": 55, + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "order": 99, + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.state.golden b/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.state.golden new file mode 100644 index 0000000000000..5f001d4f104bc --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters-order/converted_state.state.golden @@ -0,0 +1,50 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "09d607d0-f6dc-4d6b-b76c-0c532f34721e", + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "ac504187-c31b-408f-8f1a-f7927a6de3bc" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "Example", + "type": "string", + "required": true, + "order": 55, + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "order": 99, + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.plan.golden b/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.plan.golden new file mode 100644 index 0000000000000..1476afaf6f2d8 --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.plan.golden @@ -0,0 +1,78 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "number_example", + "type": "number", + "mutable": true, + "default_value": "4", + "ephemeral": true, + "form_type": 4 + }, + { + "name": "number_example_max", + "type": "number", + "default_value": "4", + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_max_zero", + "type": "number", + "default_value": "-3", + "validation_max": 0, + "form_type": 4 + }, + { + "name": "number_example_min", + "type": "number", + "default_value": "4", + "validation_min": 3, + "form_type": 4 + }, + { + "name": "number_example_min_max", + "type": "number", + "default_value": "4", + "validation_min": 3, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_min_zero", + "type": "number", + "default_value": "4", + "validation_min": 0, + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.state.golden b/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.state.golden new file mode 100644 index 0000000000000..d8817ca5e900e --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters-validation/converted_state.state.golden @@ -0,0 +1,79 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "9c8368da-924c-4df4-a049-940a9a035051", + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "e09a4d7d-8341-4adf-b93b-21f3724d76d7" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "number_example", + "type": "number", + "mutable": true, + "default_value": "4", + "ephemeral": true, + "form_type": 4 + }, + { + "name": "number_example_max", + "type": "number", + "default_value": "4", + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_max_zero", + "type": "number", + "default_value": "-3", + "validation_max": 0, + "form_type": 4 + }, + { + "name": "number_example_min", + "type": "number", + "default_value": "4", + "validation_min": 3, + "form_type": 4 + }, + { + "name": "number_example_min_max", + "type": "number", + "default_value": "4", + "validation_min": 3, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_min_zero", + "type": "number", + "default_value": "4", + "validation_min": 0, + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters/converted_state.plan.golden b/provisioner/terraform/testdata/resources/rich-parameters/converted_state.plan.golden new file mode 100644 index 0000000000000..1089e51a88db8 --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters/converted_state.plan.golden @@ -0,0 +1,119 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "First parameter from child module", + "description": "First parameter from child module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from child module", + "description": "Second parameter from child module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "First parameter from module", + "description": "First parameter from module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from module", + "description": "Second parameter from module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "Example", + "type": "string", + "options": [ + { + "name": "First Option", + "value": "first" + }, + { + "name": "Second Option", + "value": "second" + } + ], + "required": true, + "form_type": 2 + }, + { + "name": "number_example", + "type": "number", + "default_value": "4", + "form_type": 4 + }, + { + "name": "number_example_max_zero", + "type": "number", + "default_value": "-2", + "validation_min": -3, + "validation_max": 0, + "form_type": 4 + }, + { + "name": "number_example_min_max", + "type": "number", + "default_value": "4", + "validation_min": 3, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_min_zero", + "type": "number", + "default_value": "4", + "validation_min": 0, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +} diff --git a/provisioner/terraform/testdata/resources/rich-parameters/converted_state.state.golden b/provisioner/terraform/testdata/resources/rich-parameters/converted_state.state.golden new file mode 100644 index 0000000000000..1a0efa09663fb --- /dev/null +++ b/provisioner/terraform/testdata/resources/rich-parameters/converted_state.state.golden @@ -0,0 +1,120 @@ +{ + "Resources": [ + { + "name": "dev", + "type": "null_resource", + "agents": [ + { + "id": "047fe781-ea5d-411a-b31c-4400a00e6166", + "name": "dev", + "operating_system": "windows", + "architecture": "arm64", + "Auth": { + "Token": "261ca0f7-a388-42dd-b113-d25e31e346c9" + }, + "connection_timeout_seconds": 120, + "display_apps": { + "vscode": true, + "web_terminal": true, + "ssh_helper": true, + "port_forwarding_helper": true + }, + "resources_monitoring": {}, + "api_key_scope": "all" + } + ] + } + ], + "Parameters": [ + { + "name": "First parameter from child module", + "description": "First parameter from child module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from child module", + "description": "Second parameter from child module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "First parameter from module", + "description": "First parameter from module", + "type": "string", + "mutable": true, + "default_value": "abcdef", + "form_type": 4 + }, + { + "name": "Second parameter from module", + "description": "Second parameter from module", + "type": "string", + "mutable": true, + "default_value": "ghijkl", + "form_type": 4 + }, + { + "name": "Example", + "type": "string", + "options": [ + { + "name": "First Option", + "value": "first" + }, + { + "name": "Second Option", + "value": "second" + } + ], + "required": true, + "form_type": 2 + }, + { + "name": "number_example", + "type": "number", + "default_value": "4", + "form_type": 4 + }, + { + "name": "number_example_max_zero", + "type": "number", + "default_value": "-2", + "validation_min": -3, + "validation_max": 0, + "form_type": 4 + }, + { + "name": "number_example_min_max", + "type": "number", + "default_value": "4", + "validation_min": 3, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "number_example_min_zero", + "type": "number", + "default_value": "4", + "validation_min": 0, + "validation_max": 6, + "form_type": 4 + }, + { + "name": "Sample", + "description": "blah blah", + "type": "string", + "default_value": "ok", + "form_type": 4 + } + ], + "Presets": [], + "ExternalAuthProviders": [], + "AITasks": [], + "HasAITasks": false, + "HasExternalAgents": false +}