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

Add coder envs rebuild and watch-build commands #146

Merged
merged 1 commit into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions coder-sdk/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,29 @@ func (c Client) StopEnvironment(ctx context.Context, envID string) error {
return c.requestBody(ctx, http.MethodPut, "/api/environments/"+envID+"/stop", nil, nil)
}

// UpdateEnvironmentReq defines the update operation, only setting
// nil-fields.
type UpdateEnvironmentReq struct {
ImageID *string `json:"image_id"`
ImageTag *string `json:"image_tag"`
CPUCores *float32 `json:"cpu_cores"`
MemoryGB *float32 `json:"memory_gb"`
DiskGB *int `json:"disk_gb"`
GPUs *int `json:"gpus"`
Services *[]string `json:"services"`
CodeServerReleaseURL *string `json:"code_server_release_url"`
}

// RebuildEnvironment requests that the given envID is rebuilt with no changes to its specification.
func (c Client) RebuildEnvironment(ctx context.Context, envID string) error {
return c.requestBody(ctx, http.MethodPatch, "/api/environments/"+envID, UpdateEnvironmentReq{}, nil)
}

// EditEnvironment modifies the environment specification and initiates a rebuild.
func (c Client) EditEnvironment(ctx context.Context, envID string, req UpdateEnvironmentReq) error {
return c.requestBody(ctx, http.MethodPatch, "/api/environments/"+envID, req, nil)
}

// DialWsep dials an environments command execution interface
// See https://github.com/cdr/wsep for details.
func (c Client) DialWsep(ctx context.Context, env *Environment) (*websocket.Conn, error) {
Expand All @@ -138,6 +161,49 @@ func (c Client) DialEnvironmentBuildLog(ctx context.Context, envID string) (*web
return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-update")
}

// BuildLog defines a build log record for a Coder environment.
type BuildLog struct {
ID string `db:"id" json:"id"`
EnvironmentID string `db:"environment_id" json:"environment_id"`
// BuildID allows the frontend to separate the logs from the old build with the logs from the new.
BuildID string `db:"build_id" json:"build_id"`
Time time.Time `db:"time" json:"time"`
Type BuildLogType `db:"type" json:"type"`
Msg string `db:"msg" json:"msg"`
}

// BuildLogFollowMsg wraps the base BuildLog and adds a field for collecting
// errors that may occur when follow or parsing.
type BuildLogFollowMsg struct {
BuildLog
Err error
}

// FollowEnvironmentBuildLog trails the build log of a Coder environment.
func (c Client) FollowEnvironmentBuildLog(ctx context.Context, envID string) (<-chan BuildLogFollowMsg, error) {
ch := make(chan BuildLogFollowMsg)
ws, err := c.DialEnvironmentBuildLog(ctx, envID)
Comment on lines +175 to +185
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@f0ssel @coadler Any thoughts on this pattern? Seems better to me than having a BuildLogWatcher type with methods for iterating when channels are designed for that.

if err != nil {
return nil, err
}
go func() {
defer ws.Close(websocket.StatusNormalClosure, "normal closure")
defer close(ch)
for {
var msg BuildLog
if err := wsjson.Read(ctx, ws, &msg); err != nil {
ch <- BuildLogFollowMsg{Err: err}
if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) {
return
}
continue
}
ch <- BuildLogFollowMsg{BuildLog: msg}
}
}()
return ch, nil
}

