From 5418ed7084b46f0d2bfd1db8bc8522004b5ba07c Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:02:00 +0000 Subject: [PATCH 01/10] feat: allow presets to define prebuilds --- README.md | 2 +- provider/workspace.go | 22 +++++++++++++++++++ provider/workspace_preset.go | 35 ++++++++++++++++++++++++++++-- provider/workspace_preset_test.go | 36 +++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b8ee884..f055961 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ to setup your local Terraform to use your local version rather than the registry } ``` 2. Run `terraform init` and observe a warning like `Warning: Provider development overrides are in effect` -4. Run `go build -o terraform-provider-coder` to build the provider binary, which Terraform will try locate and execute +4. Run `make build` to build the provider binary, which Terraform will try locate and execute 5. All local Terraform runs will now use your local provider! 6. _**NOTE**: we vendor in this provider into `github.com/coder/coder`, so if you're testing with a local clone then you should also run `go mod edit -replace github.com/coder/terraform-provider-coder=/path/to/terraform-provider-coder` in your clone._ diff --git a/provider/workspace.go b/provider/workspace.go index fde742b..19da0d0 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -27,6 +27,14 @@ func workspaceDataSource() *schema.Resource { } _ = rd.Set("start_count", count) + prebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) + prebuildCount := 0 + if prebuild == "true" { + prebuildCount = 1 + _ = rd.Set("is_prebuild", true) + } + _ = rd.Set("prebuild_count", prebuildCount) + name := helpers.OptionalEnvOrDefault("CODER_WORKSPACE_NAME", "default") rd.Set("name", name) @@ -88,6 +96,16 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "A computed count based on `transition` state. If `start`, count will equal 1.", }, + "prebuild_count": { + Type: schema.TypeInt, + Computed: true, + Description: "TODO", + }, + "is_prebuild": { + Type: schema.TypeBool, + Computed: true, + Description: "TODO", + }, "transition": { Type: schema.TypeString, Computed: true, @@ -121,3 +139,7 @@ func workspaceDataSource() *schema.Resource { }, } } + +func IsPrebuildEnvironmentVariable() string { + return "CODER_WORKSPACE_IS_PREBUILD" +} diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index cd56c98..eafc39e 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -10,8 +10,13 @@ import ( ) type WorkspacePreset struct { - Name string `mapstructure:"name"` - Parameters map[string]string `mapstructure:"parameters"` + Name string `mapstructure:"name"` + Parameters map[string]string `mapstructure:"parameters"` + Prebuild []WorkspacePrebuild `mapstructure:"prebuilds"` +} + +type WorkspacePrebuild struct { + Instances int `mapstructure:"instances"` } func workspacePresetDataSource() *schema.Resource { @@ -24,9 +29,19 @@ func workspacePresetDataSource() *schema.Resource { err := mapstructure.Decode(struct { Name interface{} Parameters interface{} + Prebuilds []struct { + Instances interface{} + } }{ Name: rd.Get("name"), Parameters: rd.Get("parameters"), + Prebuilds: []struct { + Instances interface{} + }{ + { + Instances: rd.Get("prebuilds.0.instances"), + }, + }, }, &preset) if err != nil { return diag.Errorf("decode workspace preset: %s", err) @@ -65,6 +80,22 @@ func workspacePresetDataSource() *schema.Resource { ValidateFunc: validation.StringIsNotEmpty, }, }, + "prebuilds": { + Type: schema.TypeSet, + Description: "Prebuilds of the workspace preset.", + Optional: true, + MaxItems: 1, // TODO: is this always true? More than 1 prebuilds config per preset? + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "instances": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(0), + }, + }, + }, + }, }, } } diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go index 876e204..8f0d31e 100644 --- a/provider/workspace_preset_test.go +++ b/provider/workspace_preset_test.go @@ -108,6 +108,42 @@ func TestWorkspacePreset(t *testing.T) { // So we test it here to make sure we don't regress. ExpectError: regexp.MustCompile("Inappropriate value for attribute \"parameters\": map of string required"), }, + { + Name: "Prebuilds is set, but not its required fields", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + prebuilds {} + }`, + ExpectError: regexp.MustCompile("The argument \"instances\" is required, but no definition was found."), + }, + { + Name: "Prebuilds is set, and so are its required fields", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + prebuilds { + instances = 1 + } + }`, + ExpectError: nil, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"] + require.NotNil(t, resource) + attrs := resource.Primary.Attributes + require.Equal(t, attrs["name"], "preset_1") + require.Equal(t, attrs["prebuilds.0.instances"], "1") + return nil + }, + }, } for _, testcase := range testcases { From af250375660c21ab9c2ecfa4001834b44e6db46f Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:06:34 +0000 Subject: [PATCH 02/10] document prebuild parameters --- provider/workspace.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/provider/workspace.go b/provider/workspace.go index 19da0d0..30e7ad8 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -91,20 +91,15 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "The access port of the Coder deployment provisioning this workspace.", }, - "start_count": { - Type: schema.TypeInt, - Computed: true, - Description: "A computed count based on `transition` state. If `start`, count will equal 1.", - }, "prebuild_count": { Type: schema.TypeInt, Computed: true, - Description: "TODO", + Description: "A computed count, equal to 1 if the workspace was prebuilt.", }, - "is_prebuild": { - Type: schema.TypeBool, + "start_count": { + Type: schema.TypeInt, Computed: true, - Description: "TODO", + Description: "A computed count based on `transition` state. If `start`, count will equal 1.", }, "transition": { Type: schema.TypeString, @@ -116,6 +111,11 @@ func workspaceDataSource() *schema.Resource { Computed: true, Description: "UUID of the workspace.", }, + "is_prebuild": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the workspace is a prebuild.", + }, "name": { Type: schema.TypeString, Computed: true, From 56d1ab72a05ac67eaf2409d8826868234ffa1109 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:07:59 +0000 Subject: [PATCH 03/10] remove todo --- provider/workspace_preset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index eafc39e..b29ecba 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -84,7 +84,7 @@ func workspacePresetDataSource() *schema.Resource { Type: schema.TypeSet, Description: "Prebuilds of the workspace preset.", Optional: true, - MaxItems: 1, // TODO: is this always true? More than 1 prebuilds config per preset? + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instances": { From c8c510180877b66ca4098f5eeaeaf751343de3e0 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:09:44 +0000 Subject: [PATCH 04/10] make gen --- docs/data-sources/workspace.md | 2 ++ docs/data-sources/workspace_preset.md | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/data-sources/workspace.md b/docs/data-sources/workspace.md index 26396ba..30deee2 100644 --- a/docs/data-sources/workspace.md +++ b/docs/data-sources/workspace.md @@ -69,7 +69,9 @@ resource "docker_container" "workspace" { - `access_port` (Number) The access port of the Coder deployment provisioning this workspace. - `access_url` (String) The access URL of the Coder deployment provisioning this workspace. - `id` (String) UUID of the workspace. +- `is_prebuild` (Boolean) Whether the workspace is a prebuild. - `name` (String) Name of the workspace. +- `prebuild_count` (Number) A computed count, equal to 1 if the workspace was prebuilt. - `start_count` (Number) A computed count based on `transition` state. If `start`, count will equal 1. - `template_id` (String) ID of the workspace's template. - `template_name` (String) Name of the workspace's template. diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 28f90fa..9c393fa 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -37,6 +37,17 @@ data "coder_workspace_preset" "example" { - `name` (String) Name of the workspace preset. - `parameters` (Map of String) Parameters of the workspace preset. +### Optional + +- `prebuilds` (Block Set, Max: 1) Prebuilds of the workspace preset. (see [below for nested schema](#nestedblock--prebuilds)) + ### Read-Only - `id` (String) ID of the workspace preset. + + +### Nested Schema for `prebuilds` + +Required: + +- `instances` (Number) From 0a50b31f2add793ceb9a0b4870f063d7d9e030c0 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 3 Apr 2025 12:17:10 +0000 Subject: [PATCH 05/10] feat: reuse agent tokens when a prebuilt agent reinitializes --- provider/agent.go | 55 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/provider/agent.go b/provider/agent.go index 3ddae23..9232db7 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -3,10 +3,13 @@ package provider import ( "context" "fmt" + "os" "path/filepath" "reflect" "strings" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -22,10 +25,54 @@ func agentResource() *schema.Resource { SchemaVersion: 1, Description: "Use this resource to associate an agent.", - CreateContext: func(_ context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { + CreateContext: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { // This should be a real authentication token! resourceData.SetId(uuid.NewString()) - err := resourceData.Set("token", uuid.NewString()) + + // CODER_RUNNING_WORKSPACE_AGENT_TOKEN is *only* used for prebuilds. We pass it down when we want to rebuild a prebuilt workspace + // but not generate a new agent token. The provisionerdserver will retrieve this token and push it down to + // here where it will be reused. + // Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) + // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus + // obviating the whole point of the prebuild. + // + // The default path is for a new token to be generated on each new resource creation. + // TODO: add logging when the running token is actually used. + var token string + + isPrebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true" + if !isPrebuild { + token = os.Getenv(RunningAgentTokenEnvironmentVariable()) + } + + allEnv := make(map[string]interface{}) + for _, v := range os.Environ() { + split := strings.Split(v, "=") + var key, val string + if len(split) > 0 { + key = split[0] + } + if len(split) > 1 { + val = split[1] + } + + allEnv[key] = val + } + + allEnv["is_prebuild"] = fmt.Sprintf("%v", isPrebuild) + + if token == "" { + token = uuid.NewString() + if !isPrebuild { + tflog.Warn(ctx, "NOT USING EXISTING AGENT TOKEN", allEnv) + } + } else { + if !isPrebuild { + tflog.Info(ctx, "IS USING EXISTING AGENT TOKEN", allEnv) + } + } + + err := resourceData.Set("token", token) if err != nil { return diag.FromErr(err) } @@ -469,3 +516,7 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia } return nil } + +func RunningAgentTokenEnvironmentVariable() string { + return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN" +} From e46f69a01efe9047ae66d9c46757068811833e18 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Thu, 17 Apr 2025 11:20:58 +0000 Subject: [PATCH 06/10] WIP: get agent.go ready to be merged with support for prebuilds --- provider/agent.go | 81 ++++++++++++++++++++----------------------- provider/workspace.go | 6 ++++ 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/provider/agent.go b/provider/agent.go index 9232db7..4df312e 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -2,16 +2,16 @@ package provider import ( "context" + "crypto/sha256" + "encoding/hex" "fmt" - "os" "path/filepath" "reflect" "strings" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -26,50 +26,34 @@ func agentResource() *schema.Resource { Description: "Use this resource to associate an agent.", CreateContext: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - // This should be a real authentication token! - resourceData.SetId(uuid.NewString()) + agentID := uuid.NewString() + resourceData.SetId(agentID) - // CODER_RUNNING_WORKSPACE_AGENT_TOKEN is *only* used for prebuilds. We pass it down when we want to rebuild a prebuilt workspace - // but not generate a new agent token. The provisionerdserver will retrieve this token and push it down to - // here where it will be reused. - // Context: the agent token is often used in immutable attributes of workspace resource (e.g. VM/container) - // to initialize the agent, so if that value changes it will necessitate a replacement of that resource, thus - // obviating the whole point of the prebuild. - // - // The default path is for a new token to be generated on each new resource creation. - // TODO: add logging when the running token is actually used. - var token string + // 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() + // If isPrebuild is true, then this workspace was built by the prebuilds system. + // This does not determine whether the workspace has been claimed by a user. + // At this point, it may or may not have been claimed. isPrebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true" - if !isPrebuild { - token = os.Getenv(RunningAgentTokenEnvironmentVariable()) - } - - allEnv := make(map[string]interface{}) - for _, v := range os.Environ() { - split := strings.Split(v, "=") - var key, val string - if len(split) > 0 { - key = split[0] - } - if len(split) > 1 { - val = split[1] - } - - allEnv[key] = val + // existingToken should only have been set if isPrebuild is true, because we only + // reuse the token when a prebuilt workspace is being claimed. + existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) + logFields := map[string]interface{}{ + "agent_id": agentID, + "is_prebuild": isPrebuild, + "token_provided": existingToken != "", } - - allEnv["is_prebuild"] = fmt.Sprintf("%v", isPrebuild) - - if token == "" { - token = uuid.NewString() - if !isPrebuild { - tflog.Warn(ctx, "NOT USING EXISTING AGENT TOKEN", allEnv) - } + if isPrebuild && existingToken != "" { + // check if a token was already generated for this agent. + // If so, this workspace is in the process of being claimed + // and we should reuse the token. If not, we use a new token as usual. + tflog.Info(ctx, "using provided agent token for prebuild", logFields) + token = existingToken } else { - if !isPrebuild { - tflog.Info(ctx, "IS USING EXISTING AGENT TOKEN", allEnv) - } + tflog.Info(ctx, "using a new agent token", logFields) } err := resourceData.Set("token", token) @@ -517,6 +501,15 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia return nil } -func RunningAgentTokenEnvironmentVariable() string { - return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN" +// RunningAgentTokenEnvironmentVariable returns the name of the environment variable +// that contains the token 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. +// +// agentID is unused for now, but will be used as soon as we support multiple agents. +func RunningAgentTokenEnvironmentVariable(agentID string) string { + agentID = "" // remove this once we need to support multiple agents per prebuilt workspace. + + sum := sha256.Sum256([]byte(agentID)) + return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN_" + hex.EncodeToString(sum[:]) } diff --git a/provider/workspace.go b/provider/workspace.go index 5ddd3ee..ee318ba 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -140,6 +140,12 @@ func workspaceDataSource() *schema.Resource { } } +// IsPrebuildEnvironmentVariable returns the name of the environment +// variable that indicates whether the workspace was prebuilt. The value of +// this environment variable should be set to "true" if the workspace is prebuilt. +// Any other values, including "false" and "" will be interpreted to mean that the +// workspace is not prebuilt. If the workspace is prebuilt, it may or may not yet +// have been claimed by a user. func IsPrebuildEnvironmentVariable() string { return "CODER_WORKSPACE_IS_PREBUILD" } From 0f5842aac0c61522a5febc67614995d7fb9ae7f4 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 23 Apr 2025 12:06:01 +0000 Subject: [PATCH 07/10] fix: ensure the agent token is reused for prebuilds --- provider/agent.go | 70 ++++++++++++++++++++++--------------------- provider/workspace.go | 16 ++++++---- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/provider/agent.go b/provider/agent.go index 4df312e..95b94ae 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -29,33 +29,7 @@ func agentResource() *schema.Resource { agentID := uuid.NewString() resourceData.SetId(agentID) - // 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() - - // If isPrebuild is true, then this workspace was built by the prebuilds system. - // This does not determine whether the workspace has been claimed by a user. - // At this point, it may or may not have been claimed. - isPrebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true" - // existingToken should only have been set if isPrebuild is true, because we only - // reuse the token when a prebuilt workspace is being claimed. - existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) - logFields := map[string]interface{}{ - "agent_id": agentID, - "is_prebuild": isPrebuild, - "token_provided": existingToken != "", - } - if isPrebuild && existingToken != "" { - // check if a token was already generated for this agent. - // If so, this workspace is in the process of being claimed - // and we should reuse the token. If not, we use a new token as usual. - tflog.Info(ctx, "using provided agent token for prebuild", logFields) - token = existingToken - } else { - tflog.Info(ctx, "using a new agent token", logFields) - } - + token := agentAuthToken(ctx, "") err := resourceData.Set("token", token) if err != nil { return diag.FromErr(err) @@ -79,10 +53,12 @@ func agentResource() *schema.Resource { return updateInitScript(resourceData, i) }, ReadWithoutTimeout: func(ctx context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - err := resourceData.Set("token", uuid.NewString()) + token := agentAuthToken(ctx, "") + err := resourceData.Set("token", token) if err != nil { return diag.FromErr(err) } + if _, ok := resourceData.GetOk("display_apps"); !ok { err = resourceData.Set("display_apps", []interface{}{ map[string]bool{ @@ -501,15 +477,41 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia return nil } -// RunningAgentTokenEnvironmentVariable returns the name of the environment variable -// that contains the token 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. +func agentAuthToken(ctx context.Context, agentID string) string { + // 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() + + existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) + logFields := map[string]interface{}{ + "agent_id": agentID, + "token_provided": existingToken != "", + } + if existingToken != "" { + // 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", logFields) + token = existingToken + } else { + tflog.Info(ctx, "using a new agent token", logFields) + } + + return token +} + +// 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 { - agentID = "" // remove this once we need to support multiple agents per prebuilt workspace. - sum := sha256.Sum256([]byte(agentID)) return "CODER_RUNNING_WORKSPACE_AGENT_TOKEN_" + hex.EncodeToString(sum[:]) } diff --git a/provider/workspace.go b/provider/workspace.go index ee318ba..54b2a78 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -27,9 +27,9 @@ func workspaceDataSource() *schema.Resource { } _ = rd.Set("start_count", count) - prebuild := helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) + isPrebuild := isPrebuiltWorkspace(c) prebuildCount := 0 - if prebuild == "true" { + if isPrebuild { prebuildCount = 1 _ = rd.Set("is_prebuild", true) } @@ -140,12 +140,16 @@ func workspaceDataSource() *schema.Resource { } } +func isPrebuiltWorkspace(ctx context.Context) bool { + return helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true" +} + // IsPrebuildEnvironmentVariable returns the name of the environment -// variable that indicates whether the workspace was prebuilt. The value of -// this environment variable should be set to "true" if the workspace is prebuilt. +// variable that indicates whether the workspace is an unclaimed prebuilt workspace. +// The value of this environment variable should be set to "true" if the workspace +// is prebuilt and it has not yet been claimed by a user. // Any other values, including "false" and "" will be interpreted to mean that the -// workspace is not prebuilt. If the workspace is prebuilt, it may or may not yet -// have been claimed by a user. +// workspace is not prebuilt, or was prebuilt but has since been claimed by a user. func IsPrebuildEnvironmentVariable() string { return "CODER_WORKSPACE_IS_PREBUILD" } From f0e699a35bb344e7612d68c3d70b97193dd99929 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 23 Apr 2025 12:14:37 +0000 Subject: [PATCH 08/10] lint and make gen --- docs/data-sources/workspace_preset.md | 4 ---- provider/workspace.go | 24 ++++++++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 2ba8915..edd61f1 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -41,10 +41,6 @@ data "coder_workspace_preset" "example" { - `parameters` (Map of String) Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version. - `prebuilds` (Block Set, Max: 1) Prebuilt workspace configuration related to this workspace preset. Coder will build and maintain workspaces in reserve based on this configuration. When a user creates a new workspace using a preset, they will be assigned a prebuilt workspace, instead of waiting for a new workspace to build. (see [below for nested schema](#nestedblock--prebuilds)) -### Optional - -- `prebuilds` (Block Set, Max: 1) Prebuilds of the workspace preset. (see [below for nested schema](#nestedblock--prebuilds)) - ### Read-Only - `id` (String) The preset ID is automatically generated and may change between runs. It is recommended to use the `name` attribute to identify the preset. diff --git a/provider/workspace.go b/provider/workspace.go index 54b2a78..b44bb30 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -27,7 +27,7 @@ func workspaceDataSource() *schema.Resource { } _ = rd.Set("start_count", count) - isPrebuild := isPrebuiltWorkspace(c) + isPrebuild := isPrebuiltWorkspace() prebuildCount := 0 if isPrebuild { prebuildCount = 1 @@ -140,16 +140,24 @@ func workspaceDataSource() *schema.Resource { } } -func isPrebuiltWorkspace(ctx context.Context) bool { +// isPrebuiltWorkspace returns true if the workspace is an unclaimed prebuilt workspace. +func isPrebuiltWorkspace() bool { return helpers.OptionalEnv(IsPrebuildEnvironmentVariable()) == "true" } -// IsPrebuildEnvironmentVariable returns the name of the environment -// variable that indicates whether the workspace is an unclaimed prebuilt workspace. -// The value of this environment variable should be set to "true" if the workspace -// is prebuilt and it has not yet been claimed by a user. -// Any other values, including "false" and "" will be interpreted to mean that the -// workspace is not prebuilt, or was prebuilt but has since been claimed by a user. +// IsPrebuildEnvironmentVariable returns the name of the environment variable that +// indicates whether the workspace is an unclaimed prebuilt workspace. +// +// Knowing whether the workspace is an unclaimed prebuilt workspace allows template +// authors to conditionally execute code in the template based on whether the workspace +// has been assigned to a user or not. This allows identity specific configuration to +// be applied only after the workspace is claimed, while the rest of the workspace can +// be pre-configured. +// +// The value of this environment variable should be set to "true" if the workspace is prebuilt +// and it has not yet been claimed by a user. Any other values, including "false" +// and "" will be interpreted to mean that the workspace is not prebuilt, or was +// prebuilt but has since been claimed by a user. func IsPrebuildEnvironmentVariable() string { return "CODER_WORKSPACE_IS_PREBUILD" } From e51bf1c18a0ae5e69dde0c64703a0f481633c7eb Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 23 Apr 2025 12:46:09 +0000 Subject: [PATCH 09/10] simplify function --- go.mod | 2 +- provider/agent.go | 31 +++++++++++++------------------ provider/workspace.go | 10 +++++----- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 848423f..2d3db5d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/docker/docker v26.1.5+incompatible github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 + github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 github.com/masterminds/semver v1.5.0 github.com/mitchellh/mapstructure v1.5.0 @@ -50,7 +51,6 @@ require ( github.com/hashicorp/terraform-exec v0.22.0 // indirect github.com/hashicorp/terraform-json v0.24.0 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/provider/agent.go b/provider/agent.go index 95b94ae..ad26403 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -478,27 +478,22 @@ func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Dia } func agentAuthToken(ctx context.Context, agentID string) string { - // 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() - existingToken := helpers.OptionalEnv(RunningAgentTokenEnvironmentVariable(agentID)) - logFields := map[string]interface{}{ - "agent_id": agentID, - "token_provided": existingToken != "", - } - if existingToken != "" { - // 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", logFields) - token = existingToken - } else { - tflog.Info(ctx, "using a new agent token", logFields) + 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 } - 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 diff --git a/provider/workspace.go b/provider/workspace.go index b44bb30..c477fad 100644 --- a/provider/workspace.go +++ b/provider/workspace.go @@ -27,13 +27,13 @@ func workspaceDataSource() *schema.Resource { } _ = rd.Set("start_count", count) - isPrebuild := isPrebuiltWorkspace() - prebuildCount := 0 - if isPrebuild { - prebuildCount = 1 + if isPrebuiltWorkspace() { + _ = rd.Set("prebuild_count", 1) _ = rd.Set("is_prebuild", true) + } else { + _ = rd.Set("prebuild_count", 0) + _ = rd.Set("is_prebuild", false) } - _ = rd.Set("prebuild_count", prebuildCount) name := helpers.OptionalEnvOrDefault("CODER_WORKSPACE_NAME", "default") rd.Set("name", name) From eff062bc6c6285b1a47cbe7f00182abea5571cf5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 23 Apr 2025 08:18:08 -0500 Subject: [PATCH 10/10] test: rbac role test assertion to handle site wide roles Site wide roles have empty string org_ids --- integration/integration_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 9803aa4..a501963 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -181,7 +181,8 @@ func TestIntegration(t *testing.T) { "workspace_owner.ssh_private_key": `(?s)^.+?BEGIN OPENSSH PRIVATE KEY.+?END OPENSSH PRIVATE KEY.+?$`, "workspace_owner.ssh_public_key": `(?s)^ssh-ed25519.+$`, "workspace_owner.login_type": `password`, - "workspace_owner.rbac_roles": `(?is)\[(\{"name":"[a-z0-9-:]+","org_id":"[a-f0-9-]+"\},?)+\]`, + // org_id will either be a uuid or an empty string for site wide roles. + "workspace_owner.rbac_roles": `(?is)\[(\{"name":"[a-z0-9-:]+","org_id":"[a-f0-9-]*"\},?)+\]`, }, }, {