From 79459466a45226beee35b29976a44a2952a28cc2 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 3 Feb 2025 09:06:16 +0000 Subject: [PATCH 1/3] WIP Signed-off-by: Danny Kopping # Conflicts: # provider/agent.go --- README.md | 2 +- provider/agent.go | 55 +++++++++++++++++++++++++++++++++++++++++-- provider/workspace.go | 22 +++++++++++++++++ 3 files changed, 76 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/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" +} 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" +} From b7e9b4ff1bec3be399f590bed0dfd380096e15ce Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Thu, 6 Feb 2025 08:37:00 +0000 Subject: [PATCH 2/3] WIP Signed-off-by: Danny Kopping --- provider/workspace_preset.go | 36 ++++++++++++++++++++++++++++--- provider/workspace_preset_test.go | 36 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index cd56c98..ea037e3 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -2,7 +2,6 @@ package provider import ( "context" - "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" @@ -10,8 +9,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 +28,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 +79,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(1), + }, + }, + }, + }, }, } } 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 12b724dc9393c30731d1c63edac4584f551ec7ca Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 25 Feb 2025 09:00:06 +0000 Subject: [PATCH 3/3] Allow 0 instances Signed-off-by: Danny Kopping --- provider/workspace_preset.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index ea037e3..eafc39e 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -2,6 +2,7 @@ package provider import ( "context" + "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" @@ -90,7 +91,7 @@ func workspacePresetDataSource() *schema.Resource { Type: schema.TypeInt, Required: true, ForceNew: true, - ValidateFunc: validation.IntAtLeast(1), + ValidateFunc: validation.IntAtLeast(0), }, }, },