diff --git a/cli/builds.go b/cli/builds.go new file mode 100644 index 0000000000000..8ad463f8c05d6 --- /dev/null +++ b/cli/builds.go @@ -0,0 +1,103 @@ +package cli + +import ( + "fmt" + "strconv" + "time" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +type workspaceBuildListRow struct { + codersdk.WorkspaceBuild `table:"-"` + + BuildNumber string `json:"-" table:"build,default_sort"` + BuildID string `json:"-" table:"build id"` + Status string `json:"-" table:"status"` + Reason string `json:"-" table:"reason"` + CreatedAt string `json:"-" table:"created"` + Duration string `json:"-" table:"duration"` +} + +func workspaceBuildListRowFromBuild(build codersdk.WorkspaceBuild) workspaceBuildListRow { + status := codersdk.WorkspaceDisplayStatus(build.Job.Status, build.Transition) + createdAt := build.CreatedAt.Format("2006-01-02 15:04:05") + + duration := "" + if build.Job.CompletedAt != nil { + duration = build.Job.CompletedAt.Sub(build.CreatedAt).Truncate(time.Second).String() + } + + return workspaceBuildListRow{ + WorkspaceBuild: build, + BuildNumber: strconv.Itoa(int(build.BuildNumber)), + BuildID: build.ID.String(), + Status: status, + Reason: string(build.Reason), + CreatedAt: createdAt, + Duration: duration, + } +} + +func (r *RootCmd) builds() *serpent.Command { + return &serpent.Command{ + Use: "builds", + Short: "Manage workspace builds", + Children: []*serpent.Command{ + r.buildsList(), + }, + } +} + +func (r *RootCmd) buildsList() *serpent.Command { + var ( + formatter = cliui.NewOutputFormatter( + cliui.TableFormat( + []workspaceBuildListRow{}, + []string{"build", "build id", "status", "reason", "created", "duration"}, + ), + cliui.JSONFormat(), + ) + ) + client := new(codersdk.Client) + cmd := &serpent.Command{ + Annotations: workspaceCommand, + Use: "list ", + Short: "List builds for a workspace", + Aliases: []string{"ls"}, + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) + if err != nil { + return xerrors.Errorf("get workspace: %w", err) + } + + builds, err := client.WorkspaceBuildsByWorkspaceID(inv.Context(), workspace.ID) + if err != nil { + return xerrors.Errorf("get workspace builds: %w", err) + } + + rows := make([]workspaceBuildListRow, len(builds)) + for i, build := range builds { + rows[i] = workspaceBuildListRowFromBuild(build) + } + + out, err := formatter.Format(inv.Context(), rows) + if err != nil { + return err + } + + _, err = fmt.Fprintln(inv.Stdout, out) + return err + }, + } + formatter.AttachOptions(&cmd.Options) + return cmd +} diff --git a/cli/logs.go b/cli/logs.go new file mode 100644 index 0000000000000..d44b8a63edda5 --- /dev/null +++ b/cli/logs.go @@ -0,0 +1,67 @@ +package cli + +import ( + "fmt" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +func (r *RootCmd) logs() *serpent.Command { + var follow bool + client := new(codersdk.Client) + cmd := &serpent.Command{ + Annotations: workspaceCommand, + Use: "logs ", + Short: "Show logs for a workspace build", + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + r.InitClient(client), + ), + Handler: func(inv *serpent.Invocation) error { + buildIDStr := inv.Args[0] + buildID, err := uuid.Parse(buildIDStr) + if err != nil { + return xerrors.Errorf("invalid build ID %q: %w", buildIDStr, err) + } + + logs, closer, err := client.WorkspaceBuildLogsAfter(inv.Context(), buildID, 0) + if err != nil { + return xerrors.Errorf("get build logs: %w", err) + } + defer closer.Close() + + for { + log, ok := <-logs + if !ok { + break + } + + // Simple format with timestamp and stage + timestamp := log.CreatedAt.Format("15:04:05") + if log.Stage != "" { + _, _ = fmt.Fprintf(inv.Stdout, "[%s] %s: %s\n", + timestamp, log.Stage, log.Output) + } else { + _, _ = fmt.Fprintf(inv.Stdout, "[%s] %s\n", + timestamp, log.Output) + } + } + return nil + }, + } + + cmd.Options = serpent.OptionSet{ + { + Flag: "follow", + FlagShorthand: "f", + Description: "Follow log output (stream real-time logs).", + Value: serpent.BoolOf(&follow), + }, + } + + return cmd +} diff --git a/cli/root.go b/cli/root.go index 54215a67401dd..19ed4c07fe825 100644 --- a/cli/root.go +++ b/cli/root.go @@ -107,11 +107,13 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { // Workspace Commands r.autoupdate(), + r.builds(), r.configSSH(), r.create(), r.deleteWorkspace(), r.favorite(), r.list(), + r.logs(), r.open(), r.ping(), r.rename(), diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 53d2a89290bca..df9d5a8dfec73 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -279,3 +279,14 @@ func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (Wo var timings WorkspaceBuildTimings return timings, json.NewDecoder(res.Body).Decode(&timings) } + +func (c *Client) WorkspaceBuildsByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaces/%s/builds", workspaceID), nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var builds []WorkspaceBuild + return builds, json.NewDecoder(res.Body).Decode(&builds) +} 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