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

Lines changed: 66 additions & 0 deletions
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

Lines changed: 4 additions & 3 deletions
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

Lines changed: 7 additions & 4 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 146 additions & 0 deletions
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)
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