From afbe048dfc1ef018f0a106dcc3505b187f92fd00 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 20 Mar 2025 14:14:31 +0000 Subject: [PATCH 1/5] feat(cli): add open app command --- cli/open.go | 159 ++++++++++++++++++++++ cli/open_internal_test.go | 114 +++++++++++++++- cli/open_test.go | 72 +++++++++- cli/testdata/coder_open_--help.golden | 1 + cli/testdata/coder_open_app_--help.golden | 14 ++ docs/manifest.json | 5 + docs/reference/cli/open.md | 1 + docs/reference/cli/open_app.md | 22 +++ 8 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 cli/testdata/coder_open_app_--help.golden create mode 100644 docs/reference/cli/open_app.md diff --git a/cli/open.go b/cli/open.go index 09883684a7707..2955edfe98bdb 100644 --- a/cli/open.go +++ b/cli/open.go @@ -7,6 +7,7 @@ import ( "path" "path/filepath" "runtime" + "slices" "strings" "github.com/skratchdot/open-golang/open" @@ -26,6 +27,7 @@ func (r *RootCmd) open() *serpent.Command { }, Children: []*serpent.Command{ r.openVSCode(), + r.openApp(), }, } return cmd @@ -211,6 +213,118 @@ func (r *RootCmd) openVSCode() *serpent.Command { return cmd } +func (r *RootCmd) openApp() *serpent.Command { + var ( + preferredRegion string + testOpenError bool + ) + + client := new(codersdk.Client) + cmd := &serpent.Command{ + Annotations: workspaceCommand, + Use: "app ", + Short: "Open a workspace application.", + Middleware: serpent.Chain( + serpent.RequireNArgs(2), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + ctx, cancel := context.WithCancel(inv.Context()) + defer cancel() + + // Check if we're inside a workspace, and especially inside _this_ + // workspace so we can perform path resolution/expansion. Generally, + // we know that if we're inside a workspace, `open` can't be used. + insideAWorkspace := inv.Environ.Get("CODER") == "true" + + // Fetch the preferred region. + regions, err := client.Regions(ctx) + if err != nil { + return xerrors.Errorf("failed to fetch regions: %w", err) + } + var region codersdk.Region + preferredIdx := slices.IndexFunc(regions, func(r codersdk.Region) bool { + return r.Name == preferredRegion + }) + if preferredIdx == -1 { + allRegions := make([]string, len(regions)) + for i, r := range regions { + allRegions[i] = r.Name + } + cliui.Errorf(inv.Stderr, "Preferred region %q not found!\nAvailable regions: %v", preferredRegion, allRegions) + return xerrors.Errorf("region not found") + } + region = regions[preferredIdx] + + workspaceName := inv.Args[0] + appSlug := inv.Args[1] + + // Fetch the ws and agent + ws, agt, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName) + if err != nil { + return xerrors.Errorf("failed to get workspace and agent: %w", err) + } + + // Fetch the app + var app codersdk.WorkspaceApp + appIdx := slices.IndexFunc(agt.Apps, func(a codersdk.WorkspaceApp) bool { + return a.Slug == appSlug + }) + if appIdx == -1 { + appSlugs := make([]string, len(agt.Apps)) + for i, app := range agt.Apps { + appSlugs[i] = app.Slug + } + cliui.Errorf(inv.Stderr, "App %q not found in workspace %q!\nAvailable apps: %v", appSlug, workspaceName, appSlugs) + return xerrors.Errorf("app not found") + } + app = agt.Apps[appIdx] + + // Build the URL + baseURL, err := url.Parse(region.PathAppURL) + if err != nil { + return xerrors.Errorf("failed to parse proxy URL: %w", err) + } + baseURL.Path = "" + pathAppURL := strings.TrimPrefix(region.PathAppURL, baseURL.String()) + appURL := buildAppLinkURL(baseURL, ws, agt, app, region.WildcardHostname, pathAppURL) + + if insideAWorkspace { + _, _ = fmt.Fprintf(inv.Stderr, "Please open the following URI on your local machine:\n\n") + _, _ = fmt.Fprintf(inv.Stdout, "%s\n", appURL) + return nil + } + _, _ = fmt.Fprintf(inv.Stderr, "Opening %s\n", appURL) + + if !testOpenError { + err = open.Run(appURL) + } else { + err = xerrors.New("test.open-error") + } + return err + }, + } + + cmd.Options = serpent.OptionSet{ + { + Flag: "preferred-region", + Env: "CODER_OPEN_APP_PREFERRED_REGION", + Description: fmt.Sprintf("Preferred region to use when opening the app." + + " By default, the app will be opened using the main Coder deployment (a.k.a. \"primary\")."), + Value: serpent.StringOf(&preferredRegion), + Default: "primary", + }, + { + Flag: "test.open-error", + Description: "Don't run the open command.", + Value: serpent.BoolOf(&testOpenError), + Hidden: true, // This is for testing! + }, + } + + return cmd +} + // waitForAgentCond uses the watch workspace API to update the agent information // until the condition is met. func waitForAgentCond(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace, workspaceAgent codersdk.WorkspaceAgent, cond func(codersdk.WorkspaceAgent) bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { @@ -337,3 +451,48 @@ func doAsync(f func()) (wait func()) { <-done } } + +// buildAppLinkURL returns the URL to open the app in the browser. +// It follows similar logic to the TypeScript implementation in site/src/utils/app.ts +// except that all URLs returned are absolute and based on the provided base URL. +func buildAppLinkURL(baseURL *url.URL, workspace codersdk.Workspace, agent codersdk.WorkspaceAgent, app codersdk.WorkspaceApp, appsHost, preferredPathBase string) string { + // If app is external, return the URL directly + if app.External { + return app.URL + } + + var u url.URL + u.Scheme = baseURL.Scheme + u.Host = baseURL.Host + // We redirect if we don't include a trailing slash, so we always include one to avoid extra roundtrips. + u.Path = fmt.Sprintf( + "%s/@%s/%s.%s/apps/%s/", + preferredPathBase, + workspace.OwnerName, + workspace.Name, + agent.Name, + url.PathEscape(app.Slug), + ) + // The frontend leaves the returns a relative URL for the terminal, but we don't have that luxury. + if app.Command != "" { + u.Path = fmt.Sprintf( + "%s/@%s/%s.%s/terminal", + preferredPathBase, + workspace.OwnerName, + workspace.Name, + agent.Name, + ) + q := u.Query() + q.Set("command", app.Command) + u.RawQuery = q.Encode() + // encodeURIComponent replaces spaces with %20 but url.QueryEscape replaces them with +. + // We replace them with %20 to match the TypeScript implementation. + u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20") + } + + if appsHost != "" && app.Subdomain && app.SubdomainName != "" { + u.Host = strings.Replace(appsHost, "*", app.SubdomainName, 1) + u.Path = "/" + } + return u.String() +} diff --git a/cli/open_internal_test.go b/cli/open_internal_test.go index 1f550156d43d0..7af4359a56bc2 100644 --- a/cli/open_internal_test.go +++ b/cli/open_internal_test.go @@ -1,6 +1,14 @@ package cli -import "testing" +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) func Test_resolveAgentAbsPath(t *testing.T) { t.Parallel() @@ -54,3 +62,107 @@ func Test_resolveAgentAbsPath(t *testing.T) { }) } } + +func Test_buildAppLinkURL(t *testing.T) { + t.Parallel() + + for _, tt := range []struct { + name string + // function arguments + baseURL string + workspace codersdk.Workspace + agent codersdk.WorkspaceAgent + app codersdk.WorkspaceApp + appsHost string + preferredPathBase string + // expected results + expectedLink string + }{ + { + name: "external url", + baseURL: "https://coder.tld", + app: codersdk.WorkspaceApp{ + External: true, + URL: "https://external-url.tld", + }, + expectedLink: "https://external-url.tld", + }, + { + name: "without subdomain", + baseURL: "https://coder.tld", + workspace: codersdk.Workspace{ + Name: "Test-Workspace", + OwnerName: "username", + }, + agent: codersdk.WorkspaceAgent{ + Name: "a-workspace-agent", + }, + app: codersdk.WorkspaceApp{ + Slug: "app-slug", + Subdomain: false, + }, + preferredPathBase: "/path-base", + expectedLink: "https://coder.tld/path-base/@username/Test-Workspace.a-workspace-agent/apps/app-slug/", + }, + { + name: "with command", + baseURL: "https://coder.tld", + workspace: codersdk.Workspace{ + Name: "Test-Workspace", + OwnerName: "username", + }, + agent: codersdk.WorkspaceAgent{ + Name: "a-workspace-agent", + }, + app: codersdk.WorkspaceApp{ + Command: "ls -la", + }, + expectedLink: "https://coder.tld/@username/Test-Workspace.a-workspace-agent/terminal?command=ls%20-la", + }, + { + name: "with subdomain", + baseURL: "ftps://coder.tld", + workspace: codersdk.Workspace{ + Name: "Test-Workspace", + OwnerName: "username", + }, + agent: codersdk.WorkspaceAgent{ + Name: "a-workspace-agent", + }, + app: codersdk.WorkspaceApp{ + Subdomain: true, + SubdomainName: "hellocoder", + }, + preferredPathBase: "/path-base", + appsHost: "*.apps-host.tld", + expectedLink: "ftps://hellocoder.apps-host.tld/", + }, + { + name: "with subdomain, but not apps host", + baseURL: "https://coder.tld", + workspace: codersdk.Workspace{ + Name: "Test-Workspace", + OwnerName: "username", + }, + agent: codersdk.WorkspaceAgent{ + Name: "a-workspace-agent", + }, + app: codersdk.WorkspaceApp{ + Slug: "app-slug", + Subdomain: true, + SubdomainName: "It really doesn't matter what this is without AppsHost.", + }, + preferredPathBase: "/path-base", + expectedLink: "https://coder.tld/path-base/@username/Test-Workspace.a-workspace-agent/apps/app-slug/", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + baseURL, err := url.Parse(tt.baseURL) + require.NoError(t, err) + actual := buildAppLinkURL(baseURL, tt.workspace, tt.agent, tt.app, tt.appsHost, tt.preferredPathBase) + assert.Equal(t, tt.expectedLink, actual) + }) + } +} diff --git a/cli/open_test.go b/cli/open_test.go index 6e32e8c49fa79..4e3c732dd4d52 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -33,7 +33,7 @@ func TestOpenVSCode(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken) - _ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + _ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() insideWorkspaceEnv := map[string]string{ "CODER": "true", @@ -168,7 +168,7 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { }) _ = agenttest.New(t, client.URL, agentToken) - _ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + _ = coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait() insideWorkspaceEnv := map[string]string{ "CODER": "true", @@ -283,3 +283,71 @@ func TestOpenVSCode_NoAgentDirectory(t *testing.T) { }) } } + +func TestOpenApp(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + client, ws, _ := setupWorkspaceForAgent(t, func(agents []*proto.Agent) []*proto.Agent { + agents[0].Apps = []*proto.App{ + { + Slug: "app1", + Url: "https://example.com/app1", + }, + } + return agents + }) + + inv, root := clitest.New(t, "open", "app", ws.Name, "app1", "--test.open-error") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + w := clitest.StartWithWaiter(t, inv) + w.RequireError() + w.RequireContains("test.open-error") + }) + + t.Run("AppNotFound", func(t *testing.T) { + t.Parallel() + + client, ws, _ := setupWorkspaceForAgent(t) + + inv, root := clitest.New(t, "open", "app", ws.Name, "app1") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + w := clitest.StartWithWaiter(t, inv) + w.RequireError() + w.RequireContains("app not found") + }) + + t.Run("RegionNotFound", func(t *testing.T) { + t.Parallel() + + client, ws, _ := setupWorkspaceForAgent(t, func(agents []*proto.Agent) []*proto.Agent { + agents[0].Apps = []*proto.App{ + { + Slug: "app1", + Url: "https://example.com/app1", + }, + } + return agents + }) + + inv, root := clitest.New(t, "open", "app", ws.Name, "app1", "--preferred-region", "bad-region") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + + w := clitest.StartWithWaiter(t, inv) + w.RequireError() + w.RequireContains("region not found") + }) +} diff --git a/cli/testdata/coder_open_--help.golden b/cli/testdata/coder_open_--help.golden index fe7eed1b886a9..b9e0d70906b59 100644 --- a/cli/testdata/coder_open_--help.golden +++ b/cli/testdata/coder_open_--help.golden @@ -6,6 +6,7 @@ USAGE: Open a workspace SUBCOMMANDS: + app Open a workspace application. vscode Open a workspace in VS Code Desktop ——— diff --git a/cli/testdata/coder_open_app_--help.golden b/cli/testdata/coder_open_app_--help.golden new file mode 100644 index 0000000000000..542d931d9568d --- /dev/null +++ b/cli/testdata/coder_open_app_--help.golden @@ -0,0 +1,14 @@ +coder v0.0.0-devel + +USAGE: + coder open app [flags] + + Open a workspace application. + +OPTIONS: + --preferred-region string, $CODER_OPEN_APP_PREFERRED_REGION (default: primary) + Preferred region to use when opening the app. By default, the app will + be opened using the main Coder deployment (a.k.a. "primary"). + +——— +Run `coder --help` for a list of global options. diff --git a/docs/manifest.json b/docs/manifest.json index f37f9a9db67f7..7b15d7ac81754 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1065,6 +1065,11 @@ "description": "Open a workspace", "path": "reference/cli/open.md" }, + { + "title": "open app", + "description": "Open a workspace application.", + "path": "reference/cli/open_app.md" + }, { "title": "open vscode", "description": "Open a workspace in VS Code Desktop", diff --git a/docs/reference/cli/open.md b/docs/reference/cli/open.md index e19bdaeba884d..0f54e4648e872 100644 --- a/docs/reference/cli/open.md +++ b/docs/reference/cli/open.md @@ -14,3 +14,4 @@ coder open | Name | Purpose | |-----------------------------------------|-------------------------------------| | [vscode](./open_vscode.md) | Open a workspace in VS Code Desktop | +| [app](./open_app.md) | Open a workspace application. | diff --git a/docs/reference/cli/open_app.md b/docs/reference/cli/open_app.md new file mode 100644 index 0000000000000..6d1f3b2befd2f --- /dev/null +++ b/docs/reference/cli/open_app.md @@ -0,0 +1,22 @@ + +# open app + +Open a workspace application. + +## Usage + +```console +coder open app [flags] +``` + +## Options + +### --preferred-region + +| | | +|-------------|-----------------------------------------------| +| Type | string | +| Environment | $CODER_OPEN_APP_PREFERRED_REGION | +| Default | primary | + +Preferred region to use when opening the app. By default, the app will be opened using the main Coder deployment (a.k.a. "primary"). From 56fc1ce1055d1c0f1497b113a195a68afc26bcd0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 21 Mar 2025 13:19:12 +0000 Subject: [PATCH 2/5] Update cli/open.go --- cli/open.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/open.go b/cli/open.go index 2955edfe98bdb..58fc8e841a264 100644 --- a/cli/open.go +++ b/cli/open.go @@ -232,9 +232,8 @@ func (r *RootCmd) openApp() *serpent.Command { ctx, cancel := context.WithCancel(inv.Context()) defer cancel() - // Check if we're inside a workspace, and especially inside _this_ - // workspace so we can perform path resolution/expansion. Generally, - // we know that if we're inside a workspace, `open` can't be used. + // Check if we're inside a workspace. Generally, we know + // that if we're inside a workspace, `open` can't be used. insideAWorkspace := inv.Environ.Get("CODER") == "true" // Fetch the preferred region. From f94b6feacf49105b870c965be243571d0b095a86 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 21 Mar 2025 15:04:23 +0000 Subject: [PATCH 3/5] address PR comments --- cli/open.go | 91 ++++++++++++++++++++++++++++-------------------- cli/open_test.go | 33 +++++++++++++++++- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/cli/open.go b/cli/open.go index 58fc8e841a264..97eb85441bccc 100644 --- a/cli/open.go +++ b/cli/open.go @@ -2,7 +2,9 @@ package cli import ( "context" + "errors" "fmt" + "net/http" "net/url" "path" "path/filepath" @@ -215,8 +217,8 @@ func (r *RootCmd) openVSCode() *serpent.Command { func (r *RootCmd) openApp() *serpent.Command { var ( - preferredRegion string - testOpenError bool + regionArg string + testOpenError bool ) client := new(codersdk.Client) @@ -225,69 +227,82 @@ func (r *RootCmd) openApp() *serpent.Command { Use: "app ", Short: "Open a workspace application.", Middleware: serpent.Chain( - serpent.RequireNArgs(2), r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { ctx, cancel := context.WithCancel(inv.Context()) defer cancel() - // Check if we're inside a workspace. Generally, we know - // that if we're inside a workspace, `open` can't be used. - insideAWorkspace := inv.Environ.Get("CODER") == "true" + if len(inv.Args) == 0 || len(inv.Args) > 2 { + return inv.Command.HelpHandler(inv) + } - // Fetch the preferred region. + workspaceName := inv.Args[0] + ws, agt, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName) + if err != nil { + var sdkErr *codersdk.Error + if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound { + cliui.Errorf(inv.Stderr, "Workspace %q not found!", workspaceName) + return sdkErr + } + cliui.Errorf(inv.Stderr, "Failed to get workspace and agent: %s", err) + return err + } + + allAppSlugs := make([]string, len(agt.Apps)) + for i, app := range agt.Apps { + allAppSlugs[i] = app.Slug + } + + // If a user doesn't specify an app slug, we'll just list the available + // apps and exit. + if len(inv.Args) == 1 { + cliui.Infof(inv.Stderr, "Available apps in %q: %v", workspaceName, allAppSlugs) + return nil + } + + appSlug := inv.Args[1] + var foundApp codersdk.WorkspaceApp + appIdx := slices.IndexFunc(agt.Apps, func(a codersdk.WorkspaceApp) bool { + return a.Slug == appSlug + }) + if appIdx == -1 { + cliui.Errorf(inv.Stderr, "App %q not found in workspace %q!\nAvailable apps: %v", appSlug, workspaceName, allAppSlugs) + return xerrors.Errorf("app not found") + } + foundApp = agt.Apps[appIdx] + + // To build the app URL, we need to know the wildcard hostname + // and path app URL for the region. regions, err := client.Regions(ctx) if err != nil { return xerrors.Errorf("failed to fetch regions: %w", err) } var region codersdk.Region preferredIdx := slices.IndexFunc(regions, func(r codersdk.Region) bool { - return r.Name == preferredRegion + return r.Name == regionArg }) if preferredIdx == -1 { allRegions := make([]string, len(regions)) for i, r := range regions { allRegions[i] = r.Name } - cliui.Errorf(inv.Stderr, "Preferred region %q not found!\nAvailable regions: %v", preferredRegion, allRegions) + cliui.Errorf(inv.Stderr, "Preferred region %q not found!\nAvailable regions: %v", regionArg, allRegions) return xerrors.Errorf("region not found") } region = regions[preferredIdx] - workspaceName := inv.Args[0] - appSlug := inv.Args[1] - - // Fetch the ws and agent - ws, agt, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName) - if err != nil { - return xerrors.Errorf("failed to get workspace and agent: %w", err) - } - - // Fetch the app - var app codersdk.WorkspaceApp - appIdx := slices.IndexFunc(agt.Apps, func(a codersdk.WorkspaceApp) bool { - return a.Slug == appSlug - }) - if appIdx == -1 { - appSlugs := make([]string, len(agt.Apps)) - for i, app := range agt.Apps { - appSlugs[i] = app.Slug - } - cliui.Errorf(inv.Stderr, "App %q not found in workspace %q!\nAvailable apps: %v", appSlug, workspaceName, appSlugs) - return xerrors.Errorf("app not found") - } - app = agt.Apps[appIdx] - - // Build the URL baseURL, err := url.Parse(region.PathAppURL) if err != nil { return xerrors.Errorf("failed to parse proxy URL: %w", err) } baseURL.Path = "" pathAppURL := strings.TrimPrefix(region.PathAppURL, baseURL.String()) - appURL := buildAppLinkURL(baseURL, ws, agt, app, region.WildcardHostname, pathAppURL) + appURL := buildAppLinkURL(baseURL, ws, agt, foundApp, region.WildcardHostname, pathAppURL) + // Check if we're inside a workspace. Generally, we know + // that if we're inside a workspace, `open` can't be used. + insideAWorkspace := inv.Environ.Get("CODER") == "true" if insideAWorkspace { _, _ = fmt.Fprintf(inv.Stderr, "Please open the following URI on your local machine:\n\n") _, _ = fmt.Fprintf(inv.Stdout, "%s\n", appURL) @@ -306,11 +321,11 @@ func (r *RootCmd) openApp() *serpent.Command { cmd.Options = serpent.OptionSet{ { - Flag: "preferred-region", + Flag: "region", Env: "CODER_OPEN_APP_PREFERRED_REGION", - Description: fmt.Sprintf("Preferred region to use when opening the app." + + Description: fmt.Sprintf("Region to use when opening the app." + " By default, the app will be opened using the main Coder deployment (a.k.a. \"primary\")."), - Value: serpent.StringOf(&preferredRegion), + Value: serpent.StringOf(®ionArg), Default: "primary", }, { diff --git a/cli/open_test.go b/cli/open_test.go index 4e3c732dd4d52..23a4316b75c31 100644 --- a/cli/open_test.go +++ b/cli/open_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -311,6 +312,36 @@ func TestOpenApp(t *testing.T) { w.RequireContains("test.open-error") }) + t.Run("OnlyWorkspaceName", func(t *testing.T) { + t.Parallel() + + client, ws, _ := setupWorkspaceForAgent(t) + inv, root := clitest.New(t, "open", "app", ws.Name) + clitest.SetupConfig(t, client, root) + var sb strings.Builder + inv.Stdout = &sb + inv.Stderr = &sb + + w := clitest.StartWithWaiter(t, inv) + w.RequireSuccess() + + require.Contains(t, sb.String(), "Available apps in") + }) + + t.Run("WorkspaceNotFound", func(t *testing.T) { + t.Parallel() + + client, _, _ := setupWorkspaceForAgent(t) + inv, root := clitest.New(t, "open", "app", "not-a-workspace", "app1") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = pty.Input() + inv.Stdout = pty.Output() + w := clitest.StartWithWaiter(t, inv) + w.RequireError() + w.RequireContains("Resource not found or you do not have access to this resource") + }) + t.Run("AppNotFound", func(t *testing.T) { t.Parallel() @@ -340,7 +371,7 @@ func TestOpenApp(t *testing.T) { return agents }) - inv, root := clitest.New(t, "open", "app", ws.Name, "app1", "--preferred-region", "bad-region") + inv, root := clitest.New(t, "open", "app", ws.Name, "app1", "--region", "bad-region") clitest.SetupConfig(t, client, root) pty := ptytest.New(t) inv.Stdin = pty.Input() From c7a942be4321b44a090c8a1e277d3860a6e4b4ba Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 21 Mar 2025 15:08:03 +0000 Subject: [PATCH 4/5] make gen --- cli/open.go | 2 +- cli/testdata/coder_open_app_--help.golden | 6 +++--- docs/reference/cli/open_app.md | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cli/open.go b/cli/open.go index 97eb85441bccc..d323f8e5dec82 100644 --- a/cli/open.go +++ b/cli/open.go @@ -322,7 +322,7 @@ func (r *RootCmd) openApp() *serpent.Command { cmd.Options = serpent.OptionSet{ { Flag: "region", - Env: "CODER_OPEN_APP_PREFERRED_REGION", + Env: "CODER_OPEN_APP_REGION", Description: fmt.Sprintf("Region to use when opening the app." + " By default, the app will be opened using the main Coder deployment (a.k.a. \"primary\")."), Value: serpent.StringOf(®ionArg), diff --git a/cli/testdata/coder_open_app_--help.golden b/cli/testdata/coder_open_app_--help.golden index 542d931d9568d..c648e88d058a5 100644 --- a/cli/testdata/coder_open_app_--help.golden +++ b/cli/testdata/coder_open_app_--help.golden @@ -6,9 +6,9 @@ USAGE: Open a workspace application. OPTIONS: - --preferred-region string, $CODER_OPEN_APP_PREFERRED_REGION (default: primary) - Preferred region to use when opening the app. By default, the app will - be opened using the main Coder deployment (a.k.a. "primary"). + --region string, $CODER_OPEN_APP_REGION (default: primary) + Region to use when opening the app. By default, the app will be opened + using the main Coder deployment (a.k.a. "primary"). ——— Run `coder --help` for a list of global options. diff --git a/docs/reference/cli/open_app.md b/docs/reference/cli/open_app.md index 6d1f3b2befd2f..1edd274815c52 100644 --- a/docs/reference/cli/open_app.md +++ b/docs/reference/cli/open_app.md @@ -11,12 +11,12 @@ coder open app [flags] ## Options -### --preferred-region +### --region -| | | -|-------------|-----------------------------------------------| -| Type | string | -| Environment | $CODER_OPEN_APP_PREFERRED_REGION | -| Default | primary | +| | | +|-------------|-------------------------------------| +| Type | string | +| Environment | $CODER_OPEN_APP_REGION | +| Default | primary | -Preferred region to use when opening the app. By default, the app will be opened using the main Coder deployment (a.k.a. "primary"). +Region to use when opening the app. By default, the app will be opened using the main Coder deployment (a.k.a. "primary"). From 598dec612d9475c2e467c2ab377e9d9a62dc8d6e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 21 Mar 2025 15:14:39 +0000 Subject: [PATCH 5/5] sort ALL the things --- cli/open.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/open.go b/cli/open.go index d323f8e5dec82..10a58f1c3693a 100644 --- a/cli/open.go +++ b/cli/open.go @@ -253,6 +253,7 @@ func (r *RootCmd) openApp() *serpent.Command { for i, app := range agt.Apps { allAppSlugs[i] = app.Slug } + slices.Sort(allAppSlugs) // If a user doesn't specify an app slug, we'll just list the available // apps and exit. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy