diff --git a/agent/agent.go b/agent/agent.go index 820856d42309a..d2f0f7203fcb1 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1699,11 +1699,61 @@ func (a *agent) HandleHTTPMagicsockDebugLoggingState(w http.ResponseWriter, r *h _, _ = fmt.Fprintf(w, "updated magicsock debug logging to %v", stateBool) } +func (a *agent) HandleHTTPDebugManifest(w http.ResponseWriter, r *http.Request) { + sdkManifest := a.manifest.Load() + if sdkManifest == nil { + a.logger.Error(r.Context(), "no manifest in-memory") + w.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprintf(w, "no manifest in-memory") + return + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(sdkManifest); err != nil { + a.logger.Error(a.hardCtx, "write debug manifest", slog.Error(err)) + } +} + +func (a *agent) HandleHTTPDebugToken(w http.ResponseWriter, r *http.Request) { + tok := a.sessionToken.Load() + if tok == nil { + a.logger.Error(r.Context(), "no session token in-memory") + w.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprintf(w, "no session token in-memory") + return + } + w.WriteHeader(http.StatusOK) + _, _ = fmt.Fprintf(w, *tok) +} + +func (a *agent) HandleHTTPDebugLogs(w http.ResponseWriter, r *http.Request) { + logPath := filepath.Join(a.logDir, "coder-agent.log") + f, err := os.Open(logPath) + if err != nil { + a.logger.Error(r.Context(), "open agent log file", slog.Error(err), slog.F("path", logPath)) + w.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprintf(w, "could not open log file: %s", err) + return + } + defer f.Close() + + // Limit to 10MB. + w.WriteHeader(http.StatusOK) + _, err = io.Copy(w, io.LimitReader(f, 10*1024*1024)) + if err != nil && !errors.Is(err, io.EOF) { + a.logger.Error(r.Context(), "read agent log file", slog.Error(err)) + return + } +} + func (a *agent) HTTPDebug() http.Handler { r := chi.NewRouter() + r.Get("/debug/logs", a.HandleHTTPDebugLogs) r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock) r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState) + r.Get("/debug/manifest", a.HandleHTTPDebugManifest) + r.Get("/debug/token", a.HandleHTTPDebugToken) r.NotFound(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte("404 not found")) diff --git a/agent/agent_test.go b/agent/agent_test.go index 5626f52c9311b..d0fd422452e8d 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -55,6 +55,7 @@ import ( "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/tailnettest" @@ -1974,11 +1975,21 @@ func TestAgent_WriteVSCodeConfigs(t *testing.T) { func TestAgent_DebugServer(t *testing.T) { t.Parallel() + logDir := t.TempDir() + logPath := filepath.Join(logDir, "coder-agent.log") + randLogStr, err := cryptorand.String(32) + require.NoError(t, err) + require.NoError(t, os.WriteFile(logPath, []byte(randLogStr), 0o600)) derpMap, _ := tailnettest.RunDERPAndSTUN(t) //nolint:dogsled conn, _, _, _, agnt := setupAgent(t, agentsdk.Manifest{ DERPMap: derpMap, - }, 0) + }, 0, func(c *agenttest.Client, o *agent.Options) { + o.ExchangeToken = func(context.Context) (string, error) { + return "token", nil + } + o.LogDir = logDir + }) awaitReachableCtx := testutil.Context(t, testutil.WaitLong) ok := conn.AwaitReachable(awaitReachableCtx) @@ -2059,6 +2070,56 @@ func TestAgent_DebugServer(t *testing.T) { require.Contains(t, string(resBody), `invalid state "blah", must be a boolean`) }) }) + + t.Run("Manifest", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/manifest", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + var v agentsdk.Manifest + require.NoError(t, json.NewDecoder(res.Body).Decode(&v)) + require.NotNil(t, v) + }) + + t.Run("Token", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/token", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + defer res.Body.Close() + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, "token", string(resBody)) + }) + + t.Run("Logs", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"/debug/logs", nil) + require.NoError(t, err) + + res, err := srv.Client().Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + defer res.Body.Close() + resBody, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NotEmpty(t, string(resBody)) + require.Contains(t, string(resBody), randLogStr) + }) } func TestAgent_ScriptLogging(t *testing.T) { diff --git a/agent/api.go b/agent/api.go index b81a7b9b44e41..59988e8f06deb 100644 --- a/agent/api.go +++ b/agent/api.go @@ -36,8 +36,11 @@ func (a *agent) apiHandler() http.Handler { cacheDuration: cacheDuration, } r.Get("/api/v0/listening-ports", lp.handler) + r.Get("/debug/logs", a.HandleHTTPDebugLogs) r.Get("/debug/magicsock", a.HandleHTTPDebugMagicsock) r.Get("/debug/magicsock/debug-logging/{state}", a.HandleHTTPMagicsockDebugLoggingState) + r.Get("/debug/manifest", a.HandleHTTPDebugManifest) + r.Get("/debug/token", a.HandleHTTPDebugToken) return r } diff --git a/codersdk/workspaceagentconn.go b/codersdk/workspaceagentconn.go index e5451052bcd7e..09bed58fe66b0 100644 --- a/codersdk/workspaceagentconn.go +++ b/codersdk/workspaceagentconn.go @@ -372,6 +372,39 @@ func (c *WorkspaceAgentConn) DebugMagicsock(ctx context.Context) ([]byte, error) return bs, nil } +// DebugManifest returns the agent's in-memory manifest. Unfortunately this must +// be returns as a []byte to avoid an import cycle. +func (c *WorkspaceAgentConn) DebugManifest(ctx context.Context) ([]byte, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + res, err := c.apiRequest(ctx, http.MethodGet, "/debug/manifest", nil) + if err != nil { + return nil, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + bs, err := io.ReadAll(res.Body) + if err != nil { + return nil, xerrors.Errorf("read response body: %w", err) + } + return bs, nil +} + +// DebugLogs returns up to the last 10MB of `/tmp/coder-agent.log` +func (c *WorkspaceAgentConn) DebugLogs(ctx context.Context) ([]byte, error) { + ctx, span := tracing.StartSpan(ctx) + defer span.End() + res, err := c.apiRequest(ctx, http.MethodGet, "/debug/logs", nil) + if err != nil { + return nil, xerrors.Errorf("do request: %w", err) + } + defer res.Body.Close() + bs, err := io.ReadAll(res.Body) + if err != nil { + return nil, xerrors.Errorf("read response body: %w", err) + } + return bs, nil +} + // apiRequest makes a request to the workspace agent's HTTP API server. func (c *WorkspaceAgentConn) apiRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { ctx, span := tracing.StartSpan(ctx)