Skip to content

Commit e313bdf

Browse files
Rowan SmithRowan Smith
authored andcommitted
feat(cli): add 'read' command for authenticated API endpoint reads with pretty JSON output
1 parent f41275e commit e313bdf

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

cli/read.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"strings"
8+
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/serpent"
13+
)
14+
15+
// read returns a CLI command that performs an authenticated GET request to the given API path.
16+
func (r *RootCmd) read() *serpent.Command {
17+
client := new(codersdk.Client)
18+
return &serpent.Command{
19+
Use: "read <api-path>",
20+
Short: "Read an authenticated API endpoint using your current Coder CLI token",
21+
Long: `Read an authenticated API endpoint using your current Coder CLI token.
22+
23+
Example:
24+
coder read workspacebuilds/my-build/logs
25+
This will perform a GET request to /api/v2/workspacebuilds/my-build/logs on the connected Coder server.
26+
`,
27+
Middleware: serpent.Chain(
28+
serpent.RequireNArgs(1),
29+
r.InitClient(client),
30+
),
31+
Handler: func(inv *serpent.Invocation) error {
32+
apiPath := inv.Args[0]
33+
if !strings.HasPrefix(apiPath, "/") {
34+
apiPath = "/api/v2/" + apiPath
35+
}
36+
resp, err := client.Request(inv.Context(), http.MethodGet, apiPath, nil)
37+
if err != nil {
38+
return xerrors.Errorf("request failed: %w", err)
39+
}
40+
defer resp.Body.Close()
41+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
42+
body, _ := io.ReadAll(resp.Body)
43+
return xerrors.Errorf("API error: %s\n%s", resp.Status, string(body))
44+
}
45+
46+
contentType := resp.Header.Get("Content-Type")
47+
if strings.HasPrefix(contentType, "application/json") {
48+
// Pretty-print JSON
49+
var raw interface{}
50+
data, err := io.ReadAll(resp.Body)
51+
if err != nil {
52+
return xerrors.Errorf("failed to read response: %w", err)
53+
}
54+
err = json.Unmarshal(data, &raw)
55+
if err == nil {
56+
pretty, err := json.MarshalIndent(raw, "", " ")
57+
if err == nil {
58+
_, err = inv.Stdout.Write(pretty)
59+
if err != nil {
60+
return xerrors.Errorf("failed to write output: %w", err)
61+
}
62+
_, _ = inv.Stdout.Write([]byte("\n"))
63+
return nil
64+
}
65+
}
66+
// If JSON formatting fails, fall back to raw output
67+
_, _ = inv.Stdout.Write(data)
68+
_, _ = inv.Stdout.Write([]byte("\n"))
69+
return nil
70+
}
71+
// Non-JSON: stream as before
72+
_, err = io.Copy(inv.Stdout, resp.Body)
73+
if err != nil {
74+
return xerrors.Errorf("failed to read response: %w", err)
75+
}
76+
return nil
77+
},
78+
}
79+
}

cli/read_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package cli_test
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
)
12+
13+
func TestReadCommand(t *testing.T) {
14+
t.Parallel()
15+
client := coderdtest.New(t, nil)
16+
user := coderdtest.CreateFirstUser(t, client)
17+
18+
inv, root := clitest.New(t, "read", "users/me")
19+
clitest.SetupConfig(t, client, root)
20+
21+
var sb strings.Builder
22+
inv.Stdout = &sb
23+
24+
err := inv.Run()
25+
require.NoError(t, err)
26+
output := sb.String()
27+
require.Contains(t, output, user.UserID.String())
28+
// Check for pretty-printed JSON (indented)
29+
require.Contains(t, output, " \"") // at least one indented JSON key
30+
}
31+
32+
func TestReadCommand_NonJSON(t *testing.T) {
33+
t.Parallel()
34+
client := coderdtest.New(t, nil)
35+
_ = coderdtest.CreateFirstUser(t, client)
36+
37+
inv, root := clitest.New(t, "read", "/healthz")
38+
clitest.SetupConfig(t, client, root)
39+
40+
var sb strings.Builder
41+
inv.Stdout = &sb
42+
43+
err := inv.Run()
44+
require.NoError(t, err)
45+
output := sb.String()
46+
// Should not be pretty-printed JSON (no two-space indent at start)
47+
require.NotContains(t, output, " \"")
48+
// Should contain the plain text OK
49+
require.Contains(t, output, "OK")
50+
}

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
9898
r.organizations(),
9999
r.portForward(),
100100
r.publickey(),
101+
r.read(),
101102
r.resetPassword(),
102103
r.state(),
103104
r.templates(),

0 commit comments

Comments
 (0)
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