From c8a8a57781ea5a8fe9934cbd2f81d92875dc0a92 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 28 Sep 2022 23:29:17 +0000 Subject: [PATCH] chore: Split up the provider file It was getting huge! --- internal/provider/agent.go | 161 +++++++++ internal/provider/agent_test.go | 100 ++++++ internal/provider/app.go | 110 ++++++ internal/provider/app_test.go | 65 ++++ internal/provider/metadata.go | 103 ++++++ internal/provider/metadata_test.go | 127 +++++++ internal/provider/provider.go | 473 +------------------------- internal/provider/provider_test.go | 354 ------------------- internal/provider/provisioner.go | 35 ++ internal/provider/provisioner_test.go | 39 +++ internal/provider/workspace.go | 126 +++++++ internal/provider/workspace_test.go | 75 ++++ 12 files changed, 947 insertions(+), 821 deletions(-) create mode 100644 internal/provider/agent.go create mode 100644 internal/provider/agent_test.go create mode 100644 internal/provider/app.go create mode 100644 internal/provider/app_test.go create mode 100644 internal/provider/metadata.go create mode 100644 internal/provider/metadata_test.go create mode 100644 internal/provider/provisioner.go create mode 100644 internal/provider/provisioner_test.go create mode 100644 internal/provider/workspace.go create mode 100644 internal/provider/workspace_test.go diff --git a/internal/provider/agent.go b/internal/provider/agent.go new file mode 100644 index 00000000..054ca8c6 --- /dev/null +++ b/internal/provider/agent.go @@ -0,0 +1,161 @@ +package provider + +import ( + "context" + "fmt" + "os" + "reflect" + "strings" + + "github.com/google/uuid" + "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" +) + +func agentResource() *schema.Resource { + return &schema.Resource{ + Description: "Use this resource to associate an agent.", + CreateContext: func(c 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()) + if err != nil { + return diag.FromErr(err) + } + return updateInitScript(resourceData, i) + }, + ReadWithoutTimeout: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { + err := resourceData.Set("token", uuid.NewString()) + if err != nil { + return diag.FromErr(err) + } + return updateInitScript(resourceData, i) + }, + DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "init_script": { + Type: schema.TypeString, + Computed: true, + Description: "Run this script on startup of an instance to initialize the agent.", + }, + "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, + }, + "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.", + Type: schema.TypeString, + Optional: 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, + }, + }, + } +} + +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, + }, + "instance_id": { + ForceNew: true, + Required: true, + Description: `The instance identifier of a provisioned resource.`, + Type: schema.TypeString, + }, + }, + } +} + +// 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"))) + } + arch, valid := resourceData.Get("arch").(string) + if !valid { + return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch"))) + } + accessURL, err := config.URL.Parse("/") + if err != nil { + return diag.Errorf("parse access url: %s", err) + } + script := os.Getenv(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) + } + err = resourceData.Set("init_script", script) + if err != nil { + return diag.FromErr(err) + } + return nil +} diff --git a/internal/provider/agent_test.go b/internal/provider/agent_test.go new file mode 100644 index 00000000..79dc8790 --- /dev/null +++ b/internal/provider/agent_test.go @@ -0,0 +1,100 @@ +package provider_test + +import ( + "testing" + + "github.com/coder/terraform-provider-coder/internal/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestAgent(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + url = "https://example.com" + } + resource "coder_agent" "new" { + os = "linux" + arch = "amd64" + auth = "aws-instance-identity" + dir = "/tmp" + env = { + hi = "test" + } + startup_script = "echo test" + } + `, + 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["coder_agent.new"] + require.NotNil(t, resource) + for _, key := range []string{ + "token", + "os", + "arch", + "auth", + "dir", + "env.hi", + "startup_script", + } { + value := resource.Primary.Attributes[key] + t.Logf("%q = %q", key, value) + require.NotNil(t, value) + require.Greater(t, len(value), 0) + } + return nil + }, + }}, + }) +} + +func TestAgentInstance(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + url = "https://example.com" + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + resource "coder_agent_instance" "new" { + agent_id = coder_agent.dev.id + instance_id = "hello" + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 2) + resource := state.Modules[0].Resources["coder_agent_instance.new"] + require.NotNil(t, resource) + for _, key := range []string{ + "agent_id", + "instance_id", + } { + value := resource.Primary.Attributes[key] + t.Logf("%q = %q", key, value) + require.NotNil(t, value) + require.Greater(t, len(value), 0) + } + return nil + }, + }}, + }) +} diff --git a/internal/provider/app.go b/internal/provider/app.go new file mode 100644 index 00000000..51e60803 --- /dev/null +++ b/internal/provider/app.go @@ -0,0 +1,110 @@ +package provider + +import ( + "context" + "net/url" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func appResource() *schema.Resource { + return &schema.Resource{ + Description: "Use this resource to define shortcuts to access applications in a workspace.", + 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(ctx 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, + }, + "command": { + Type: schema.TypeString, + Description: "A command to run in a terminal opening this app. In the web, " + + "this will open in a new tab. In the CLI, this will SSH and execute the command. " + + "Either \"command\" or \"url\" may be specified, but not both.", + ConflictsWith: []string{"url"}, + Optional: true, + ForceNew: true, + }, + "icon": { + Type: schema.TypeString, + Description: "A URL to an icon that will display in the dashboard. View built-in " + + "icons here: https://github.com/coder/coder/tree/main/site/static/icons. Use a " + + "built-in icon with `data.coder_workspace.me.access_url + \"/icons/\"`.", + ForceNew: true, + Optional: true, + ValidateFunc: func(i interface{}, s string) ([]string, []error) { + _, err := url.Parse(s) + if err != nil { + return nil, []error{err} + } + return nil, nil + }, + }, + "name": { + Type: schema.TypeString, + Description: "A display name to identify the app.", + ForceNew: true, + Optional: true, + }, + "relative_path": { + Type: schema.TypeBool, + Description: "Specifies whether the URL will be accessed via a relative " + + "path or wildcard. Use if wildcard routing is unavailable.", + ForceNew: true, + Optional: true, + ConflictsWith: []string{"command"}, + }, + "url": { + Type: schema.TypeString, + Description: "A URL to be proxied to from inside the workspace. " + + "Either \"command\" or \"url\" may be specified, but not both.", + ForceNew: true, + Optional: true, + ConflictsWith: []string{"command"}, + }, + "healthcheck": { + Type: schema.TypeSet, + Description: "HTTP health checking to determine the application readiness.", + ForceNew: true, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"command"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "url": { + Type: schema.TypeString, + Description: "HTTP address used determine the application readiness. A successful health check is a HTTP response code less than 500 returned before healthcheck.interval seconds.", + ForceNew: true, + Required: true, + }, + "interval": { + Type: schema.TypeInt, + Description: "Duration in seconds to wait between healthcheck requests.", + ForceNew: true, + Required: true, + }, + "threshold": { + Type: schema.TypeInt, + Description: "Number of consecutive heathcheck failures before returning an unhealthy status.", + ForceNew: true, + Required: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/provider/app_test.go b/internal/provider/app_test.go new file mode 100644 index 00000000..d56cf556 --- /dev/null +++ b/internal/provider/app_test.go @@ -0,0 +1,65 @@ +package provider_test + +import ( + "testing" + + "github.com/coder/terraform-provider-coder/internal/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestApp(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + resource "coder_app" "code-server" { + agent_id = coder_agent.dev.id + name = "code-server" + icon = "builtin:vim" + relative_path = true + url = "http://localhost:13337" + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 2) + resource := state.Modules[0].Resources["coder_app.code-server"] + require.NotNil(t, resource) + for _, key := range []string{ + "agent_id", + "name", + "icon", + "relative_path", + "url", + "healthcheck.0.url", + "healthcheck.0.interval", + "healthcheck.0.threshold", + } { + value := resource.Primary.Attributes[key] + t.Logf("%q = %q", key, value) + require.NotNil(t, value) + require.Greater(t, len(value), 0) + } + return nil + }, + }}, + }) +} diff --git a/internal/provider/metadata.go b/internal/provider/metadata.go new file mode 100644 index 00000000..907ab450 --- /dev/null +++ b/internal/provider/metadata.go @@ -0,0 +1,103 @@ +package provider + +import ( + "context" + "net/url" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func metadataResource() *schema.Resource { + return &schema.Resource{ + Description: "Use this resource to attach key/value pairs to a resource. They will be " + + "displayed in the Coder dashboard.", + CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { + resourceData.SetId(uuid.NewString()) + + items, err := populateIsNull(resourceData) + if err != nil { + return errorAsDiagnostics(err) + } + err = resourceData.Set("item", items) + if err != nil { + return errorAsDiagnostics(err) + } + + return nil + }, + ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { + return nil + }, + DeleteContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Description: "The \"id\" property of another resource that metadata should be attached to.", + ForceNew: true, + Required: true, + }, + "hide": { + Type: schema.TypeBool, + Description: "Hide the resource from the UI.", + ForceNew: true, + Optional: true, + }, + "icon": { + Type: schema.TypeString, + Description: "A URL to an icon that will display in the dashboard. View built-in " + + "icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a " + + "built-in icon with `data.coder_workspace.me.access_url + \"/icons/\"`.", + ForceNew: true, + Optional: true, + ValidateFunc: func(i interface{}, s string) ([]string, []error) { + _, err := url.Parse(s) + if err != nil { + return nil, []error{err} + } + return nil, nil + }, + }, + "item": { + Type: schema.TypeList, + Description: "Each \"item\" block defines a single metadata item consisting of a key/value pair.", + ForceNew: true, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Description: "The key of this metadata item.", + ForceNew: true, + Required: true, + }, + "value": { + Type: schema.TypeString, + Description: "The value of this metadata item.", + ForceNew: true, + Optional: true, + }, + "sensitive": { + Type: schema.TypeBool, + Description: "Set to \"true\" to for items such as API keys whose values should be " + + "hidden from view by default. Note that this does not prevent metadata from " + + "being retrieved using the API, so it is not suitable for secrets that should " + + "not be exposed to workspace users.", + ForceNew: true, + Optional: true, + Default: false, + }, + "is_null": { + Type: schema.TypeBool, + ForceNew: true, + Computed: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/provider/metadata_test.go b/internal/provider/metadata_test.go new file mode 100644 index 00000000..dbacf928 --- /dev/null +++ b/internal/provider/metadata_test.go @@ -0,0 +1,127 @@ +package provider_test + +import ( + "regexp" + "testing" + + "github.com/coder/terraform-provider-coder/internal/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestMetadata(t *testing.T) { + t.Parallel() + prov := provider.New() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": prov, + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + resource "coder_metadata" "agent" { + resource_id = coder_agent.dev.id + hide = true + icon = "/icons/storage.svg" + item { + key = "foo" + value = "bar" + } + item { + key = "secret" + value = "squirrel" + sensitive = true + } + item { + key = "implicit_null" + } + item { + key = "explicit_null" + value = null + } + item { + key = "empty" + value = "" + } + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 2) + agent := state.Modules[0].Resources["coder_agent.dev"] + require.NotNil(t, agent) + metadata := state.Modules[0].Resources["coder_metadata.agent"] + require.NotNil(t, metadata) + t.Logf("metadata attributes: %#v", metadata.Primary.Attributes) + for key, expected := range map[string]string{ + "resource_id": agent.Primary.Attributes["id"], + "hide": "true", + "icon": "/icons/storage.svg", + "item.#": "5", + "item.0.key": "foo", + "item.0.value": "bar", + "item.0.sensitive": "false", + "item.1.key": "secret", + "item.1.value": "squirrel", + "item.1.sensitive": "true", + "item.2.key": "implicit_null", + "item.2.is_null": "true", + "item.2.sensitive": "false", + "item.3.key": "explicit_null", + "item.3.is_null": "true", + "item.3.sensitive": "false", + "item.4.key": "empty", + "item.4.value": "", + "item.4.is_null": "false", + "item.4.sensitive": "false", + } { + require.Equal(t, expected, metadata.Primary.Attributes[key]) + } + return nil + }, + }}, + }) +} + +func TestMetadataDuplicateKeys(t *testing.T) { + t.Parallel() + prov := provider.New() + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": prov, + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + resource "coder_metadata" "agent" { + resource_id = coder_agent.dev.id + hide = true + icon = "/icons/storage.svg" + item { + key = "foo" + value = "bar" + } + item { + key = "foo" + value = "bar" + } + } + `, + ExpectError: regexp.MustCompile("duplicate metadata key"), + }}, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 472b84bb..adc0635a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,19 +2,13 @@ package provider import ( "context" - "fmt" "net/url" - "os" "reflect" - "runtime" - "strconv" "strings" - "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" "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" "golang.org/x/xerrors" ) @@ -67,473 +61,18 @@ func New() *schema.Provider { }, nil }, DataSourcesMap: map[string]*schema.Resource{ - "coder_workspace": { - Description: "Use this data source to get information for the active workspace build.", - ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - transition := os.Getenv("CODER_WORKSPACE_TRANSITION") - if transition == "" { - // Default to start! - transition = "start" - } - _ = rd.Set("transition", transition) - count := 0 - if transition == "start" { - count = 1 - } - _ = rd.Set("start_count", count) - - owner := os.Getenv("CODER_WORKSPACE_OWNER") - if owner == "" { - owner = "default" - } - _ = rd.Set("owner", owner) - - ownerEmail := os.Getenv("CODER_WORKSPACE_OWNER_EMAIL") - _ = rd.Set("owner_email", ownerEmail) - - ownerID := os.Getenv("CODER_WORKSPACE_OWNER_ID") - if ownerID == "" { - ownerID = uuid.Nil.String() - } - _ = rd.Set("owner_id", ownerID) - - name := os.Getenv("CODER_WORKSPACE_NAME") - if name == "" { - name = "default" - } - rd.Set("name", name) - - id := os.Getenv("CODER_WORKSPACE_ID") - if id == "" { - id = uuid.NewString() - } - rd.SetId(id) - - config, valid := i.(config) - if !valid { - return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String()) - } - rd.Set("access_url", config.URL.String()) - - rawPort := config.URL.Port() - if rawPort == "" { - rawPort = "80" - if config.URL.Scheme == "https" { - rawPort = "443" - } - } - port, err := strconv.Atoi(rawPort) - if err != nil { - return diag.Errorf("couldn't parse port %q", port) - } - rd.Set("access_port", port) - - return nil - }, - Schema: map[string]*schema.Schema{ - "access_url": { - Type: schema.TypeString, - Computed: true, - Description: "The access URL of the Coder deployment provisioning this workspace.", - }, - "access_port": { - Type: schema.TypeInt, - 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.`, - }, - "transition": { - Type: schema.TypeString, - Computed: true, - Description: `Either "start" or "stop". Use this to start/stop resources with "count".`, - }, - "owner": { - Type: schema.TypeString, - Computed: true, - Description: "Username of the workspace owner.", - }, - "owner_email": { - Type: schema.TypeString, - Computed: true, - Description: "Email address of the workspace owner.", - }, - "owner_id": { - Type: schema.TypeString, - Computed: true, - Description: "UUID of the workspace owner.", - }, - "id": { - Type: schema.TypeString, - Computed: true, - Description: "UUID of the workspace.", - }, - "name": { - Type: schema.TypeString, - Computed: true, - Description: "Name of the workspace.", - }, - }, - }, - "coder_provisioner": { - Description: "Use this data source to get information about the Coder provisioner.", - ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - rd.SetId(uuid.NewString()) - rd.Set("os", runtime.GOOS) - rd.Set("arch", runtime.GOARCH) - - return nil - }, - Schema: map[string]*schema.Schema{ - "os": { - Type: schema.TypeString, - Computed: true, - Description: "The operating system of the host. This exposes `runtime.GOOS` (see https://pkg.go.dev/runtime#pkg-constants).", - }, - "arch": { - Type: schema.TypeString, - Computed: true, - Description: "The architecture of the host. This exposes `runtime.GOARCH` (see https://pkg.go.dev/runtime#pkg-constants).", - }, - }, - }, + "coder_workspace": workspaceDataSource(), + "coder_provisioner": provisionerDataSource(), }, ResourcesMap: map[string]*schema.Resource{ - "coder_agent": { - Description: "Use this resource to associate an agent.", - CreateContext: func(c 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()) - if err != nil { - return diag.FromErr(err) - } - return updateInitScript(resourceData, i) - }, - ReadWithoutTimeout: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - err := resourceData.Set("token", uuid.NewString()) - if err != nil { - return diag.FromErr(err) - } - return updateInitScript(resourceData, i) - }, - DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "init_script": { - Type: schema.TypeString, - Computed: true, - Description: "Run this script on startup of an instance to initialize the agent.", - }, - "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, - }, - "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.", - Type: schema.TypeString, - Optional: 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, - }, - }, - }, - "coder_agent_instance": { - 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, - }, - "instance_id": { - ForceNew: true, - Required: true, - Description: `The instance identifier of a provisioned resource.`, - Type: schema.TypeString, - }, - }, - }, - "coder_app": { - Description: "Use this resource to define shortcuts to access applications in a workspace.", - 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(ctx 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, - }, - "command": { - Type: schema.TypeString, - Description: "A command to run in a terminal opening this app. In the web, " + - "this will open in a new tab. In the CLI, this will SSH and execute the command. " + - "Either \"command\" or \"url\" may be specified, but not both.", - ConflictsWith: []string{"url"}, - Optional: true, - ForceNew: true, - }, - "icon": { - Type: schema.TypeString, - Description: "A URL to an icon that will display in the dashboard. View built-in " + - "icons here: https://github.com/coder/coder/tree/main/site/static/icons. Use a " + - "built-in icon with `data.coder_workspace.me.access_url + \"/icons/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, - }, - "name": { - Type: schema.TypeString, - Description: "A display name to identify the app.", - ForceNew: true, - Optional: true, - }, - "relative_path": { - Type: schema.TypeBool, - Description: "Specifies whether the URL will be accessed via a relative " + - "path or wildcard. Use if wildcard routing is unavailable.", - ForceNew: true, - Optional: true, - ConflictsWith: []string{"command"}, - }, - "url": { - Type: schema.TypeString, - Description: "A URL to be proxied to from inside the workspace. " + - "Either \"command\" or \"url\" may be specified, but not both.", - ForceNew: true, - Optional: true, - ConflictsWith: []string{"command"}, - }, - "healthcheck": { - Type: schema.TypeSet, - Description: "HTTP health checking to determine the application readiness.", - ForceNew: true, - Optional: true, - MaxItems: 1, - ConflictsWith: []string{"command"}, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "url": { - Type: schema.TypeString, - Description: "HTTP address used determine the application readiness. A successful health check is a HTTP response code less than 500 returned before healthcheck.interval seconds.", - ForceNew: true, - Required: true, - }, - "interval": { - Type: schema.TypeInt, - Description: "Duration in seconds to wait between healthcheck requests.", - ForceNew: true, - Required: true, - }, - "threshold": { - Type: schema.TypeInt, - Description: "Number of consecutive heathcheck failures before returning an unhealthy status.", - ForceNew: true, - Required: true, - }, - }, - }, - }, - }, - }, - "coder_metadata": { - Description: "Use this resource to attach key/value pairs to a resource. They will be " + - "displayed in the Coder dashboard.", - CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - resourceData.SetId(uuid.NewString()) - - items, err := populateIsNull(resourceData) - if err != nil { - return errorAsDiagnostics(err) - } - err = resourceData.Set("item", items) - if err != nil { - return errorAsDiagnostics(err) - } - - return nil - }, - ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - DeleteContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - return nil - }, - Schema: map[string]*schema.Schema{ - "resource_id": { - Type: schema.TypeString, - Description: "The \"id\" property of another resource that metadata should be attached to.", - ForceNew: true, - Required: true, - }, - "hide": { - Type: schema.TypeBool, - Description: "Hide the resource from the UI.", - ForceNew: true, - Optional: true, - }, - "icon": { - Type: schema.TypeString, - Description: "A URL to an icon that will display in the dashboard. View built-in " + - "icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a " + - "built-in icon with `data.coder_workspace.me.access_url + \"/icons/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, - }, - "item": { - Type: schema.TypeList, - Description: "Each \"item\" block defines a single metadata item consisting of a key/value pair.", - ForceNew: true, - Required: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Description: "The key of this metadata item.", - ForceNew: true, - Required: true, - }, - "value": { - Type: schema.TypeString, - Description: "The value of this metadata item.", - ForceNew: true, - Optional: true, - }, - "sensitive": { - Type: schema.TypeBool, - Description: "Set to \"true\" to for items such as API keys whose values should be " + - "hidden from view by default. Note that this does not prevent metadata from " + - "being retrieved using the API, so it is not suitable for secrets that should " + - "not be exposed to workspace users.", - ForceNew: true, - Optional: true, - Default: false, - }, - "is_null": { - Type: schema.TypeBool, - ForceNew: true, - Computed: true, - }, - }, - }, - }, - }, - }, + "coder_agent": agentResource(), + "coder_agent_instance": agentInstanceResource(), + "coder_app": appResource(), + "coder_metadata": metadataResource(), }, } } -// 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"))) - } - arch, valid := resourceData.Get("arch").(string) - if !valid { - return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch"))) - } - accessURL, err := config.URL.Parse("/") - if err != nil { - return diag.Errorf("parse access url: %s", err) - } - script := os.Getenv(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) - } - err = resourceData.Set("init_script", script) - if err != nil { - return diag.FromErr(err) - } - return nil -} - // populateIsNull reads the raw plan for a coder_metadata resource being created, // figures out which items have null "value"s, and augments them by setting the // "is_null" field to true. This ugly hack is necessary because terraform-plugin-sdk diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 635f636a..faa7d871 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,13 +1,8 @@ package provider_test import ( - "regexp" - "runtime" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/stretchr/testify/require" "github.com/coder/terraform-provider-coder/internal/provider" @@ -19,352 +14,3 @@ func TestProvider(t *testing.T) { err := tfProvider.InternalValidate() require.NoError(t, err) } - -func TestWorkspace(t *testing.T) { - t.Setenv("CODER_WORKSPACE_OWNER", "owner123") - t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") - - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - url = "https://example.com:8080" - } - data "coder_workspace" "me" { - }`, - 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.me"] - require.NotNil(t, resource) - - attribs := resource.Primary.Attributes - value := attribs["transition"] - require.NotNil(t, value) - t.Log(value) - require.Equal(t, "8080", attribs["access_port"]) - require.Equal(t, "owner123", attribs["owner"]) - require.Equal(t, "owner123@example.com", attribs["owner_email"]) - return nil - }, - }}, - }) - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - url = "https://example.com:8080" - } - data "coder_workspace" "me" { - }`, - 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.me"] - require.NotNil(t, resource) - - attribs := resource.Primary.Attributes - value := attribs["transition"] - require.NotNil(t, value) - t.Log(value) - require.Equal(t, "https://example.com:8080", attribs["access_url"]) - require.Equal(t, "owner123", attribs["owner"]) - require.Equal(t, "owner123@example.com", attribs["owner_email"]) - return nil - }, - }}, - }) -} - -func TestProvisioner(t *testing.T) { - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_provisioner" "me" { - }`, - 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_provisioner.me"] - require.NotNil(t, resource) - - attribs := resource.Primary.Attributes - require.Equal(t, runtime.GOOS, attribs["os"]) - require.Equal(t, runtime.GOARCH, attribs["arch"]) - return nil - }, - }}, - }) -} - -func TestAgent(t *testing.T) { - t.Parallel() - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - url = "https://example.com" - } - resource "coder_agent" "new" { - os = "linux" - arch = "amd64" - auth = "aws-instance-identity" - dir = "/tmp" - env = { - hi = "test" - } - startup_script = "echo test" - } - `, - 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["coder_agent.new"] - require.NotNil(t, resource) - for _, key := range []string{ - "token", - "os", - "arch", - "auth", - "dir", - "env.hi", - "startup_script", - } { - value := resource.Primary.Attributes[key] - t.Logf("%q = %q", key, value) - require.NotNil(t, value) - require.Greater(t, len(value), 0) - } - return nil - }, - }}, - }) -} - -func TestAgentInstance(t *testing.T) { - t.Parallel() - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - url = "https://example.com" - } - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" - } - resource "coder_agent_instance" "new" { - agent_id = coder_agent.dev.id - instance_id = "hello" - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 2) - resource := state.Modules[0].Resources["coder_agent_instance.new"] - require.NotNil(t, resource) - for _, key := range []string{ - "agent_id", - "instance_id", - } { - value := resource.Primary.Attributes[key] - t.Logf("%q = %q", key, value) - require.NotNil(t, value) - require.Greater(t, len(value), 0) - } - return nil - }, - }}, - }) -} - -func TestApp(t *testing.T) { - t.Parallel() - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": provider.New(), - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" - } - resource "coder_app" "code-server" { - agent_id = coder_agent.dev.id - name = "code-server" - icon = "builtin:vim" - relative_path = true - url = "http://localhost:13337" - healthcheck { - url = "http://localhost:13337/healthz" - interval = 5 - threshold = 6 - } - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 2) - resource := state.Modules[0].Resources["coder_app.code-server"] - require.NotNil(t, resource) - for _, key := range []string{ - "agent_id", - "name", - "icon", - "relative_path", - "url", - "healthcheck.0.url", - "healthcheck.0.interval", - "healthcheck.0.threshold", - } { - value := resource.Primary.Attributes[key] - t.Logf("%q = %q", key, value) - require.NotNil(t, value) - require.Greater(t, len(value), 0) - } - return nil - }, - }}, - }) -} - -func TestMetadata(t *testing.T) { - t.Parallel() - prov := provider.New() - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": prov, - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" - } - resource "coder_metadata" "agent" { - resource_id = coder_agent.dev.id - hide = true - icon = "/icons/storage.svg" - item { - key = "foo" - value = "bar" - } - item { - key = "secret" - value = "squirrel" - sensitive = true - } - item { - key = "implicit_null" - } - item { - key = "explicit_null" - value = null - } - item { - key = "empty" - value = "" - } - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 2) - agent := state.Modules[0].Resources["coder_agent.dev"] - require.NotNil(t, agent) - metadata := state.Modules[0].Resources["coder_metadata.agent"] - require.NotNil(t, metadata) - t.Logf("metadata attributes: %#v", metadata.Primary.Attributes) - for key, expected := range map[string]string{ - "resource_id": agent.Primary.Attributes["id"], - "hide": "true", - "icon": "/icons/storage.svg", - "item.#": "5", - "item.0.key": "foo", - "item.0.value": "bar", - "item.0.sensitive": "false", - "item.1.key": "secret", - "item.1.value": "squirrel", - "item.1.sensitive": "true", - "item.2.key": "implicit_null", - "item.2.is_null": "true", - "item.2.sensitive": "false", - "item.3.key": "explicit_null", - "item.3.is_null": "true", - "item.3.sensitive": "false", - "item.4.key": "empty", - "item.4.value": "", - "item.4.is_null": "false", - "item.4.sensitive": "false", - } { - require.Equal(t, expected, metadata.Primary.Attributes[key]) - } - return nil - }, - }}, - }) -} - -func TestMetadataDuplicateKeys(t *testing.T) { - t.Parallel() - prov := provider.New() - resource.Test(t, resource.TestCase{ - Providers: map[string]*schema.Provider{ - "coder": prov, - }, - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" - } - resource "coder_metadata" "agent" { - resource_id = coder_agent.dev.id - hide = true - icon = "/icons/storage.svg" - item { - key = "foo" - value = "bar" - } - item { - key = "foo" - value = "bar" - } - } - `, - ExpectError: regexp.MustCompile("duplicate metadata key"), - }}, - }) -} diff --git a/internal/provider/provisioner.go b/internal/provider/provisioner.go new file mode 100644 index 00000000..49d8f401 --- /dev/null +++ b/internal/provider/provisioner.go @@ -0,0 +1,35 @@ +package provider + +import ( + "context" + "runtime" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func provisionerDataSource() *schema.Resource { + return &schema.Resource{ + Description: "Use this data source to get information about the Coder provisioner.", + ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + rd.SetId(uuid.NewString()) + rd.Set("os", runtime.GOOS) + rd.Set("arch", runtime.GOARCH) + + return nil + }, + Schema: map[string]*schema.Schema{ + "os": { + Type: schema.TypeString, + Computed: true, + Description: "The operating system of the host. This exposes `runtime.GOOS` (see https://pkg.go.dev/runtime#pkg-constants).", + }, + "arch": { + Type: schema.TypeString, + Computed: true, + Description: "The architecture of the host. This exposes `runtime.GOARCH` (see https://pkg.go.dev/runtime#pkg-constants).", + }, + }, + } +} diff --git a/internal/provider/provisioner_test.go b/internal/provider/provisioner_test.go new file mode 100644 index 00000000..7b64e332 --- /dev/null +++ b/internal/provider/provisioner_test.go @@ -0,0 +1,39 @@ +package provider_test + +import ( + "runtime" + "testing" + + "github.com/coder/terraform-provider-coder/internal/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestProvisioner(t *testing.T) { + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + data "coder_provisioner" "me" { + }`, + 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_provisioner.me"] + require.NotNil(t, resource) + + attribs := resource.Primary.Attributes + require.Equal(t, runtime.GOOS, attribs["os"]) + require.Equal(t, runtime.GOARCH, attribs["arch"]) + return nil + }, + }}, + }) +} diff --git a/internal/provider/workspace.go b/internal/provider/workspace.go new file mode 100644 index 00000000..6b2175e5 --- /dev/null +++ b/internal/provider/workspace.go @@ -0,0 +1,126 @@ +package provider + +import ( + "context" + "os" + "reflect" + "strconv" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func workspaceDataSource() *schema.Resource { + return &schema.Resource{ + Description: "Use this data source to get information for the active workspace build.", + ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { + transition := os.Getenv("CODER_WORKSPACE_TRANSITION") + if transition == "" { + // Default to start! + transition = "start" + } + _ = rd.Set("transition", transition) + count := 0 + if transition == "start" { + count = 1 + } + _ = rd.Set("start_count", count) + + owner := os.Getenv("CODER_WORKSPACE_OWNER") + if owner == "" { + owner = "default" + } + _ = rd.Set("owner", owner) + + ownerEmail := os.Getenv("CODER_WORKSPACE_OWNER_EMAIL") + _ = rd.Set("owner_email", ownerEmail) + + ownerID := os.Getenv("CODER_WORKSPACE_OWNER_ID") + if ownerID == "" { + ownerID = uuid.Nil.String() + } + _ = rd.Set("owner_id", ownerID) + + name := os.Getenv("CODER_WORKSPACE_NAME") + if name == "" { + name = "default" + } + rd.Set("name", name) + + id := os.Getenv("CODER_WORKSPACE_ID") + if id == "" { + id = uuid.NewString() + } + rd.SetId(id) + + config, valid := i.(config) + if !valid { + return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String()) + } + rd.Set("access_url", config.URL.String()) + + rawPort := config.URL.Port() + if rawPort == "" { + rawPort = "80" + if config.URL.Scheme == "https" { + rawPort = "443" + } + } + port, err := strconv.Atoi(rawPort) + if err != nil { + return diag.Errorf("couldn't parse port %q", port) + } + rd.Set("access_port", port) + + return nil + }, + Schema: map[string]*schema.Schema{ + "access_url": { + Type: schema.TypeString, + Computed: true, + Description: "The access URL of the Coder deployment provisioning this workspace.", + }, + "access_port": { + Type: schema.TypeInt, + 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.`, + }, + "transition": { + Type: schema.TypeString, + Computed: true, + Description: `Either "start" or "stop". Use this to start/stop resources with "count".`, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + Description: "Username of the workspace owner.", + }, + "owner_email": { + Type: schema.TypeString, + Computed: true, + Description: "Email address of the workspace owner.", + }, + "owner_id": { + Type: schema.TypeString, + Computed: true, + Description: "UUID of the workspace owner.", + }, + "id": { + Type: schema.TypeString, + Computed: true, + Description: "UUID of the workspace.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the workspace.", + }, + }, + } +} diff --git a/internal/provider/workspace_test.go b/internal/provider/workspace_test.go new file mode 100644 index 00000000..4ea3a3e6 --- /dev/null +++ b/internal/provider/workspace_test.go @@ -0,0 +1,75 @@ +package provider_test + +import ( + "testing" + + "github.com/coder/terraform-provider-coder/internal/provider" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestWorkspace(t *testing.T) { + t.Setenv("CODER_WORKSPACE_OWNER", "owner123") + t.Setenv("CODER_WORKSPACE_OWNER_EMAIL", "owner123@example.com") + + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + url = "https://example.com:8080" + } + data "coder_workspace" "me" { + }`, + 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.me"] + require.NotNil(t, resource) + + attribs := resource.Primary.Attributes + value := attribs["transition"] + require.NotNil(t, value) + t.Log(value) + require.Equal(t, "8080", attribs["access_port"]) + require.Equal(t, "owner123", attribs["owner"]) + require.Equal(t, "owner123@example.com", attribs["owner_email"]) + return nil + }, + }}, + }) + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + url = "https://example.com:8080" + } + data "coder_workspace" "me" { + }`, + 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.me"] + require.NotNil(t, resource) + + attribs := resource.Primary.Attributes + value := attribs["transition"] + require.NotNil(t, value) + t.Log(value) + require.Equal(t, "https://example.com:8080", attribs["access_url"]) + require.Equal(t, "owner123", attribs["owner"]) + require.Equal(t, "owner123@example.com", attribs["owner_email"]) + return nil + }, + }}, + }) +}