// DialEnvironmentStats opens a websocket connection for environment stats.
func (c Client) DialEnvironmentStats(ctx context.Context, envID string) (*websocket.Conn, error) {
return c.dialWebsocket(ctx, "/api/environments/"+envID+"/watch-stats")
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ go 1.14
require (
cdr.dev/slog v1.3.0
cdr.dev/wsep v0.0.0-20200728013649-82316a09813f
github.com/fatih/color v1.9.0 // indirect
github.com/briandowns/spinner v1.11.1
github.com/fatih/color v1.9.0
github.com/gorilla/websocket v1.4.1
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f
github.com/klauspost/compress v1.10.8 // indirect
github.com/manifoldco/promptui v0.7.0
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
github.com/rjeczalik/notify v0.9.2
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.6.1
go.coder.com/flog v0.0.0-20190906214207-47dd47ea0512
golang.org/x/crypto v0.0.0-20200422194213-44a606286825
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
nhooyr.io/websocket v1.8.6
Expand Down
11 changes: 7 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0=
github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
Expand Down Expand Up @@ -176,10 +178,11 @@ github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW
github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
Expand Down Expand Up @@ -334,8 +337,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13 h1:5jaG59Zhd+8ZXe8C+lgiAGqkOaZBruqrWclLkgAww34=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func envsCommand() *cobra.Command {
cmd.AddCommand(lsCmd)
cmd.AddCommand(stopEnvCommand(&user))

cmd.AddCommand(watchBuildLogCommand())
cmd.AddCommand(rebuildEnvCommand())
return cmd
}

Expand Down
146 changes: 146 additions & 0 deletions internal/cmd/rebuild.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package cmd

import (
"context"
"fmt"
"strings"
"time"

"cdr.dev/coder-cli/coder-sdk"
"github.com/briandowns/spinner"
"github.com/fatih/color"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"go.coder.com/flog"
"golang.org/x/xerrors"
)

func rebuildEnvCommand() *cobra.Command {
var follow bool
var force bool
cmd := &cobra.Command{
Use: "rebuild [environment_name]",
Short: "rebuild a Coder environment",
Args: cobra.ExactArgs(1),
Example: `coder envs rebuild front-end-env --follow
coder envs rebuild backend-env --force`,
Hidden: true, // TODO(@cmoog) un-hide
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient()
if err != nil {
return err
}
env, err := findEnv(ctx, client, args[0], coder.Me)
if err != nil {
return err
}

if !force && env.LatestStat.ContainerStatus == coder.EnvironmentOn {
_, err = (&promptui.Prompt{
Label: fmt.Sprintf("Rebuild environment \"%s\"? (will destroy any work outside of /home)", env.Name),
IsConfirm: true,
}).Run()
if err != nil {
return err
}
}

if err = client.RebuildEnvironment(ctx, env.ID); err != nil {
return err
}
if follow {
if err = trailBuildLogs(ctx, client, env.ID); err != nil {
return err
}
} else {
flog.Info("Use \"coder envs watch-build %s\" to follow the build logs", env.Name)
}
return nil
},
}

cmd.Flags().BoolVar(&follow, "follow", false, "follow buildlog after initiating rebuild")
cmd.Flags().BoolVar(&force, "force", false, "force rebuild without showing a confirmation prompt")
return cmd
}

// trailBuildLogs follows the build log for a given environment and prints the staged
// output with loaders and success/failure indicators for each stage
func trailBuildLogs(ctx context.Context, client *coder.Client, envID string) error {
const check = "✅"
const failure = "❌"
const loading = "⌛"

newSpinner := func() *spinner.Spinner { return spinner.New(spinner.CharSets[11], 100*time.Millisecond) }

logs, err := client.FollowEnvironmentBuildLog(ctx, envID)
if err != nil {
return err
}
var s *spinner.Spinner
for l := range logs {
if l.Err != nil {
return l.Err
}
switch l.BuildLog.Type {
case coder.BuildLogTypeStart:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do anything for this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FE uses this to reset the UI logs, but since we're just trailing I don't think it makes sense to do anything. Plus we exit after the Done message is sent anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense.

I think that explanation should be plugged in as a comment.

// the FE uses this to reset the UI
// the CLI doesn't need to do anything here given that we only append to the trail
case coder.BuildLogTypeStage:
if s != nil {
s.Stop()
fmt.Print("\n")
}
s = newSpinner()
msg := fmt.Sprintf("%s %s", l.BuildLog.Time.Format(time.RFC3339), l.BuildLog.Msg)
s.Suffix = fmt.Sprintf(" -- %s", msg)
s.FinalMSG = fmt.Sprintf("%s -- %s", check, msg)
s.Start()
case coder.BuildLogTypeSubstage:
// TODO(@cmoog) add verbose substage printing
case coder.BuildLogTypeError:
if s != nil {
s.FinalMSG = fmt.Sprintf("%s %s", failure, strings.TrimPrefix(s.Suffix, " "))
s.Stop()
}
fmt.Print(color.RedString("\t%s", l.BuildLog.Msg))
s = newSpinner()
case coder.BuildLogTypeDone:
if s != nil {
s.Stop()
}
return nil
default:
return xerrors.Errorf("unknown buildlog type: %s", l.BuildLog.Type)
}
}
return nil
}

func watchBuildLogCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "watch-build [environment_name]",
Example: "coder watch-build front-end-env",
Short: "trail the build log of a Coder environment",
Args: cobra.ExactArgs(1),
Hidden: true, // TODO(@cmoog) un-hide
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := newClient()
if err != nil {
return err
}
env, err := findEnv(ctx, client, args[0], coder.Me)
if err != nil {
return err
}

if err = trailBuildLogs(ctx, client, env.ID); err != nil {
return err
}
return nil
},
}
return cmd
}