From 00ea9f1f9987a879dc91ee5877b956a826cf286e Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 04:57:40 +0000 Subject: [PATCH 01/13] feat: add confliction with `subdomain` --- provider/app.go | 5 +++-- provider/app_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/provider/app.go b/provider/app.go index dd0428a1..3892b078 100644 --- a/provider/app.go +++ b/provider/app.go @@ -85,8 +85,9 @@ func appResource() *schema.Resource { 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"}, + "Either `command` or `url` may be specified, but not both." + + "If `command` is specified, `subdomain` must either be false or not specified.", + ConflictsWith: []string{"url", "subdomain"}, Optional: true, ForceNew: true, }, diff --git a/provider/app_test.go b/provider/app_test.go index b8d4c8e7..5c122867 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -596,4 +596,36 @@ func TestApp(t *testing.T) { }) } }) + + // TODO: Find a better place for this? + // TODO: Do we need to test this with the schema rules already existing? + t.Run("Command", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + command string + subdomain bool + expectError *regexp.Regexp + }{ + { + name: "Command", + command: "read -p \"Workspace spawned. Press enter to continue...\"", + }, + { + name: "CommandAndURL", + command: "read -p \"Workspace spawned. Press enter to continue...\"", + subdomain: true, + expectError: regexp.MustCompile("conflicts with subdomain"), + }, + } + + for _, c := range cases { + c := c + + t.Run(c.name, func(t *testing.T) { + t.Parallel() + }) + } + }) } From 31fc7f8c649d9078855dc55a6ef6c05d32b658ea Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 05:14:08 +0000 Subject: [PATCH 02/13] fix: provide correct error --- provider/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/app.go b/provider/app.go index 3892b078..b4b1d9e3 100644 --- a/provider/app.go +++ b/provider/app.go @@ -86,7 +86,7 @@ func appResource() *schema.Resource { 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." + - "If `command` is specified, `subdomain` must either be false or not specified.", + "If `command` is specified, `subdomain` must be unset.", ConflictsWith: []string{"url", "subdomain"}, Optional: true, ForceNew: true, From 06190614b78e2903ef07f526ccabcd595053369d Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 05:14:19 +0000 Subject: [PATCH 03/13] feat: implement testing suite --- provider/app_test.go | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index 5c122867..c637ec95 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -610,11 +610,11 @@ func TestApp(t *testing.T) { }{ { name: "Command", - command: "read -p \"Workspace spawned. Press enter to continue...\"", + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", }, { name: "CommandAndURL", - command: "read -p \"Workspace spawned. Press enter to continue...\"", + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", subdomain: true, expectError: regexp.MustCompile("conflicts with subdomain"), }, @@ -625,6 +625,36 @@ func TestApp(t *testing.T) { t.Run(c.name, func(t *testing.T) { t.Parallel() + + subdomainLine := "" + if c.subdomain { + subdomainLine = "subdomain = true" + } + + config := fmt.Sprintf(` + 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 = "Testing" + open_in = "slim-window" + command = "%s" + %s + } + `, c.command, subdomainLine) + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: config, + ExpectError: c.expectError, + }}, + }) }) } }) From 3d63e36fbc23ecc7cdb314fce3052c052741cb77 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 05:14:32 +0000 Subject: [PATCH 04/13] chore: remove unnecessary test suite --- provider/app_test.go | 62 -------------------------------------------- 1 file changed, 62 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index c637ec95..b8d4c8e7 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -596,66 +596,4 @@ func TestApp(t *testing.T) { }) } }) - - // TODO: Find a better place for this? - // TODO: Do we need to test this with the schema rules already existing? - t.Run("Command", func(t *testing.T) { - t.Parallel() - - cases := []struct { - name string - command string - subdomain bool - expectError *regexp.Regexp - }{ - { - name: "Command", - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", - }, - { - name: "CommandAndURL", - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", - subdomain: true, - expectError: regexp.MustCompile("conflicts with subdomain"), - }, - } - - for _, c := range cases { - c := c - - t.Run(c.name, func(t *testing.T) { - t.Parallel() - - subdomainLine := "" - if c.subdomain { - subdomainLine = "subdomain = true" - } - - config := fmt.Sprintf(` - 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 = "Testing" - open_in = "slim-window" - command = "%s" - %s - } - `, c.command, subdomainLine) - - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: config, - ExpectError: c.expectError, - }}, - }) - }) - } - }) } From e1377f5185cef467bae4c63f1e60677d790a0edc Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 05:20:25 +0000 Subject: [PATCH 05/13] fix: resolve `app.md` lint --- docs/resources/app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/app.md b/docs/resources/app.md index 35a9951b..78eaed41 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -61,7 +61,7 @@ resource "coder_app" "vim" { ### Optional -- `command` (String) 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. +- `command` (String) 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. If `command` is specified, `subdomain` must be unset. - `display_name` (String) A display name to identify the app. Defaults to the slug. - `external` (Boolean) Specifies whether `url` is opened on the client machine instead of proxied through the workspace. - `group` (String) The name of a group that this app belongs to. From 7fec6cfafab312a561ba960c682cdefc06efbf7d Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 05:21:55 +0000 Subject: [PATCH 06/13] chore: resolve `app.go` description --- provider/app.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/app.go b/provider/app.go index b4b1d9e3..429f6c91 100644 --- a/provider/app.go +++ b/provider/app.go @@ -85,7 +85,7 @@ func appResource() *schema.Resource { 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." + + "Either `command` or `url` may be specified, but not both. " + "If `command` is specified, `subdomain` must be unset.", ConflictsWith: []string{"url", "subdomain"}, Optional: true, From 291d92639b5ef36427f23874e101660011b7e219 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 10:47:06 +0000 Subject: [PATCH 07/13] Revert "chore: remove unnecessary test suite" This reverts commit 3d63e36fbc23ecc7cdb314fce3052c052741cb77. --- provider/app_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/provider/app_test.go b/provider/app_test.go index b8d4c8e7..c637ec95 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -596,4 +596,66 @@ func TestApp(t *testing.T) { }) } }) + + // TODO: Find a better place for this? + // TODO: Do we need to test this with the schema rules already existing? + t.Run("Command", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + command string + subdomain bool + expectError *regexp.Regexp + }{ + { + name: "Command", + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + }, + { + name: "CommandAndURL", + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + subdomain: true, + expectError: regexp.MustCompile("conflicts with subdomain"), + }, + } + + for _, c := range cases { + c := c + + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + subdomainLine := "" + if c.subdomain { + subdomainLine = "subdomain = true" + } + + config := fmt.Sprintf(` + 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 = "Testing" + open_in = "slim-window" + command = "%s" + %s + } + `, c.command, subdomainLine) + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: config, + ExpectError: c.expectError, + }}, + }) + }) + } + }) } From ea9ea65aafeeb363fd3798a91e0ce537265ccb4f Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 10:47:22 +0000 Subject: [PATCH 08/13] chore: remove unused comments --- provider/app_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index c637ec95..8b216ad1 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -597,8 +597,6 @@ func TestApp(t *testing.T) { } }) - // TODO: Find a better place for this? - // TODO: Do we need to test this with the schema rules already existing? t.Run("Command", func(t *testing.T) { t.Parallel() From 5c3e18c4e3f97ad23c75ad768480cf0dac6f73f2 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 10:47:51 +0000 Subject: [PATCH 09/13] chore: rename `Command` to `ConflictsWith` --- provider/app_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/app_test.go b/provider/app_test.go index 8b216ad1..0fadd0e0 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -597,7 +597,7 @@ func TestApp(t *testing.T) { } }) - t.Run("Command", func(t *testing.T) { + t.Run("ConflictsWith", func(t *testing.T) { t.Parallel() cases := []struct { From 63c5cdce5a311cb3abf79e2cb7dc801fb0fd8592 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 11:13:24 +0000 Subject: [PATCH 10/13] feat: implement extensive test case --- provider/app_test.go | 86 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index 0fadd0e0..0e5209c0 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -564,8 +564,6 @@ func TestApp(t *testing.T) { } for _, c := range cases { - c := c - t.Run(c.name, func(t *testing.T) { t.Parallel() @@ -600,33 +598,100 @@ func TestApp(t *testing.T) { t.Run("ConflictsWith", func(t *testing.T) { t.Parallel() + type healthcheck struct { + url string + interval int + threshold int + } + cases := []struct { name string + url string command string subdomain bool + healthcheck healthcheck + external bool + share string expectError *regexp.Regexp }{ { - name: "Command", - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + name: "CommandAndSubdomain", + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + subdomain: true, + expectError: regexp.MustCompile("conflicts with subdomain"), }, { - name: "CommandAndURL", + name: "URLAndCommand", + url: "https://google.com", command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + expectError: regexp.MustCompile("conflicts with command"), + }, + { + name: "HealthcheckAndCommand", + healthcheck: healthcheck{ + url: "https://google.com", + interval: 5, + threshold: 6, + }, + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + expectError: regexp.MustCompile("conflicts with command"), + }, + { + name: "ExternalAndHealthcheck", + external: true, + healthcheck: healthcheck{ + url: "https://google.com", + interval: 5, + threshold: 6, + }, + expectError: regexp.MustCompile("conflicts with healthcheck"), + }, + { + name: "ExternalAndCommand", + external: true, + command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + expectError: regexp.MustCompile("conflicts with command"), + }, + { + name: "ExternalAndSubdomain", + external: true, subdomain: true, expectError: regexp.MustCompile("conflicts with subdomain"), }, + { + name: "ExternalAndShare", + external: true, + share: "https://google.com", + expectError: regexp.MustCompile("conflicts with share"), + }, } for _, c := range cases { - c := c - t.Run(c.name, func(t *testing.T) { t.Parallel() - subdomainLine := "" + extraLines := []string{} + if c.command != "" { + extraLines = append(extraLines, fmt.Sprintf("command = %q", c.command)) + } if c.subdomain { - subdomainLine = "subdomain = true" + extraLines = append(extraLines, "subdomain = true") + } + if c.external { + extraLines = append(extraLines, "external = true") + } + if c.url != "" { + extraLines = append(extraLines, fmt.Sprintf("url = %q", c.url)) + } + if c.healthcheck != (healthcheck{}) { + extraLines = append(extraLines, fmt.Sprintf(`healthcheck { + url = %q + interval = %d + threshold = %d + }`, c.healthcheck.url, c.healthcheck.interval, c.healthcheck.threshold)) + } + if c.share != "" { + extraLines = append(extraLines, fmt.Sprintf("share = %q", c.share)) } config := fmt.Sprintf(` @@ -640,10 +705,9 @@ func TestApp(t *testing.T) { slug = "code-server" display_name = "Testing" open_in = "slim-window" - command = "%s" %s } - `, c.command, subdomainLine) + `, strings.Join(extraLines, "\n ")) resource.Test(t, resource.TestCase{ ProviderFactories: coderFactory(), From 0750f37e8a096d7ff270dd77cb2bf9bbd3d9b25c Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Mon, 24 Nov 2025 11:19:22 +0000 Subject: [PATCH 11/13] fix: convert to using dummy values --- provider/app_test.go | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index 0e5209c0..3f6915c8 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -604,6 +604,17 @@ func TestApp(t *testing.T) { threshold int } + dummyURL := "https://google.com" + dummyCommand := "read -p \\\"Workspace spawned. Press enter to continue...\\\"" + dummyExternal := true + dummySubdomain := true + dummyHealthcheck := healthcheck{ + url: "https://google.com", + interval: 5, + threshold: 6, + } + dummyShare := "owner" + cases := []struct { name string url string @@ -616,52 +627,44 @@ func TestApp(t *testing.T) { }{ { name: "CommandAndSubdomain", - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", - subdomain: true, + command: dummyCommand, + subdomain: dummySubdomain, expectError: regexp.MustCompile("conflicts with subdomain"), }, { name: "URLAndCommand", - url: "https://google.com", - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + url: dummyURL, + command: dummyCommand, expectError: regexp.MustCompile("conflicts with command"), }, { - name: "HealthcheckAndCommand", - healthcheck: healthcheck{ - url: "https://google.com", - interval: 5, - threshold: 6, - }, - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + name: "HealthcheckAndCommand", + healthcheck: dummyHealthcheck, + command: dummyCommand, expectError: regexp.MustCompile("conflicts with command"), }, { - name: "ExternalAndHealthcheck", - external: true, - healthcheck: healthcheck{ - url: "https://google.com", - interval: 5, - threshold: 6, - }, + name: "ExternalAndHealthcheck", + external: dummyExternal, + healthcheck: dummyHealthcheck, expectError: regexp.MustCompile("conflicts with healthcheck"), }, { name: "ExternalAndCommand", - external: true, - command: "read -p \\\"Workspace spawned. Press enter to continue...\\\"", + external: dummyExternal, + command: dummyCommand, expectError: regexp.MustCompile("conflicts with command"), }, { name: "ExternalAndSubdomain", - external: true, - subdomain: true, + external: dummyExternal, + subdomain: dummySubdomain, expectError: regexp.MustCompile("conflicts with subdomain"), }, { name: "ExternalAndShare", - external: true, - share: "https://google.com", + external: dummyExternal, + share: dummyShare, expectError: regexp.MustCompile("conflicts with share"), }, } From 1f0ced1ed3d01e647b95ece659b80268e9271868 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Sat, 29 Nov 2025 10:39:43 +0000 Subject: [PATCH 12/13] chore: remove duplicate testcase --- provider/app_test.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/provider/app_test.go b/provider/app_test.go index 3f6915c8..17b3dce4 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -109,25 +109,6 @@ func TestApp(t *testing.T) { } `, external: true, - }, { - name: "ConflictsWithSubdomain", - config: ` - provider "coder" {} - resource "coder_agent" "dev" { - os = "linux" - arch = "amd64" - } - resource "coder_app" "test" { - agent_id = coder_agent.dev.id - slug = "test" - display_name = "Testing" - url = "https://google.com" - external = true - subdomain = true - open_in = "slim-window" - } - `, - expectError: regexp.MustCompile("conflicts with subdomain"), }} for _, tc := range cases { tc := tc From c536b4b0fd1a888948c1c5eb29146f8ddca7f572 Mon Sep 17 00:00:00 2001 From: Jake Howell Date: Sat, 29 Nov 2025 10:40:42 +0000 Subject: [PATCH 13/13] chore: update wording to `Conflicts with subdomain.` --- docs/resources/app.md | 2 +- provider/app.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/resources/app.md b/docs/resources/app.md index 78eaed41..67efb5fc 100644 --- a/docs/resources/app.md +++ b/docs/resources/app.md @@ -61,7 +61,7 @@ resource "coder_app" "vim" { ### Optional -- `command` (String) 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. If `command` is specified, `subdomain` must be unset. +- `command` (String) 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. Conflicts with `subdomain`. - `display_name` (String) A display name to identify the app. Defaults to the slug. - `external` (Boolean) Specifies whether `url` is opened on the client machine instead of proxied through the workspace. - `group` (String) The name of a group that this app belongs to. diff --git a/provider/app.go b/provider/app.go index 429f6c91..fb567573 100644 --- a/provider/app.go +++ b/provider/app.go @@ -86,7 +86,7 @@ func appResource() *schema.Resource { 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. " + - "If `command` is specified, `subdomain` must be unset.", + "Conflicts with `subdomain`.", ConflictsWith: []string{"url", "subdomain"}, Optional: true, ForceNew: true,