diff --git a/docs/resources/app.md b/docs/resources/app.md index 3b564862..d91c20a3 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -30,6 +30,7 @@ resource "coder_app" "code-server" { name = "VS Code" icon = data.coder_workspace.me.access_url + "/icons/vscode.svg" url = "http://localhost:13337" + share = "owner" subdomain = false healthcheck { url = "http://localhost:13337/healthz" @@ -67,6 +68,7 @@ resource "coder_app" "intellij" { - `icon` (String) 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/"`. - `name` (String) A display name to identify the app. - `relative_path` (Boolean, Deprecated) Specifies whether the URL will be accessed via a relative path or wildcard. Use if wildcard routing is unavailable. Defaults to true. +- `share` (String) Application sharing is an enterprise feature and any values will be ignored (and sharing disabled) if your deployment is not entitled to use application sharing. Valid values are "owner", "template", "authenticated" and "public". Level "owner" disables sharing on the app, so only the workspace owner can access it. Level "template" shares the app with all users that can read the workspace's template. Level "authenticated" shares the app with all authenticated users. Level "public" shares it with any user, including unauthenticated users. Permitted application sharing levels can be configured via a flag on "coder server". Defaults to "owner" (sharing disabled). - `subdomain` (Boolean) Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. Defaults to false. - `url` (String) A URL to be proxied to from inside the workspace. Either "command" or "url" may be specified, but not both. diff --git a/examples/resources/coder_app/resource.tf b/examples/resources/coder_app/resource.tf index 715f5814..48190440 100644 --- a/examples/resources/coder_app/resource.tf +++ b/examples/resources/coder_app/resource.tf @@ -15,6 +15,7 @@ resource "coder_app" "code-server" { name = "VS Code" icon = data.coder_workspace.me.access_url + "/icons/vscode.svg" url = "http://localhost:13337" + share = "owner" subdomain = false healthcheck { url = "http://localhost:13337/healthz" diff --git a/examples/resources/coder_parameter/resource.tf b/examples/resources/coder_parameter/resource.tf index 21fb5e4b..9057b3a1 100644 --- a/examples/resources/coder_parameter/resource.tf +++ b/examples/resources/coder_parameter/resource.tf @@ -1,46 +1,46 @@ data "coder_parameter" "example" { - display_name = "Region" - description = "Specify a region to place your workspace." - immutable = true - type = "string" - option { - value = "us-central1-a" - label = "US Central" - icon = "/icon/usa.svg" - } - option { - value = "asia-central1-a" - label = "Asia" - icon = "/icon/asia.svg" - } + display_name = "Region" + description = "Specify a region to place your workspace." + immutable = true + type = "string" + option { + value = "us-central1-a" + label = "US Central" + icon = "/icon/usa.svg" + } + option { + value = "asia-central1-a" + label = "Asia" + icon = "/icon/asia.svg" + } } data "coder_parameter" "ami" { - display_name = "Machine Image" - option { - value = "ami-xxxxxxxx" - label = "Ubuntu" - icon = "/icon/ubuntu.svg" - } + display_name = "Machine Image" + option { + value = "ami-xxxxxxxx" + label = "Ubuntu" + icon = "/icon/ubuntu.svg" + } } data "coder_parameter" "image" { - display_name = "Docker Image" - icon = "/icon/docker.svg" - type = "bool" + display_name = "Docker Image" + icon = "/icon/docker.svg" + type = "bool" } data "coder_parameter" "cores" { - display_name = "CPU Cores" - icon = "/icon/" + display_name = "CPU Cores" + icon = "/icon/" } data "coder_parameter" "disk_size" { - display_name = "Disk Size" - type = "number" - validation { - # This can apply to number and string types. - min = 0 - max = 10 - } + display_name = "Disk Size" + type = "number" + validation { + # This can apply to number and string types. + min = 0 + max = 10 + } } diff --git a/provider/app.go b/provider/app.go index 2e0485bb..961f4ed3 100644 --- a/provider/app.go +++ b/provider/app.go @@ -5,6 +5,7 @@ import ( "net/url" "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" ) @@ -76,6 +77,38 @@ func appResource() *schema.Resource { ForceNew: true, Optional: true, }, + "share": { + Type: schema.TypeString, + Description: "Application sharing is an enterprise feature " + + "and any values will be ignored (and sharing disabled) " + + "if your deployment is not entitled to use application " + + `sharing. Valid values are "owner", "template", ` + + `"authenticated" and "public". Level "owner" disables ` + + "sharing on the app, so only the workspace owner can " + + `access it. Level "template" shares the app with all users ` + + `that can read the workspace's template. Level ` + + `"authenticated" shares the app with all authenticated ` + + `users. Level "public" shares it with any user, ` + + "including unauthenticated users. Permitted application " + + `sharing levels can be configured via a flag on "coder ` + + `server". Defaults to "owner" (sharing disabled).`, + ForceNew: true, + Optional: true, + Default: "owner", + ValidateDiagFunc: func(val interface{}, c cty.Path) diag.Diagnostics { + valStr, ok := val.(string) + if !ok { + return diag.Errorf("expected string, got %T", val) + } + + switch valStr { + case "owner", "template", "authenticated", "public": + return nil + } + + return diag.Errorf(`invalid app share %q, must be one of "owner", "template", "authenticated", "public"`, valStr) + }, + }, "url": { Type: schema.TypeString, Description: "A URL to be proxied to from inside the workspace. " + diff --git a/provider/app_test.go b/provider/app_test.go index f28ccaf6..977c3ea1 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -1,6 +1,8 @@ package provider_test import ( + "fmt" + "regexp" "testing" "github.com/coder/terraform-provider-coder/provider" @@ -12,13 +14,17 @@ import ( 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: ` + + t.Run("OK", func(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" { @@ -38,28 +44,135 @@ func TestApp(t *testing.T) { } } `, - 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", - "subdomain", - "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 + 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", + "subdomain", + // Should be set by default even though it isn't + // specified. + "share", + "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 + }, + }}, + }) + }) + + t.Run("SharingLevel", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + value string + expectValue string + expectError *regexp.Regexp + }{ + { + name: "Default", + value: "", // default + expectValue: "owner", + }, + { + name: "InvalidValue", + value: "blah", + expectError: regexp.MustCompile(`invalid app share "blah"`), + }, + { + name: "ExplicitOwner", + value: "owner", + expectValue: "owner", }, - }}, + { + name: "ExplicitTemplate", + value: "template", + expectValue: "template", + }, + { + name: "ExplicitAuthenticated", + value: "authenticated", + expectValue: "authenticated", + }, + { + name: "ExplicitPublic", + value: "public", + expectValue: "public", + }, + } + + for _, c := range cases { + c := c + + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + sharingLine := "" + if c.value != "" { + sharingLine = fmt.Sprintf("share = %q", c.value) + } + config := fmt.Sprintf(` + 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" + url = "http://localhost:13337" + %s + healthcheck { + url = "http://localhost:13337/healthz" + interval = 5 + threshold = 6 + } + } + `, sharingLine) + + checkFn := 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) + + // Read share and ensure it matches the expected + // value. + value := resource.Primary.Attributes["share"] + require.Equal(t, c.expectValue, value) + return nil + } + if c.expectError != nil { + checkFn = nil + } + + resource.Test(t, resource.TestCase{ + Providers: map[string]*schema.Provider{ + "coder": provider.New(), + }, + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: config, + Check: checkFn, + ExpectError: c.expectError, + }}, + }) + }) + } }) }