Skip to content

Commit

Permalink
add open_in option to coder_app (#321)
Browse files Browse the repository at this point in the history
* add open_in option to coder_app

* work on tests

* add missing example

* rename test

* lint

* generate docs
  • Loading branch information
defelmnq authored Dec 19, 2024
1 parent 8349a69 commit d933a71
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/resources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ resource "coder_app" "code-server" {
url = "http://localhost:13337"
share = "owner"
subdomain = false
open_in = "window"
healthcheck {
url = "http://localhost:13337/healthz"
interval = 5
Expand Down Expand Up @@ -65,6 +66,7 @@ resource "coder_app" "vim" {
- `healthcheck` (Block Set, Max: 1) HTTP health checking to determine the application readiness. (see [below for nested schema](#nestedblock--healthcheck))
- `hidden` (Boolean) Determines if the app is visible in the UI (minimum Coder version: v2.16).
- `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/icon. Use a built-in icon with `"${data.coder_workspace.me.access_url}/icon/<path>"`.
- `open_in` (String) Determines where the app will be opened. Valid values are `"tab"`, `"window"`, and `"slim-window" (default)`. `"tab"` opens in a new tab in the same browser window. `"window"` opens a fresh browser window with navigation options. `"slim-window"` opens a new browser window without navigation controls.
- `order` (Number) The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order).
- `share` (String) Determines the level which the application is shared at. Valid levels are `"owner"` (default), `"authenticated"` and `"public"`. Level `"owner"` disables sharing on the app, so only the workspace owner can access it. 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 site-wide via a flag on `coder server` (Enterprise only).
- `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`.
Expand Down
1 change: 1 addition & 0 deletions examples/resources/coder_app/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ resource "coder_app" "code-server" {
url = "http://localhost:13337"
share = "owner"
subdomain = false
open_in = "window"
healthcheck {
url = "http://localhost:13337/healthz"
interval = 5
Expand Down
62 changes: 62 additions & 0 deletions integration/coder-app-open-in/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
local = {
source = "hashicorp/local"
}
}
}

data "coder_workspace" "me" {}

resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
dir = "/workspace"
}

resource "coder_app" "window" {
agent_id = coder_agent.dev.id
slug = "window"
share = "owner"
open_in = "window"
}

resource "coder_app" "slim-window" {
agent_id = coder_agent.dev.id
slug = "slim-window"
share = "owner"
open_in = "slim-window"
}

resource "coder_app" "defaulted" {
agent_id = coder_agent.dev.id
slug = "defaulted"
share = "owner"
}

locals {
# NOTE: these must all be strings in the output
output = {
"coder_app.window.open_in" = tostring(coder_app.window.open_in)
"coder_app.slim-window.open_in" = tostring(coder_app.slim-window.open_in)
"coder_app.defaulted.open_in" = tostring(coder_app.defaulted.open_in)
}
}

variable "output_path" {
type = string
}

resource "local_file" "output" {
filename = var.output_path
content = jsonencode(local.output)
}

output "output" {
value = local.output
sensitive = true
}

9 changes: 9 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ func TestIntegration(t *testing.T) {
"workspace_owner.login_type": `password`,
},
},
{
name: "coder-app-open-in",
minVersion: "v2.19.0",
expectedOutput: map[string]string{
"coder_app.window.open_in": "window",
"coder_app.slim-window.open_in": "slim-window",
"coder_app.defaulted.open_in": "slim-window",
},
},
{
name: "coder-app-hidden",
minVersion: "v0.0.0",
Expand Down
22 changes: 22 additions & 0 deletions provider/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,28 @@ func appResource() *schema.Resource {
ForceNew: true,
Optional: true,
},
"open_in": {
Type: schema.TypeString,
Description: "Determines where the app will be opened. Valid values are `\"tab\"`, `\"window\"`, and `\"slim-window\" (default)`. " +
"`\"tab\"` opens in a new tab in the same browser window. `\"window\"` opens a fresh browser window with navigation options. " +
"`\"slim-window\"` opens a new browser window without navigation controls.",
ForceNew: true,
Optional: true,
Default: "slim-window",
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 "tab", "window", "slim-window":
return nil
}

return diag.Errorf(`invalid "coder_app" open_in value, must be one of "tab", "window", "slim-window": %q`, valStr)
},
},
},
}
}
111 changes: 111 additions & 0 deletions provider/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func TestApp(t *testing.T) {
}
order = 4
hidden = false
open_in = "slim-window"
}
`,
Check: func(state *terraform.State) error {
Expand All @@ -64,6 +65,7 @@ func TestApp(t *testing.T) {
"healthcheck.0.threshold",
"order",
"hidden",
"open_in",
} {
value := resource.Primary.Attributes[key]
t.Logf("%q = %q", key, value)
Expand Down Expand Up @@ -98,6 +100,7 @@ func TestApp(t *testing.T) {
display_name = "Testing"
url = "https://google.com"
external = true
open_in = "slim-window"
}
`,
external: true,
Expand All @@ -116,6 +119,7 @@ func TestApp(t *testing.T) {
url = "https://google.com"
external = true
subdomain = true
open_in = "slim-window"
}
`,
expectError: regexp.MustCompile("conflicts with subdomain"),
Expand Down Expand Up @@ -209,6 +213,7 @@ func TestApp(t *testing.T) {
interval = 5
threshold = 6
}
open_in = "slim-window"
}
`, sharingLine)

