Skip to content

feat(cli): add 'read' command for querying API endpoints via Coder CLI #19027

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions cli/read.go
Original file line number Diff line number Diff line change
@@ -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 <api-path>",
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
},
}
}
50 changes: 50 additions & 0 deletions cli/read_test.go
Original file line number Diff line number Diff line change
@@ -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)

Check failure on line 19 in cli/read_test.go

View workflow job for this annotation

GitHub Actions / lint

ruleguard: The CLI will be operating as the owner user, which has unrestricted permissions. Consider creating a different user. (gocritic)

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")
}
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
r.organizations(),
r.portForward(),
r.publickey(),
r.read(),
r.resetPassword(),
r.state(),
r.templates(),
Expand Down
Loading
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