- Notifications
You must be signed in to change notification settings - Fork18
Add coder envs rebuild and watch-build commands#146
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
@@ -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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. | ||
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") | ||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Do we need to do anything for this case? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
fuskovic marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
if err != nil { | ||
return err | ||
} | ||
if err = trailBuildLogs(ctx, client, env.ID); err != nil { | ||
return err | ||
} | ||
return nil | ||
}, | ||
} | ||
return cmd | ||
} |