Expand Down Expand Up @@ -241,13 +246,114 @@ func TestApp(t *testing.T) {
}
})

t.Run("OpenIn", func(t *testing.T) {
t.Parallel()

cases := []struct {
name string
value string
expectValue string
expectError *regexp.Regexp
}{
{
name: "default",
value: "", // default
expectValue: "slim-window",
},
{
name: "InvalidValue",
value: "nonsense",
expectError: regexp.MustCompile(`invalid "coder_app" open_in value, must be one of "tab", "window", "slim-window": "nonsense"`),
},
{
name: "ExplicitWindow",
value: "window",
expectValue: "window",
},
{
name: "ExplicitSlimWindow",
value: "slim-window",
expectValue: "slim-window",
},
{
name: "ExplicitTab",
value: "tab",
expectValue: "tab",
},
}

for _, c := range cases {
c := c

t.Run(c.name, func(t *testing.T) {
t.Parallel()

config := `
provider "coder" {
}
resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
}
resource "coder_app" "code-server" {
agent_id = coder_agent.dev.id
slug = "code-server"
display_name = "code-server"
icon = "builtin:vim"
url = "http://localhost:13337"
healthcheck {
url = "http://localhost:13337/healthz"
interval = 5
threshold = 6
}`

if c.value != "" {
config += fmt.Sprintf(`
open_in = %q
`, c.value)
}

config += `
}
`

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["open_in"]
require.Equal(t, c.expectValue, value)
return nil
}
if c.expectError != nil {
checkFn = nil
}

resource.Test(t, resource.TestCase{
ProviderFactories: coderFactory(),
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: config,
Check: checkFn,
ExpectError: c.expectError,
}},
})
})
}
})

t.Run("Hidden", func(t *testing.T) {
t.Parallel()

cases := []struct {
name string
config string
hidden bool
openIn string
}{{
name: "Is Hidden",
config: `
Expand All @@ -263,9 +369,11 @@ func TestApp(t *testing.T) {
url = "https://google.com"
external = true
hidden = true
open_in = "slim-window"
}
`,
hidden: true,
openIn: "slim-window",
}, {
name: "Is Not Hidden",
config: `
Expand All @@ -281,9 +389,11 @@ func TestApp(t *testing.T) {
url = "https://google.com"
external = true
hidden = false
open_in = "window"
}
`,
hidden: false,
openIn: "window",
}}
for _, tc := range cases {
tc := tc
Expand All @@ -300,6 +410,7 @@ func TestApp(t *testing.T) {
resource := state.Modules[0].Resources["coder_app.test"]
require.NotNil(t, resource)
require.Equal(t, strconv.FormatBool(tc.hidden), resource.Primary.Attributes["hidden"])
require.Equal(t, tc.openIn, resource.Primary.Attributes["open_in"])
return nil
},
ExpectError: nil,
Expand Down

0 comments on commit d933a71

Please sign in to comment.