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)