From 2c2f3c1c6d6ea65bfe14bea0701effc7c6fe7ace Mon Sep 17 00:00:00 2001 From: Rowan Smith Date: Thu, 24 Jul 2025 15:05:40 +1000 Subject: [PATCH] feat(cli): add 'read' command for authenticated API endpoint reads with pretty JSON output --- cli/read.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ cli/read_test.go | 50 ++++++++++++++++++++++++++++++ cli/root.go | 1 + 3 files changed, 130 insertions(+) create mode 100644 cli/read.go create mode 100644 cli/read_test.go diff --git a/cli/read.go b/cli/read.go new file mode 100644 index 0000000000000..11d286ffe9f58 --- /dev/null +++ b/cli/read.go @@ -0,0 +1,79 @@ +package cli + +import ( + "encoding/json" + "io" + "net/http" + "strings" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +// read returns a CLI command that performs an authenticated GET request to the given API path. +func (r *RootCmd) read() *serpent.Command { + client := new(codersdk.Client) + return &serpent.Command{ + Use: "read ", + Short: "Read an authenticated API endpoint using your current Coder CLI token", + Long: `Read an authenticated API endpoint using your current Coder CLI token. + +Example: + coder read workspacebuilds/my-build/logs +This will perform a GET request to /api/v2/workspacebuilds/my-build/logs on the connected Coder server. +`, + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + apiPath := inv.Args[0] + if !strings.HasPrefix(apiPath, "/") { + apiPath = "/api/v2/" + apiPath + } + resp, err := client.Request(inv.Context(), http.MethodGet, apiPath, nil) + if err != nil { + return xerrors.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return xerrors.Errorf("API error: %s\n%s", resp.Status, string(body)) + } + + contentType := resp.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "application/json") { + // Pretty-print JSON + var raw interface{} + data, err := io.ReadAll(resp.Body) + if err != nil { + return xerrors.Errorf("failed to read response: %w", err) + } + err = json.Unmarshal(data, &raw) + if err == nil { + pretty, err := json.MarshalIndent(raw, "", " ") + if err == nil { + _, err = inv.Stdout.Write(pretty) + if err != nil { + return xerrors.Errorf("failed to write output: %w", err) + } + _, _ = inv.Stdout.Write([]byte("\n")) + return nil + } + } + // If JSON formatting fails, fall back to raw output + _, _ = inv.Stdout.Write(data) + _, _ = inv.Stdout.Write([]byte("\n")) + return nil + } + // Non-JSON: stream as before + _, err = io.Copy(inv.Stdout, resp.Body) + if err != nil { + return xerrors.Errorf("failed to read response: %w", err) + } + return nil + }, + } +} diff --git a/cli/read_test.go b/cli/read_test.go new file mode 100644 index 0000000000000..3aa6b59e5d61f --- /dev/null +++ b/cli/read_test.go @@ -0,0 +1,50 @@ +package cli_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" +) + +func TestReadCommand(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + inv, root := clitest.New(t, "read", "users/me") + clitest.SetupConfig(t, client, root) + + var sb strings.Builder + inv.Stdout = &sb + + err := inv.Run() + require.NoError(t, err) + output := sb.String() + require.Contains(t, output, user.UserID.String()) + // Check for pretty-printed JSON (indented) + require.Contains(t, output, " \"") // at least one indented JSON key +} + +func TestReadCommand_NonJSON(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + inv, root := clitest.New(t, "read", "/healthz") + clitest.SetupConfig(t, client, root) + + var sb strings.Builder + inv.Stdout = &sb + + err := inv.Run() + require.NoError(t, err) + output := sb.String() + // Should not be pretty-printed JSON (no two-space indent at start) + require.NotContains(t, output, " \"") + // Should contain the plain text OK + require.Contains(t, output, "OK") +} diff --git a/cli/root.go b/cli/root.go index 54215a67401dd..58d903993f48d 100644 --- a/cli/root.go +++ b/cli/root.go @@ -98,6 +98,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { r.organizations(), r.portForward(), r.publickey(), + r.read(), r.resetPassword(), r.state(), r.templates(), 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