Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 70615c4

Browse files
authored
Add coder envs rebuild and watch-build commands (#146)
1 parent d11afcd commit 70615c4

File tree

5 files changed

+225
-7
lines changed

5 files changed

+225
-7
lines changed

coder-sdk/env.go

+66
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,29 @@ func (c Client) StopEnvironment(ctx context.Context, envID string) error {
122122
return c.requestBody(ctx, http.MethodPut, "/api/environments/"+envID+"/stop", nil, nil)
123123
}
124124

125+
// UpdateEnvironmentReq defines the update operation, only setting
126+
// nil-fields.
127+
type UpdateEnvironmentReq struct {
128+
ImageID *string `json:"image_id"`
129+
ImageTag *string `json:"image_tag"`
130+
CPUCores *float32 `json:"cpu_cores"`
131+
MemoryGB *float32 `json:"memory_gb"`
132+
DiskGB *int `json:"disk_gb"`
133+
GPUs *int `json:"gpus"`
134+
Services *[]string `json:"services"`
135+
CodeServerReleaseURL *string `json:"code_server_release_url"`
136+
}
137+
138+
// RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification.
139+
func (c Client) RebuildEnvironment(ctx context.Context, envID string) error {
140+
return c.requestBody(ctx, http.MethodPatch, "/api/environments/"+envID, UpdateEnvironmentReq{}, nil)
141+
}
142+
143+
// EditEnvironment modifies the environment specification and initiates a rebuild.
144+
func (c Client) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error {
145+
return c.requestBody(ctx, http.MethodPatch, "/api/environments/"+envID, req, nil)
146+
}
147+
125148
// DialWsep dials an environments command execution interface
126149
// See https://github.com/cdr/wsep for details.
127150
func (c Client) DialWsep(ctx context.Context, env *Environment) (*websocket.Conn, error) {
@@ -138,6 +161,49 @@ func (c Client) DialEnvironmentBuildLog(ctx context.Context, envID string) (*web
138161
return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-update")
139162
}
140163

164+
// BuildLog defines a build log record for a Coder environment.
165+
type BuildLog struct {
166+
ID string `db:"id" json:"id"`
167+
EnvironmentID string `db:"environment_id" json:"environment_id"`
168+
// BuildID allows the frontend to separate the logs from the old build with the logs from the new.
169+
BuildID string `db:"build_id" json:"build_id"`
170+
Time time.Time `db:"time" json:"time"`
171+
Type BuildLogType `db:"type" json:"type"`
172+
Msg string `db:"msg" json:"msg"`
173+
}
174+
175+
// BuildLogFollowMsg wraps the base BuildLog and adds a field for collecting
176+
// errors that may occur when follow or parsing.
177+
type BuildLogFollowMsg struct {
178+
BuildLog
179+
Err error
180+
}
181+
182+
// FollowEnvironmentBuildLog trails the build log of a Coder environment.
183+
func (c Client) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) {
184+
ch := make(chan BuildLogFollowMsg)
185+
ws, err := c.DialEnvironmentBuildLog(ctx, envID)
186+
if err != nil {
187+
return nil, err
188+
}
189+
go func() {
190+
defer ws.Close(websocket.StatusNormalClosure, "normal closure")
191+
defer close(ch)
192+
for {
193+
var msg BuildLog
194+
if err := wsjson.Read(ctx, ws, &msg); err != nil {
195+
ch <- BuildLogFollowMsg{Err: err}
196+
if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) {
197+
return
198+
}
199+
continue
200+
}
201+
ch <- BuildLogFollowMsg{BuildLog: msg}
202+
}
203+
}()
204+
return ch, nil
205+
}
206+
141207
// DialEnvironmentStats opens a websocket connection for environment stats.
142208
func (c Client) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) {
143209
return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-stats")

go.mod

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ go 1.14
55
require (
66
cdr.dev/slog v1.3.0
77
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f
8-
github.com/fatih/color v1.9.0 // indirect
8+
github.com/briandowns/spinner v1.11.1
9+
github.com/fatih/color v1.9.0
910
github.com/gorilla/websocket v1.4.1
1011
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
1112
github.com/klauspost/compress v1.10.8 // indirect
1213
github.com/manifoldco/promptui v0.7.0
13-
github.com/mattn/go-colorable v0.1.6 // indirect
14+
github.com/mattn/go-colorable v0.1.8 // indirect
1415
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
1516
github.com/rjeczalik/notify v0.9.2
1617
github.com/spf13/cobra v1.0.0
1718
github.com/stretchr/testify v1.6.1
1819
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512
1920
golang.org/x/crypto v0.0.0-20200422194213-44a606286825
2021
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
21-
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1
22+
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13
2223
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
2324
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
2425
nhooyr.io/websocket v1.8.6

go.sum

+7-4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
3838
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
3939
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
4040
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
41+
github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0=
42+
github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
4143
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
4244
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
4345
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
@@ -176,10 +178,11 @@ github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW
176178
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
177179
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
178180
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
181+
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
179182
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
180183
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
181-
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
182-
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
184+
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
185+
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
183186
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
184187
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
185188
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -334,8 +337,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
334337
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
335338
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
336339
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
337-
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
338-
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
340+
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
341+
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
339342
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
340343
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
341344
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

internal/cmd/envs.go

+2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ func envsCommand() *cobra.Command {
6464
cmd.AddCommand(lsCmd)
6565
cmd.AddCommand(stopEnvCommand(&user))
6666

67+
cmd.AddCommand(watchBuildLogCommand())
68+
cmd.AddCommand(rebuildEnvCommand())
6769
return cmd
6870
}
6971

internal/cmd/rebuild.go

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"cdr.dev/coder-cli/coder-sdk"
10+
"github.com/briandowns/spinner"
11+
"github.com/fatih/color"
12+
"github.com/manifoldco/promptui"
13+
"github.com/spf13/cobra"
14+
"go.coder.com/flog"
15+
"golang.org/x/xerrors"
16+
)
17+
18+
func rebuildEnvCommand() *cobra.Command {
19+
var follow bool
20+
var force bool
21+
cmd := &cobra.Command{
22+
Use: "rebuild [environment_name]",
23+
Short: "rebuild a Coder environment",
24+
Args: cobra.ExactArgs(1),
25+
Example: `coder envs rebuild front-end-env --follow
26+
coder envs rebuild backend-env --force`,
27+
Hidden: true, // TODO(@cmoog) un-hide
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
ctx := cmd.Context()
30+
client, err := newClient()
31+
if err != nil {
32+
return err
33+
}
34+
env, err := findEnv(ctx, client, args[0], coder.Me)
35+
if err != nil {
36+
return err
37+
}
38+
39+
if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn {
40+
_, err = (&promptui.Prompt{
41+
Label: fmt.Sprintf("Rebuild environment \"%s\"? (will destroy any work outside of /home)", env.Name),
42+
IsConfirm: true,
43+
}).Run()
44+
if err != nil {
45+
return err
46+
}
47+
}
48+
49+
if err = client.RebuildEnvironment(ctx, env.ID); err != nil {
50+
return err
51+
}
52+
if follow {
53+
if err = trailBuildLogs(ctx, client, env.ID); err != nil {
54+
return err
55+
}
56+
} else {
57+
flog.Info("Use \"coder envs watch-build %s\" to follow the build logs", env.Name)
58+
}
59+
return nil
60+
},
61+
}
62+
63+
cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild")
64+
cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt")
65+
return cmd
66+
}
67+
68+
// trailBuildLogs follows the build log for a given environment and prints the staged
69+
// output with loaders and success/failure indicators for each stage
70+
func trailBuildLogs(ctx context.Context, client *coder.Client, envID string) error {
71+
const check = "✅"
72+
const failure = "❌"
73+
const loading = "⌛"
74+
75+
newSpinner := func() *spinner.Spinner { return spinner.New(spinner.CharSets[11], 100*time.Millisecond) }
76+
77+
logs, err := client.FollowEnvironmentBuildLog(ctx, envID)
78+
if err != nil {
79+
return err
80+
}
81+
var s *spinner.Spinner
82+
for l := range logs {
83+
if l.Err != nil {
84+
return l.Err
85+
}
86+
switch l.BuildLog.Type {
87+
case coder.BuildLogTypeStart:
88+
// the FE uses this to reset the UI
89+
// the CLI doesn't need to do anything here given that we only append to the trail
90+
case coder.BuildLogTypeStage:
91+
if s != nil {
92+
s.Stop()
93+
fmt.Print("\n")
94+
}
95+
s = newSpinner()
96+
msg := fmt.Sprintf("%s %s", l.BuildLog.Time.Format(time.RFC3339), l.BuildLog.Msg)
97+
s.Suffix = fmt.Sprintf(" -- %s", msg)
98+
s.FinalMSG = fmt.Sprintf("%s -- %s", check, msg)
99+
s.Start()
100+
case coder.BuildLogTypeSubstage:
101+
// TODO(@cmoog) add verbose substage printing
102+
case coder.BuildLogTypeError:
103+
if s != nil {
104+
s.FinalMSG = fmt.Sprintf("%s %s", failure, strings.TrimPrefix(s.Suffix, " "))
105+
s.Stop()
106+
}
107+
fmt.Print(color.RedString("\t%s", l.BuildLog.Msg))
108+
s = newSpinner()
109+
case coder.BuildLogTypeDone:
110+
if s != nil {
111+
s.Stop()
112+
}
113+
return nil
114+
default:
115+
return xerrors.Errorf("unknown buildlog type: %s", l.BuildLog.Type)
116+
}
117+
}
118+
return nil
119+
}
120+
121+
func watchBuildLogCommand() *cobra.Command {
122+
cmd := &cobra.Command{
123+
Use: "watch-build [environment_name]",
124+
Example: "coder watch-build front-end-env",
125+
Short: "trail the build log of a Coder environment",
126+
Args: cobra.ExactArgs(1),
127+
Hidden: true, // TODO(@cmoog) un-hide
128+
RunE: func(cmd *cobra.Command, args []string) error {
129+
ctx := cmd.Context()
130+
client, err := newClient()
131+
if err != nil {
132+
return err
133+
}
134+
env, err := findEnv(ctx, client, args[0], coder.Me)
135+
if err != nil {
136+
return err
137+
}
138+
139+
if err = trailBuildLogs(ctx, client, env.ID); err != nil {
140+
return err
141+
}
142+
return nil
143+
},
144+
}
145+
return cmd
146+
}

0 commit comments

Comments
 (0)