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)
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