Skip to content

Commit

Permalink
Add command to watch builds until completion.
Browse files Browse the repository at this point in the history
  • Loading branch information
lnsp committed Dec 8, 2024
1 parent c2a2ff0 commit 2327b76
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 10 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,12 @@ valar builds abort [prefix]
#### Show logs from build
```bash
valar builds logs [--follow] [optional buildid]
valar builds logs [--follow] [--raw] [optional buildid]
```
#### Watch the build and status until its completed
```bash
valar builds watch [optional buildid]
```
#### Show build status
Expand Down
80 changes: 76 additions & 4 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"bufio"
"bytes"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -194,25 +195,96 @@ func (client *Client) AbortBuild(project, service, id string) error {
return nil
}

type LogEntry struct {
Timestamp time.Time `json:"timestamp"`
Source LogEntrySource `json:"source"`
Stage LogEntryStage `json:"stage"`
Content string `json:"content"`
}

type LogEntrySource string

const (
LogEntrySourceUnspecified = ""
LogEntrySourceProcess = "PROCESS"
LogEntrySourceWrapper = "WRAPPER"
)

type LogEntryStage string

const (
LogEntryStageUnspecified = ""
LogEntryStageSetup = "SETUP"
LogEntryStageTurndown = "TURNDOWN"
)

type logEntryDecoder struct {
consumer func(LogEntry)
writer *io.PipeWriter
done chan struct{}
}

func newLogEntryDecoder(consumer func(LogEntry)) *logEntryDecoder {
done := make(chan struct{}, 1)
reader, writer := io.Pipe()
go func() {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
logentry := LogEntry{}
logline := scanner.Text()
if err := json.Unmarshal([]byte(logline), &logentry); err != nil {
consumer(LogEntry{Content: logline})
continue
}
consumer(logentry)
}
close(done)
}()

return &logEntryDecoder{consumer, writer, done}
}

func (decoder *logEntryDecoder) Write(b []byte) (int, error) {
return decoder.writer.Write(b)
}

func (decoder *logEntryDecoder) Close() error {
return decoder.writer.Close()
}

func (decoder *logEntryDecoder) Wait() {
<-decoder.done
}

