From 3addc9497e7e59280b0e5ea7445025902d82eafd Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 17 Jan 2024 00:51:27 +0000 Subject: [PATCH 1/2] fix: avoid returning 500 on apps when workspace stopped --- coderd/workspaceapps/apptest/apptest.go | 19 ++++++++++++++ coderd/workspaceapps/db.go | 3 +++ coderd/workspaceapps/errors.go | 27 ++++++++++++++++++++ coderd/workspaceapps/request.go | 11 +++++++- site/src/pages/TerminalPage/TerminalPage.tsx | 2 +- 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index a8c593c1ec89d..a38189a1ff25c 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -26,6 +26,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" @@ -1484,6 +1485,24 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { assert.Equal(t, "test-app-owner", stats[0].SlugOrPort) assert.Equal(t, 1, stats[0].Requests) }) + + t.Run("WorkspaceOffline", func(t *testing.T) { + t.Parallel() + + appDetails := setupProxyTest(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + u := appDetails.PathAppURL(appDetails.Apps.Owner) + resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + }) } type fakeStatsReporter struct { diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 9b196a4b7480e..b17c4a4a05c69 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -103,6 +103,9 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * if xerrors.Is(err, sql.ErrNoRows) { WriteWorkspaceApp404(p.Logger, p.DashboardURL, rw, r, &appReq, nil, err.Error()) return nil, "", false + } else if xerrors.Is(err, errWorkspaceStopped) { + WriteWorkspaceOffline(p.Logger, p.DashboardURL, rw, r, &appReq) + return nil, "", false } else if err != nil { WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "get app details from database") return nil, "", false diff --git a/coderd/workspaceapps/errors.go b/coderd/workspaceapps/errors.go index bcc890c81e89a..64d61de3678ed 100644 --- a/coderd/workspaceapps/errors.go +++ b/coderd/workspaceapps/errors.go @@ -1,10 +1,12 @@ package workspaceapps import ( + "fmt" "net/http" "net/url" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/site" ) @@ -90,3 +92,28 @@ func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.Respo DashboardURL: accessURL.String(), }) } + +// WriteWorkspaceOffline writes a HTML 400 error page for a workspace app. If +// appReq is not nil, it will be used to log the request details at debug level. +func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request) { + if appReq != nil { + slog.Helper() + log.Debug(r.Context(), + "workspace app unavailable: workspace stopped", + slog.F("username_or_id", appReq.UsernameOrID), + slog.F("workspace_and_agent", appReq.WorkspaceAndAgent), + slog.F("workspace_name_or_id", appReq.WorkspaceNameOrID), + slog.F("agent_name_or_id", appReq.AgentNameOrID), + slog.F("app_slug_or_port", appReq.AppSlugOrPort), + slog.F("hostname_prefix", appReq.Prefix), + ) + } + + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Workspace Offline", + Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop), + RetryEnabled: false, + DashboardURL: accessURL.String(), + }) +} diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index c46413d22961f..834ed6f449b2f 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -17,6 +17,8 @@ import ( "github.com/coder/coder/v2/codersdk" ) +var errWorkspaceStopped = xerrors.New("stopped workspace") + type AccessMethod string const ( @@ -260,10 +262,17 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR if err != nil { return nil, xerrors.Errorf("get workspace agents: %w", err) } + build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest workspace build: %w", err) + } + if build.Transition == database.WorkspaceTransitionStop { + return nil, errWorkspaceStopped + } if len(agents) == 0 { // TODO(@deansheather): return a 404 if there are no agents in the // workspace, requires a different error type. - return nil, xerrors.New("no agents in workspace") + return nil, xerrors.Errorf("no agents in workspace: %w", sql.ErrNoRows) } // Get workspace apps. diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index e86d874006a94..dd33622613161 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -191,7 +191,7 @@ const TerminalPage: FC = () => { return; } else if (!workspaceAgent) { terminal.writeln( - Language.workspaceAgentErrorMessagePrefix + "no agent found with ID", + Language.workspaceAgentErrorMessagePrefix + "no agent found with ID, is the workspace started?", ); return; } From c9d547817d289f3d076af95b8fe7bbb44e94f58e Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 17 Jan 2024 00:58:17 +0000 Subject: [PATCH 2/2] make fmt --- site/src/pages/TerminalPage/TerminalPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index dd33622613161..8e6dbcdbcdb05 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -191,7 +191,8 @@ const TerminalPage: FC = () => { return; } else if (!workspaceAgent) { terminal.writeln( - Language.workspaceAgentErrorMessagePrefix + "no agent found with ID, is the workspace started?", + Language.workspaceAgentErrorMessagePrefix + + "no agent found with ID, is the workspace started?", ); return; }