Skip to content

Commit 3d3dc08

Browse files
committed
Attempt to add functionality to retrieve workspace build logs to CLI
1 parent f41275e commit 3d3dc08

File tree

4 files changed

+183
-0
lines changed

4 files changed

+183
-0
lines changed

cli/builds.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"time"
7+
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/serpent"
13+
)
14+
15+
type workspaceBuildListRow struct {
16+
codersdk.WorkspaceBuild `table:"-"`
17+
18+
BuildNumber string `json:"-" table:"build,default_sort"`
19+
BuildID string `json:"-" table:"build id"`
20+
Status string `json:"-" table:"status"`
21+
Reason string `json:"-" table:"reason"`
22+
CreatedAt string `json:"-" table:"created"`
23+
Duration string `json:"-" table:"duration"`
24+
}
25+
26+
func workspaceBuildListRowFromBuild(build codersdk.WorkspaceBuild) workspaceBuildListRow {
27+
status := codersdk.WorkspaceDisplayStatus(build.Job.Status, build.Transition)
28+
createdAt := build.CreatedAt.Format("2006-01-02 15:04:05")
29+
30+
duration := ""
31+
if build.Job.CompletedAt != nil {
32+
duration = build.Job.CompletedAt.Sub(build.CreatedAt).Truncate(time.Second).String()
33+
}
34+
35+
return workspaceBuildListRow{
36+
WorkspaceBuild: build,
37+
BuildNumber: strconv.Itoa(int(build.BuildNumber)),
38+
BuildID: build.ID.String(),
39+
Status: status,
40+
Reason: string(build.Reason),
41+
CreatedAt: createdAt,
42+
Duration: duration,
43+
}
44+
}
45+
46+
func (r *RootCmd) builds() *serpent.Command {
47+
return &serpent.Command{
48+
Use: "builds",
49+
Short: "Manage workspace builds",
50+
Children: []*serpent.Command{
51+
r.buildsList(),
52+
},
53+
}
54+
}
55+
56+
func (r *RootCmd) buildsList() *serpent.Command {
57+
var (
58+
formatter = cliui.NewOutputFormatter(
59+
cliui.TableFormat(
60+
[]workspaceBuildListRow{},
61+
[]string{"build", "build id", "status", "reason", "created", "duration"},
62+
),
63+
cliui.JSONFormat(),
64+
)
65+
)
66+
client := new(codersdk.Client)
67+
cmd := &serpent.Command{
68+
Annotations: workspaceCommand,
69+
Use: "list <workspace>",
70+
Short: "List builds for a workspace",
71+
Aliases: []string{"ls"},
72+
Middleware: serpent.Chain(
73+
serpent.RequireNArgs(1),
74+
r.InitClient(client),
75+
),
76+
Handler: func(inv *serpent.Invocation) error {
77+
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0])
78+
if err != nil {
79+
return xerrors.Errorf("get workspace: %w", err)
80+
}
81+
82+
builds, err := client.WorkspaceBuildsByWorkspaceID(inv.Context(), workspace.ID)
83+
if err != nil {
84+
return xerrors.Errorf("get workspace builds: %w", err)
85+
}
86+
87+
rows := make([]workspaceBuildListRow, len(builds))
88+
for i, build := range builds {
89+
rows[i] = workspaceBuildListRowFromBuild(build)
90+
}
91+
92+
out, err := formatter.Format(inv.Context(), rows)
93+
if err != nil {
94+
return err
95+
}
96+
97+
_, err = fmt.Fprintln(inv.Stdout, out)
98+
return err
99+
},
100+
}
101+
formatter.AttachOptions(&cmd.Options)
102+
return cmd
103+
}

cli/logs.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/uuid"
7+
"golang.org/x/xerrors"
8+
9+
"github.com/coder/coder/v2/codersdk"
10+
"github.com/coder/serpent"
11+
)
12+
13+
func (r *RootCmd) logs() *serpent.Command {
14+
var follow bool
15+
client := new(codersdk.Client)
16+
cmd := &serpent.Command{
17+
Annotations: workspaceCommand,
18+
Use: "logs <build-id>",
19+
Short: "Show logs for a workspace build",
20+
Middleware: serpent.Chain(
21+
serpent.RequireNArgs(1),
22+
r.InitClient(client),
23+
),
24+
Handler: func(inv *serpent.Invocation) error {
25+
buildIDStr := inv.Args[0]
26+
buildID, err := uuid.Parse(buildIDStr)
27+
if err != nil {
28+
return xerrors.Errorf("invalid build ID %q: %w", buildIDStr, err)
29+
}
30+
31+
logs, closer, err := client.WorkspaceBuildLogsAfter(inv.Context(), buildID, 0)
32+
if err != nil {
33+
return xerrors.Errorf("get build logs: %w", err)
34+
}
35+
defer closer.Close()
36+
37+
for {
38+
log, ok := <-logs
39+
if !ok {
40+
break
41+
}
42+
43+
// Simple format with timestamp and stage
44+
timestamp := log.CreatedAt.Format("15:04:05")
45+
if log.Stage != "" {
46+
_, _ = fmt.Fprintf(inv.Stdout, "[%s] %s: %s\n",
47+
timestamp, log.Stage, log.Output)
48+
} else {
49+
_, _ = fmt.Fprintf(inv.Stdout, "[%s] %s\n",
50+
timestamp, log.Output)
51+
}
52+
}
53+
return nil
54+
},
55+
}
56+
57+
cmd.Options = serpent.OptionSet{
58+
{
59+
Flag: "follow",
60+
FlagShorthand: "f",
61+
Description: "Follow log output (stream real-time logs).",
62+
Value: serpent.BoolOf(&follow),
63+
},
64+
}
65+
66+
return cmd
67+
}

cli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,13 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
107107

108108
// Workspace Commands
109109
r.autoupdate(),
110+
r.builds(),
110111
r.configSSH(),
111112
r.create(),
112113
r.deleteWorkspace(),
113114
r.favorite(),
114115
r.list(),
116+
r.logs(),
115117
r.open(),
116118
r.ping(),
117119
r.rename(),

codersdk/workspacebuilds.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,14 @@ func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (Wo
279279
var timings WorkspaceBuildTimings
280280
return timings, json.NewDecoder(res.Body).Decode(&timings)
281281
}
282+
283+
func (c *Client) WorkspaceBuildsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) {
284+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspaceID), nil)
285+
if err != nil {
286+
return nil, err
287+
}
288+
defer res.Body.Close()
289+
290+
var builds []WorkspaceBuild
291+
return builds, json.NewDecoder(res.Body).Decode(&builds)
292+
}

0 commit comments

Comments
 (0)