// ShowBuildLogs retrieves a specific build task logs.
func (client *Client) ShowBuildLogs(project, service, id string, w io.Writer) error {
func (client *Client) ShowBuildLogs(project, service, id string, consumer func(LogEntry)) error {
var (
path = fmt.Sprintf("/projects/%s/services/%s/builds/%s/logs", project, service, id)
)
if err := client.streamRequest(http.MethodGet, path, w); err != nil {
decoder := newLogEntryDecoder(consumer)
if err := client.streamRequest(http.MethodGet, path, decoder); err != nil {
return err
}
if err := decoder.Close(); err != nil {
return err
}
decoder.Wait()
return nil
}

// StreamBuildLogs streams the active build logs to stdout.
func (client *Client) StreamBuildLogs(project, service, id string, w io.Writer) error {
func (client *Client) StreamBuildLogs(project, service, id string, consumer func(LogEntry)) error {
var (
path = fmt.Sprintf("/projects/%s/services/%s/builds/%s/logs?follow=true", project, service, id)
)
if err := client.streamRequest(http.MethodGet, path, w); err != nil {
decoder := newLogEntryDecoder(consumer)
if err := client.streamRequest(http.MethodGet, path, decoder); err != nil {
return err
}
if err := decoder.Close(); err != nil {
return err
}
decoder.Wait()
return nil
}

Expand Down
110 changes: 107 additions & 3 deletions cmd/builds.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import (
"sort"
"strings"
"text/tabwriter"
"time"

"github.com/dustin/go-humanize"
"github.com/fatih/color"
"github.com/juju/ansiterm"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra"
"github.com/valar/cli/api"
"github.com/valar/cli/config"
"golang.org/x/crypto/ssh/terminal"
)

var buildService string
Expand Down Expand Up @@ -64,6 +67,29 @@ var buildAbortCmd = &cobra.Command{
}

var logsFollow = false
var logsRaw = false

func formatLogEntry(logEntry *api.LogEntry) string {
line := &strings.Builder{}

fmt.Fprintf(line, "│ %s │ ", color.HiBlackString(logEntry.Timestamp.Format(time.RFC3339)))

switch logEntry.Source {
case api.LogEntrySourceUnspecified:
case api.LogEntrySourceWrapper:
switch logEntry.Stage {
case api.LogEntryStageUnspecified:
line.WriteString(color.WhiteString("→ %s", logEntry.Content))
case api.LogEntryStageSetup:
line.WriteString(color.GreenString("setup ↗ %s", logEntry.Content))
case api.LogEntryStageTurndown:
line.WriteString(color.YellowString("turndown ↘ %s", logEntry.Content))
}
case api.LogEntrySourceProcess:
line.WriteString(color.WhiteString("→ %s", logEntry.Content))
}
return line.String()
}

var buildLogsCmd = &cobra.Command{
Use: "logs [buildid]",
Expand Down Expand Up @@ -93,10 +119,87 @@ var buildLogsCmd = &cobra.Command{
// Sort builds by date
sort.Slice(builds, func(i, j int) bool { return builds[i].CreatedAt.After(builds[j].CreatedAt) })
latestBuildID := builds[0].ID
consumer := func(le api.LogEntry) {
if logsRaw {
fmt.Println(le.Content)
} else {
fmt.Println(formatLogEntry(&le))
}
}
if logsFollow {
return client.StreamBuildLogs(cfg.Project(), cfg.Service(), latestBuildID, os.Stdout)
return client.StreamBuildLogs(cfg.Project(), cfg.Service(), latestBuildID, consumer)
}
return client.ShowBuildLogs(cfg.Project(), cfg.Service(), latestBuildID, consumer)
}),
}

var buildWatchCmd = &cobra.Command{
Use: "watch [prefix]",
Short: "Watch a live build until its completion.",
Args: cobra.MaximumNArgs(1),
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &buildService, globalConfiguration)
if err != nil {
return err
}
client, err := globalConfiguration.APIClient()
if err != nil {
return err
}
// Get latest matching build
prefix := ""
if len(args) > 0 {
prefix = args[0]
}
return client.ShowBuildLogs(cfg.Project(), cfg.Service(), latestBuildID, os.Stdout)
builds, err := client.ListBuilds(cfg.Project(), cfg.Service(), prefix)
if err != nil {
return err
}
if len(builds) == 0 {
return fmt.Errorf("no builds available")
}
// Sort builds by date
sort.Slice(builds, func(i, j int) bool { return builds[i].CreatedAt.After(builds[j].CreatedAt) })
latestBuildID := builds[0].ID

_, rows, _ := terminal.GetSize(0)
bar := progressbar.NewOptions(-1, progressbar.OptionEnableColorCodes(true), progressbar.OptionSpinnerType(3), progressbar.OptionSetElapsedTime(false), progressbar.OptionSetMaxDetailRow(rows-2))
build, err := client.InspectBuild(cfg.Project(), cfg.Service(), latestBuildID)
if err != nil {
return err
}

switch build.Status {
case "scheduled":
bar.Describe("Scheduling build onto worker ...")
}

client.StreamBuildLogs(cfg.Project(), cfg.Service(), latestBuildID, func(le api.LogEntry) {
switch le.Stage {
case api.LogEntryStageUnspecified:
bar.Describe("Processing ...")
case api.LogEntryStageSetup:
bar.Describe("Setting up build environment ...")
case api.LogEntryStageTurndown:
bar.Describe("Turning down build environment ...")
}
bar.AddDetail(formatLogEntry(&le))
})

build, err = client.InspectBuild(cfg.Project(), cfg.Service(), latestBuildID)
if err != nil {
return err
}
switch build.Status {
case "done":
bar.Describe("Build has succeeded.")
case "failed":
bar.Describe("Build has failed.")
}
bar.Finish()
fmt.Println()

return nil
}),
}

Expand Down Expand Up @@ -227,6 +330,7 @@ func colorize(status string) string {
func initBuildsCmd() {
buildCmd.PersistentFlags().StringVarP(&buildService, "service", "s", "", "The service to inspect for builds")
buildLogsCmd.PersistentFlags().BoolVarP(&logsFollow, "follow", "f", false, "Follow the logs")
buildCmd.AddCommand(buildListCmd, buildInspectCmd, buildLogsCmd, buildAbortCmd, buildStatusCmd)
buildLogsCmd.PersistentFlags().BoolVarP(&logsRaw, "raw", "r", false, "Dump the unformatted log content")
buildCmd.AddCommand(buildListCmd, buildInspectCmd, buildLogsCmd, buildAbortCmd, buildStatusCmd, buildWatchCmd)
rootCmd.AddCommand(buildCmd)
}
11 changes: 9 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/valar/cli

go 1.21
go 1.22

toolchain go1.23.3

require (
github.com/dustin/go-humanize v1.0.1
Expand Down Expand Up @@ -29,10 +31,15 @@ require (
github.com/lunixbochs/vtclean v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/schollz/progressbar/v3 v3.17.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.27.0 // indirect
)
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
Expand All @@ -88,9 +90,13 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
Expand All @@ -109,6 +115,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
Expand All @@ -127,6 +135,14 @@ golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down

0 comments on commit 2327b76

Please sign in to comment.