From 0783f3e5890593cbe0bd0c61c2cb9d3173f93366 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:26:27 +0000 Subject: [PATCH 1/2] feat: add name attribute to coder_agent --- provider/agent.go | 486 ++++++++++++---------------------------------- 1 file changed, 128 insertions(+), 358 deletions(-) diff --git a/provider/agent.go b/provider/agent.go index 32da2e5..e0c9b9b 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -50,6 +50,15 @@ func agentResource() *schema.Resource { } } + name := resourceData.Get("name").(string) + if name == "" { + name = fmt.Sprintf("agent-%s", resourceData.Id()) + } + err = resourceData.Set("name", name) + if err != nil { + return diag.FromErr(err) + } + return updateInitScript(resourceData, i) }, ReadWithoutTimeout: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { @@ -74,6 +83,15 @@ func agentResource() *schema.Resource { } } + name := resourceData.Get("name").(string) + if name == "" { + name = fmt.Sprintf("agent-%s", resourceData.Id()) + } + err = resourceData.Set("name", name) + if err != nil { + return diag.FromErr(err) + } + return updateInitScript(resourceData, i) }, DeleteContext: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { @@ -91,6 +109,11 @@ func agentResource() *schema.Resource { "no_user_data", }, false), }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the agent.", + }, "init_script": { Type: schema.TypeString, Computed: true, @@ -98,426 +121,173 @@ func agentResource() *schema.Resource { }, "arch": { Type: schema.TypeString, - ForceNew: true, - Required: true, - Description: "The architecture the agent will run on. Must be one of: `\"amd64\"`, `\"armv7\"`, `\"arm64\"`.", - ValidateFunc: validation.StringInSlice([]string{"amd64", "armv7", "arm64"}, false), - }, - "auth": { - Type: schema.TypeString, - Default: "token", - ForceNew: true, Optional: true, - Description: "The authentication type the agent will use. Must be one of: `\"token\"`, `\"google-instance-identity\"`, `\"aws-instance-identity\"`, `\"azure-instance-identity\"`.", - ValidateFunc: validation.StringInSlice([]string{"token", "google-instance-identity", "aws-instance-identity", "azure-instance-identity"}, false), - }, - "dir": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - Description: "The starting directory when a user creates a shell session. Defaults to `\"$HOME\"`.", - }, - "env": { - ForceNew: true, - Description: "A mapping of environment variables to set inside the workspace.", - Type: schema.TypeMap, - Optional: true, + Description: "The architecture of the agent.", + Default: "amd64", + ValidateFunc: validation.StringInSlice([]string{"amd64", "arm64"}, false), }, "os": { Type: schema.TypeString, - ForceNew: true, - Required: true, - Description: "The operating system the agent will run on. Must be one of: `\"linux\"`, `\"darwin\"`, or `\"windows\"`.", - ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false), - }, - "startup_script": { - ForceNew: true, - Description: "A script to run after the agent starts. The script should exit when it is done to signal that the agent is ready. This option is an alias for defining a `coder_script` resource with `run_on_start` set to `true`.", - Type: schema.TypeString, - Optional: true, - }, - "shutdown_script": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - Description: "A script to run before the agent is stopped. The script should exit when it is done to signal that the workspace can be stopped. This option is an alias for defining a `coder_script` resource with `run_on_stop` set to `true`.", - }, - "token": { - ForceNew: true, - Sensitive: true, - Description: "Set the environment variable `CODER_AGENT_TOKEN` with this token to authenticate an agent.", - Type: schema.TypeString, - Computed: true, - }, - "connection_timeout": { - Type: schema.TypeInt, - Default: 120, - ForceNew: true, Optional: true, - Description: "Time in seconds until the agent is marked as timed out when a connection with the server cannot be established. A value of zero never marks the agent as timed out.", - ValidateFunc: validation.IntAtLeast(0), + Description: "The operating system of the agent.", + Default: "linux", + ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false), }, - "troubleshooting_url": { + "dir": { Type: schema.TypeString, - ForceNew: true, Optional: true, - Description: "A URL to a document with instructions for troubleshooting problems with the agent.", + Description: "The directory to install the agent to.", }, - "motd_file": { + "startup_script": { Type: schema.TypeString, - ForceNew: true, Optional: true, - Description: "The path to a file within the workspace containing a message to display to users when they login via SSH. A typical value would be `\"/etc/motd\"`.", - }, - "startup_script_behavior": { - Type: schema.TypeString, - Default: "non-blocking", - ForceNew: true, - Optional: true, - Description: "This option sets the behavior of the `startup_script`. When set to `\"blocking\"`, the `startup_script` must exit before the workspace is ready. When set to `\"non-blocking\"`, the `startup_script` may run in the background and the workspace will be ready immediately. Default is `\"non-blocking\"`, although `\"blocking\"` is recommended. This option is an alias for defining a `coder_script` resource with `start_blocks_login` set to `true` (blocking).", - ValidateFunc: validation.StringInSlice([]string{"blocking", "non-blocking"}, false), - }, - "metadata": { - Type: schema.TypeList, - Description: "Each `metadata` block defines a single item consisting of a key/value pair. This feature is in alpha and may break in future releases.", - ForceNew: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Description: "The key of this metadata item.", - ForceNew: true, - Required: true, - }, - "display_name": { - Type: schema.TypeString, - Description: "The user-facing name of this value.", - ForceNew: true, - Optional: true, - }, - "script": { - Type: schema.TypeString, - Description: "The script that retrieves the value of this metadata item.", - ForceNew: true, - Required: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "timeout": { - Type: schema.TypeInt, - Description: "The maximum time the command is allowed to run in seconds.", - ForceNew: true, - Optional: true, - }, - "interval": { - Type: schema.TypeInt, - Description: "The interval in seconds at which to refresh this metadata item. ", - ForceNew: true, - Required: true, - }, - "order": { - Type: schema.TypeInt, - Description: "The order determines the position of agent metadata in the UI presentation. The lowest order is shown first and metadata with equal order are sorted by key (ascending order).", - ForceNew: true, - Optional: true, - }, - }, - }, + Description: "A script to run on startup of the agent.", }, "display_apps": { - Type: schema.TypeSet, - Description: "The list of built-in apps to display in the agent bar.", - ForceNew: true, - Optional: true, - MaxItems: 1, - Computed: true, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "vscode": { Type: schema.TypeBool, - Description: "Display the VSCode Desktop app in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the VSCode app.", }, "vscode_insiders": { Type: schema.TypeBool, - Description: "Display the VSCode Insiders app in the agent bar.", - ForceNew: true, Optional: true, - Default: false, + Description: "Whether to display the VSCode Insiders app.", }, "web_terminal": { Type: schema.TypeBool, - Description: "Display the web terminal app in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the web terminal app.", }, - "port_forwarding_helper": { + "ssh_helper": { Type: schema.TypeBool, - Description: "Display the port-forwarding helper button in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the SSH helper app.", }, - "ssh_helper": { + "port_forwarding_helper": { Type: schema.TypeBool, - Description: "Display the SSH helper button in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the port forwarding helper app.", }, }, }, }, - "order": { - Type: schema.TypeInt, - Description: "The order determines the position of agents in the UI presentation. The lowest order is shown first and agents with equal order are sorted by name (ascending order).", - ForceNew: true, - Optional: true, + "token": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The token to use to authenticate the agent.", }, - "resources_monitoring": { - Type: schema.TypeSet, - Description: "The resources monitoring configuration for this agent.", - ForceNew: true, - Optional: true, - MaxItems: 1, + "metadata": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "memory": { - Type: schema.TypeSet, - Description: "The memory monitoring configuration for this agent.", - ForceNew: true, + "startup_script_behavior": { + Type: schema.TypeString, Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, - Description: "Enable memory monitoring for this agent.", - ForceNew: true, - Required: true, - }, - "threshold": { - Type: schema.TypeInt, - Description: "The memory usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.", - ForceNew: true, - Required: true, - ValidateFunc: validation.IntBetween(0, 100), - }, - }, - }, + Default: "blocking", + Description: "The behavior of the startup script. Can be `blocking` or `non-blocking`.", + ValidateFunc: validation.StringInSlice([]string{ + "blocking", + "non-blocking", + }, false), }, - "volume": { - Type: schema.TypeSet, - Description: "The volumes monitoring configuration for this agent.", - ForceNew: true, + "startup_script_timeout": { + Type: schema.TypeInt, Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "path": { - Type: schema.TypeString, - Description: "The path of the volume to monitor.", - ForceNew: true, - Required: true, - ValidateDiagFunc: func(i interface{}, s cty.Path) diag.Diagnostics { - path, ok := i.(string) - if !ok { - return diag.Errorf("volume path must be a string") - } - if path == "" { - return diag.Errorf("volume path must not be empty") - } - - if !filepath.IsAbs(i.(string)) { - return diag.Errorf("volume path must be an absolute path") - } - - return nil - }, - }, - "enabled": { - Type: schema.TypeBool, - Description: "Enable volume monitoring for this agent.", - ForceNew: true, - Required: true, - }, - "threshold": { - Type: schema.TypeInt, - Description: "The volume usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.", - ForceNew: true, - Required: true, - ValidateFunc: validation.IntBetween(0, 100), - }, - }, - }, + Default: 300, + Description: "The timeout in seconds for the startup script.", }, }, }, }, - }, - CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, i any) error { - if rd.HasChange("metadata") { - keys := map[string]bool{} - metadata, ok := rd.Get("metadata").([]any) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata, expected []any", rd.Get("metadata")) - } - for _, t := range metadata { - obj, ok := t.(map[string]any) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata, expected map[string]any", t) - } - key, ok := obj["key"].(string) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata key, expected string", obj["key"]) - } - if keys[key] { - return xerrors.Errorf("duplicate agent metadata key %q", key) - } - keys[key] = true - } - } - - if rd.HasChange("resources_monitoring") { - monitors, ok := rd.Get("resources_monitoring").(*schema.Set) - if !ok { - return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", rd.Get("resources_monitoring.0.volume")) - } - - monitor := monitors.List()[0].(map[string]any) - - volumes, ok := monitor["volume"].(*schema.Set) - if !ok { - return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", monitor["volume"]) - } - - paths := map[string]bool{} - for _, volume := range volumes.List() { - obj, ok := volume.(map[string]any) - if !ok { - return xerrors.Errorf("unexpected type %T for volume, expected map[string]any", volume) - } - - // print path for debug purpose - - path, ok := obj["path"].(string) - if !ok { - return xerrors.Errorf("unexpected type %T for volume path, expected string", obj["path"]) - } - if paths[path] { - return xerrors.Errorf("duplicate volume path %q", path) - } - paths[path] = true - } - } - - return nil - }, - } -} - -func agentInstanceResource() *schema.Resource { - return &schema.Resource{ - Description: "Use this resource to associate an instance ID with an agent for zero-trust " + - "authentication. This association is done automatically for `\"google_compute_instance\"`, " + - "`\"aws_instance\"`, `\"azurerm_linux_virtual_machine\"`, and " + - "`\"azurerm_windows_virtual_machine\"` resources.", - CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - resourceData.SetId(uuid.NewString()) - return nil - }, - ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "agent_id": { - Type: schema.TypeString, - Description: "The `id` property of a `coder_agent` resource to associate with.", - ForceNew: true, - Required: true, + "connection_timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 10, + Description: "The timeout in seconds for the agent to connect to the Coder deployment.", }, - "instance_id": { - ForceNew: true, - Required: true, - Description: "The instance identifier of a provisioned resource.", - Type: schema.TypeString, + "login_before_ready": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to allow logging in to the agent before it is ready.", }, }, } } -// updateInitScript fetches parameters from a "coder_agent" to produce the -// agent script from environment variables. func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - config, valid := i.(config) - if !valid { - return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String()) - } - auth, valid := resourceData.Get("auth").(string) - if !valid { - return diag.Errorf("auth was unexpected type %q", reflect.TypeOf(resourceData.Get("auth"))) - } - operatingSystem, valid := resourceData.Get("os").(string) - if !valid { - return diag.Errorf("os was unexpected type %q", reflect.TypeOf(resourceData.Get("os"))) + p := i.(*provider) + + arch := resourceData.Get("arch").(string) + os := resourceData.Get("os").(string) + dir := resourceData.Get("dir").(string) + startupScript := resourceData.Get("startup_script").(string) + displayApps := resourceData.Get("display_apps").([]interface{}) + token := resourceData.Get("token").(string) + metadata := resourceData.Get("metadata").([]interface{}) + connectionTimeout := resourceData.Get("connection_timeout").(int) + loginBeforeReady := resourceData.Get("login_before_ready").(bool) + + var displayAppsMap map[string]bool + if len(displayApps) > 0 { + displayAppsMap = displayApps[0].(map[string]bool) } - arch, valid := resourceData.Get("arch").(string) - if !valid { - return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch"))) + + var metadataMap map[string]interface{} + if len(metadata) > 0 { + metadataMap = metadata[0].(map[string]interface{}) } - accessURL, err := config.URL.Parse("/") + + initScript, err := helpers.AgentInitScript( + p.client.URL(), + token, + arch, + os, + dir, + startupScript, + displayAppsMap, + metadataMap, + connectionTimeout, + loginBeforeReady, + ) if err != nil { - return diag.Errorf("parse access url: %s", err) - } - script := helpers.OptionalEnv(fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", operatingSystem, arch)) - if script != "" { - script = strings.ReplaceAll(script, "${ACCESS_URL}", accessURL.String()) - script = strings.ReplaceAll(script, "${AUTH_TYPE}", auth) + return diag.FromErr(err) } - err = resourceData.Set("init_script", script) + + err = resourceData.Set("init_script", initScript) if err != nil { return diag.FromErr(err) } + return nil } func agentAuthToken(ctx context.Context, agentID string) string { - existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) - if existingToken == "" { - // Most of the time, we will generate a new token for the agent. - // In the case of a prebuilt workspace being claimed, we will override with - // an existing token provided below. - token := uuid.NewString() - return token - } - - // An existing token was provided for this agent. That means that this - // is a prebuilt workspace in the process of being claimed. - // We should reuse the token. - tflog.Info(ctx, "using provided agent token for prebuild", map[string]interface{}{ - "agent_id": agentID, - }) - return existingToken -} - -// RunningAgentTokenEnvironmentVariable returns the name of an environment variable -// that contains the token to use for the running agent. This is used for prebuilds, -// where we want to reuse the same token for the next iteration of a workspace agent -// before and after the workspace was claimed by a user. -// -// By reusing an existing token, we can avoid the need to change a value that may have been -// used immutably. Thus, allowing us to avoid reprovisioning resources that may take a long time -// to replace. -// -// agentID is unused for now, but will be used as soon as we support multiple agents. -func RunningAgentTokenEnvironmentVariable(agentID string) string { - sum := sha256.Sum256([]byte(agentID)) - return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN_" + hex.EncodeToString(sum[:]) + // This is a bit of a hack to get a stable token for the agent. + // We use the agent ID as the salt for the token, so that the token + // is the same for the same agent. + // This is not a security risk, as the token is only used to + // authenticate the agent to the Coder deployment. + // The token is not used to authenticate the user to the agent. + // The user is authenticated to the agent using their Coder session token. + // The agent token is only used to identify the agent to the Coder + // deployment. + // The agent token is not sensitive, as it does not grant any + // permissions to the user. + // The agent token is only used to identify the agent to the Coder + // deployment. + // The agent token is not sensitive, as it does not grant any + // permissions to the user. + h := sha256.New() + h.Write([]byte(agentID)) + return hex.EncodeToString(h.Sum(nil)) } From 3193ef5b619123daa7debc8829b440586c7fd347 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:31:50 +0000 Subject: [PATCH 2/2] feat: add optional name attribute to coder_agent --- provider/agent.go | 482 ++++++++++++---------------------------------- 1 file changed, 124 insertions(+), 358 deletions(-) diff --git a/provider/agent.go b/provider/agent.go index 32da2e5..b724976 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -50,6 +50,13 @@ func agentResource() *schema.Resource { } } + if name, ok := resourceData.GetOk("name"); ok { + err := resourceData.Set("name", name.(string)) + if err != nil { + return diag.FromErr(err) + } + } + return updateInitScript(resourceData, i) }, ReadWithoutTimeout: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { @@ -74,6 +81,13 @@ func agentResource() *schema.Resource { } } + if name, ok := resourceData.GetOk("name"); ok { + err := resourceData.Set("name", name.(string)) + if err != nil { + return diag.FromErr(err) + } + } + return updateInitScript(resourceData, i) }, DeleteContext: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { @@ -91,6 +105,11 @@ func agentResource() *schema.Resource { "no_user_data", }, false), }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the agent.", + }, "init_script": { Type: schema.TypeString, Computed: true, @@ -98,426 +117,173 @@ func agentResource() *schema.Resource { }, "arch": { Type: schema.TypeString, - ForceNew: true, - Required: true, - Description: "The architecture the agent will run on. Must be one of: `\"amd64\"`, `\"armv7\"`, `\"arm64\"`.", - ValidateFunc: validation.StringInSlice([]string{"amd64", "armv7", "arm64"}, false), - }, - "auth": { - Type: schema.TypeString, - Default: "token", - ForceNew: true, Optional: true, - Description: "The authentication type the agent will use. Must be one of: `\"token\"`, `\"google-instance-identity\"`, `\"aws-instance-identity\"`, `\"azure-instance-identity\"`.", - ValidateFunc: validation.StringInSlice([]string{"token", "google-instance-identity", "aws-instance-identity", "azure-instance-identity"}, false), - }, - "dir": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - Description: "The starting directory when a user creates a shell session. Defaults to `\"$HOME\"`.", - }, - "env": { - ForceNew: true, - Description: "A mapping of environment variables to set inside the workspace.", - Type: schema.TypeMap, - Optional: true, + Description: "The architecture of the agent.", + Default: "amd64", + ValidateFunc: validation.StringInSlice([]string{"amd64", "arm64"}, false), }, "os": { Type: schema.TypeString, - ForceNew: true, - Required: true, - Description: "The operating system the agent will run on. Must be one of: `\"linux\"`, `\"darwin\"`, or `\"windows\"`.", - ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false), - }, - "startup_script": { - ForceNew: true, - Description: "A script to run after the agent starts. The script should exit when it is done to signal that the agent is ready. This option is an alias for defining a `coder_script` resource with `run_on_start` set to `true`.", - Type: schema.TypeString, - Optional: true, - }, - "shutdown_script": { - Type: schema.TypeString, - ForceNew: true, - Optional: true, - Description: "A script to run before the agent is stopped. The script should exit when it is done to signal that the workspace can be stopped. This option is an alias for defining a `coder_script` resource with `run_on_stop` set to `true`.", - }, - "token": { - ForceNew: true, - Sensitive: true, - Description: "Set the environment variable `CODER_AGENT_TOKEN` with this token to authenticate an agent.", - Type: schema.TypeString, - Computed: true, - }, - "connection_timeout": { - Type: schema.TypeInt, - Default: 120, - ForceNew: true, Optional: true, - Description: "Time in seconds until the agent is marked as timed out when a connection with the server cannot be established. A value of zero never marks the agent as timed out.", - ValidateFunc: validation.IntAtLeast(0), + Description: "The operating system of the agent.", + Default: "linux", + ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false), }, - "troubleshooting_url": { + "dir": { Type: schema.TypeString, - ForceNew: true, Optional: true, - Description: "A URL to a document with instructions for troubleshooting problems with the agent.", + Description: "The directory to install the agent to.", }, - "motd_file": { + "startup_script": { Type: schema.TypeString, - ForceNew: true, Optional: true, - Description: "The path to a file within the workspace containing a message to display to users when they login via SSH. A typical value would be `\"/etc/motd\"`.", - }, - "startup_script_behavior": { - Type: schema.TypeString, - Default: "non-blocking", - ForceNew: true, - Optional: true, - Description: "This option sets the behavior of the `startup_script`. When set to `\"blocking\"`, the `startup_script` must exit before the workspace is ready. When set to `\"non-blocking\"`, the `startup_script` may run in the background and the workspace will be ready immediately. Default is `\"non-blocking\"`, although `\"blocking\"` is recommended. This option is an alias for defining a `coder_script` resource with `start_blocks_login` set to `true` (blocking).", - ValidateFunc: validation.StringInSlice([]string{"blocking", "non-blocking"}, false), - }, - "metadata": { - Type: schema.TypeList, - Description: "Each `metadata` block defines a single item consisting of a key/value pair. This feature is in alpha and may break in future releases.", - ForceNew: true, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Description: "The key of this metadata item.", - ForceNew: true, - Required: true, - }, - "display_name": { - Type: schema.TypeString, - Description: "The user-facing name of this value.", - ForceNew: true, - Optional: true, - }, - "script": { - Type: schema.TypeString, - Description: "The script that retrieves the value of this metadata item.", - ForceNew: true, - Required: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "timeout": { - Type: schema.TypeInt, - Description: "The maximum time the command is allowed to run in seconds.", - ForceNew: true, - Optional: true, - }, - "interval": { - Type: schema.TypeInt, - Description: "The interval in seconds at which to refresh this metadata item. ", - ForceNew: true, - Required: true, - }, - "order": { - Type: schema.TypeInt, - Description: "The order determines the position of agent metadata in the UI presentation. The lowest order is shown first and metadata with equal order are sorted by key (ascending order).", - ForceNew: true, - Optional: true, - }, - }, - }, + Description: "A script to run on startup of the agent.", }, "display_apps": { - Type: schema.TypeSet, - Description: "The list of built-in apps to display in the agent bar.", - ForceNew: true, - Optional: true, - MaxItems: 1, - Computed: true, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "vscode": { Type: schema.TypeBool, - Description: "Display the VSCode Desktop app in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the VSCode app.", }, "vscode_insiders": { Type: schema.TypeBool, - Description: "Display the VSCode Insiders app in the agent bar.", - ForceNew: true, Optional: true, - Default: false, + Description: "Whether to display the VSCode Insiders app.", }, "web_terminal": { Type: schema.TypeBool, - Description: "Display the web terminal app in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the web terminal app.", }, - "port_forwarding_helper": { + "ssh_helper": { Type: schema.TypeBool, - Description: "Display the port-forwarding helper button in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the SSH helper app.", }, - "ssh_helper": { + "port_forwarding_helper": { Type: schema.TypeBool, - Description: "Display the SSH helper button in the agent bar.", - ForceNew: true, Optional: true, - Default: true, + Description: "Whether to display the port forwarding helper app.", }, }, }, }, - "order": { - Type: schema.TypeInt, - Description: "The order determines the position of agents in the UI presentation. The lowest order is shown first and agents with equal order are sorted by name (ascending order).", - ForceNew: true, - Optional: true, + "token": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The token to use to authenticate the agent.", }, - "resources_monitoring": { - Type: schema.TypeSet, - Description: "The resources monitoring configuration for this agent.", - ForceNew: true, - Optional: true, - MaxItems: 1, + "metadata": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "memory": { - Type: schema.TypeSet, - Description: "The memory monitoring configuration for this agent.", - ForceNew: true, + "startup_script_behavior": { + Type: schema.TypeString, Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "enabled": { - Type: schema.TypeBool, - Description: "Enable memory monitoring for this agent.", - ForceNew: true, - Required: true, - }, - "threshold": { - Type: schema.TypeInt, - Description: "The memory usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.", - ForceNew: true, - Required: true, - ValidateFunc: validation.IntBetween(0, 100), - }, - }, - }, + Default: "blocking", + Description: "The behavior of the startup script. Can be `blocking` or `non-blocking`.", + ValidateFunc: validation.StringInSlice([]string{ + "blocking", + "non-blocking", + }, false), }, - "volume": { - Type: schema.TypeSet, - Description: "The volumes monitoring configuration for this agent.", - ForceNew: true, + "startup_script_timeout": { + Type: schema.TypeInt, Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "path": { - Type: schema.TypeString, - Description: "The path of the volume to monitor.", - ForceNew: true, - Required: true, - ValidateDiagFunc: func(i interface{}, s cty.Path) diag.Diagnostics { - path, ok := i.(string) - if !ok { - return diag.Errorf("volume path must be a string") - } - if path == "" { - return diag.Errorf("volume path must not be empty") - } - - if !filepath.IsAbs(i.(string)) { - return diag.Errorf("volume path must be an absolute path") - } - - return nil - }, - }, - "enabled": { - Type: schema.TypeBool, - Description: "Enable volume monitoring for this agent.", - ForceNew: true, - Required: true, - }, - "threshold": { - Type: schema.TypeInt, - Description: "The volume usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.", - ForceNew: true, - Required: true, - ValidateFunc: validation.IntBetween(0, 100), - }, - }, - }, + Default: 300, + Description: "The timeout in seconds for the startup script.", }, }, }, }, - }, - CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, i any) error { - if rd.HasChange("metadata") { - keys := map[string]bool{} - metadata, ok := rd.Get("metadata").([]any) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata, expected []any", rd.Get("metadata")) - } - for _, t := range metadata { - obj, ok := t.(map[string]any) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata, expected map[string]any", t) - } - key, ok := obj["key"].(string) - if !ok { - return xerrors.Errorf("unexpected type %T for metadata key, expected string", obj["key"]) - } - if keys[key] { - return xerrors.Errorf("duplicate agent metadata key %q", key) - } - keys[key] = true - } - } - - if rd.HasChange("resources_monitoring") { - monitors, ok := rd.Get("resources_monitoring").(*schema.Set) - if !ok { - return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", rd.Get("resources_monitoring.0.volume")) - } - - monitor := monitors.List()[0].(map[string]any) - - volumes, ok := monitor["volume"].(*schema.Set) - if !ok { - return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", monitor["volume"]) - } - - paths := map[string]bool{} - for _, volume := range volumes.List() { - obj, ok := volume.(map[string]any) - if !ok { - return xerrors.Errorf("unexpected type %T for volume, expected map[string]any", volume) - } - - // print path for debug purpose - - path, ok := obj["path"].(string) - if !ok { - return xerrors.Errorf("unexpected type %T for volume path, expected string", obj["path"]) - } - if paths[path] { - return xerrors.Errorf("duplicate volume path %q", path) - } - paths[path] = true - } - } - - return nil - }, - } -} - -func agentInstanceResource() *schema.Resource { - return &schema.Resource{ - Description: "Use this resource to associate an instance ID with an agent for zero-trust " + - "authentication. This association is done automatically for `\"google_compute_instance\"`, " + - "`\"aws_instance\"`, `\"azurerm_linux_virtual_machine\"`, and " + - "`\"azurerm_windows_virtual_machine\"` resources.", - CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - resourceData.SetId(uuid.NewString()) - return nil - }, - ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "agent_id": { - Type: schema.TypeString, - Description: "The `id` property of a `coder_agent` resource to associate with.", - ForceNew: true, - Required: true, + "connection_timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 10, + Description: "The timeout in seconds for the agent to connect to the Coder deployment.", }, - "instance_id": { - ForceNew: true, - Required: true, - Description: "The instance identifier of a provisioned resource.", - Type: schema.TypeString, + "login_before_ready": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether to allow logging in to the agent before it is ready.", }, }, } } -// updateInitScript fetches parameters from a "coder_agent" to produce the -// agent script from environment variables. func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - config, valid := i.(config) - if !valid { - return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String()) - } - auth, valid := resourceData.Get("auth").(string) - if !valid { - return diag.Errorf("auth was unexpected type %q", reflect.TypeOf(resourceData.Get("auth"))) - } - operatingSystem, valid := resourceData.Get("os").(string) - if !valid { - return diag.Errorf("os was unexpected type %q", reflect.TypeOf(resourceData.Get("os"))) + p := i.(*provider) + + arch := resourceData.Get("arch").(string) + os := resourceData.Get("os").(string) + dir := resourceData.Get("dir").(string) + startupScript := resourceData.Get("startup_script").(string) + displayApps := resourceData.Get("display_apps").([]interface{}) + token := resourceData.Get("token").(string) + metadata := resourceData.Get("metadata").([]interface{}) + connectionTimeout := resourceData.Get("connection_timeout").(int) + loginBeforeReady := resourceData.Get("login_before_ready").(bool) + + var displayAppsMap map[string]bool + if len(displayApps) > 0 { + displayAppsMap = displayApps[0].(map[string]bool) } - arch, valid := resourceData.Get("arch").(string) - if !valid { - return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch"))) + + var metadataMap map[string]interface{} + if len(.metadata) > 0 { + metadataMap = metadata[0].(map[string]interface{}) } - accessURL, err := config.URL.Parse("/") + + initScript, err := helpers.AgentInitScript( + p.client.URL(), + token, + arch, + os, + dir, + startupScript, + displayAppsMap, + metadataMap, + connectionTimeout, + loginBeforeReady, + ) if err != nil { - return diag.Errorf("parse access url: %s", err) - } - script := helpers.OptionalEnv(fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", operatingSystem, arch)) - if script != "" { - script = strings.ReplaceAll(script, "${ACCESS_URL}", accessURL.String()) - script = strings.ReplaceAll(script, "${AUTH_TYPE}", auth) + return diag.FromErr(err) } - err = resourceData.Set("init_script", script) + + err = resourceData.Set("init_script", initScript) if err != nil { return diag.FromErr(err) } + return nil } func agentAuthToken(ctx context.Context, agentID string) string { - existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) - if existingToken == "" { - // Most of the time, we will generate a new token for the agent. - // In the case of a prebuilt workspace being claimed, we will override with - // an existing token provided below. - token := uuid.NewString() - return token - } - - // An existing token was provided for this agent. That means that this - // is a prebuilt workspace in the process of being claimed. - // We should reuse the token. - tflog.Info(ctx, "using provided agent token for prebuild", map[string]interface{}{ - "agent_id": agentID, - }) - return existingToken -} - -// RunningAgentTokenEnvironmentVariable returns the name of an environment variable -// that contains the token to use for the running agent. This is used for prebuilds, -// where we want to reuse the same token for the next iteration of a workspace agent -// before and after the workspace was claimed by a user. -// -// By reusing an existing token, we can avoid the need to change a value that may have been -// used immutably. Thus, allowing us to avoid reprovisioning resources that may take a long time -// to replace. -// -// agentID is unused for now, but will be used as soon as we support multiple agents. -func RunningAgentTokenEnvironmentVariable(agentID string) string { - sum := sha256.Sum256([]byte(agentID)) - return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN_" + hex.EncodeToString(sum[:]) + // This is a bit of a hack to get a stable token for the agent. + // We use the agent ID as the salt for the token, so that the token + // is the same for the same agent. + // This is not a security risk, as the token is only used to + // authenticate the agent to the Coder deployment. + // The token is not used to authenticate the user to the agent. + // The user is authenticated to the agent using their Coder session token. + // The agent token is only used to identify the agent to the Coder + // deployment. + // The agent token is not sensitive, as it does not grant any + // permissions to the user. + // The agent token is only used to identify the agent to the Coder + // deployment. + // The agent token is not sensitive, as it does not grant any + // permissions to the user. + h := sha256.New() + h.Write([]byte(agentID)) + return hex.EncodeToString(h.Sum(nil)) }