-
Notifications
You must be signed in to change notification settings - Fork 894
feat(cli): allow showing schedules for multiple workspaces #10596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
dd3fa2a
9a98fbf
e59d9ce
5d74fea
4ab40ca
6dd691e
8e118a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,9 +3,9 @@ package cli | |
import ( | ||
"fmt" | ||
"io" | ||
"strings" | ||
"time" | ||
|
||
"github.com/jedib0t/go-pretty/v6/table" | ||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/v2/cli/clibase" | ||
|
@@ -17,7 +17,7 @@ import ( | |
) | ||
|
||
const ( | ||
scheduleShowDescriptionLong = `Shows the following information for the given workspace: | ||
scheduleShowDescriptionLong = `Shows the following information for the given workspace(s): | ||
* The automatic start schedule | ||
* The next scheduled start time | ||
* The duration after which it will stop | ||
|
@@ -72,25 +72,67 @@ func (r *RootCmd) schedules() *clibase.Cmd { | |
return scheduleCmd | ||
} | ||
|
||
// scheduleShow() is just a wrapper for list() with some different defaults. | ||
func (r *RootCmd) scheduleShow() *clibase.Cmd { | ||
var ( | ||
filter cliui.WorkspaceFilter | ||
formatter = cliui.NewOutputFormatter( | ||
cliui.TableFormat( | ||
[]scheduleListRow{}, | ||
[]string{ | ||
"workspace", | ||
"starts at", | ||
"starts next", | ||
"stops after", | ||
"stops next", | ||
}, | ||
), | ||
cliui.JSONFormat(), | ||
) | ||
) | ||
client := new(codersdk.Client) | ||
showCmd := &clibase.Cmd{ | ||
Use: "show <workspace-name>", | ||
Short: "Show workspace schedule", | ||
Use: "show [<workspace>] | [--search <query>] [--all]", | ||
Short: "Show workspace schedules", | ||
Long: scheduleShowDescriptionLong, | ||
Middleware: clibase.Chain( | ||
clibase.RequireNArgs(1), | ||
clibase.RequireRangeArgs(0, 1), | ||
r.InitClient(client), | ||
), | ||
Handler: func(inv *clibase.Invocation) error { | ||
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) | ||
// To preserve existing behavior, if an argument is passed we will | ||
// only show the schedule for that workspace. | ||
// This will clobber the search query if one is passed. | ||
f := filter.Filter() | ||
if len(inv.Args) == 1 { | ||
// If the argument contains a slash, we assume it's a full owner/name reference | ||
if strings.Contains(inv.Args[0], "/") { | ||
_, workspaceName, err := splitNamedWorkspace(inv.Args[0]) | ||
if err != nil { | ||
return err | ||
} | ||
f.FilterQuery = fmt.Sprintf("name:%s", workspaceName) | ||
} else { | ||
// Otherwise, we assume it's a workspace name owned by the current user | ||
f.FilterQuery = fmt.Sprintf("owner:me name:%s", inv.Args[0]) | ||
} | ||
} | ||
res, err := queryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return displaySchedule(workspace, inv.Stdout) | ||
out, err := formatter.Format(inv.Context(), res) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
_, err = fmt.Fprintln(inv.Stdout, out) | ||
return err | ||
}, | ||
} | ||
filter.AttachOptions(&showCmd.Options) | ||
formatter.AttachOptions(&showCmd.Options) | ||
return showCmd | ||
} | ||
|
||
|
@@ -242,50 +284,52 @@ func (r *RootCmd) scheduleOverride() *clibase.Cmd { | |
return overrideCmd | ||
} | ||
|
||
func displaySchedule(workspace codersdk.Workspace, out io.Writer) error { | ||
loc, err := tz.TimezoneIANA() | ||
func displaySchedule(ws codersdk.Workspace, out io.Writer) error { | ||
rows := []workspaceListRow{workspaceListRowFromWorkspace(time.Now(), ws)} | ||
rendered, err := cliui.DisplayTable(rows, "workspace", []string{ | ||
"workspace", "starts at", "starts next", "stops after", "stops next", | ||
}) | ||
if err != nil { | ||
loc = time.UTC // best effort | ||
return err | ||
} | ||
_, _ = fmt.Fprintln(out, rendered) | ||
return nil | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
var ( | ||
schedStart = "manual" | ||
schedStop = "manual" | ||
schedNextStart = "-" | ||
schedNextStop = "-" | ||
) | ||
// scheduleListRow is a row in the schedule list. | ||
// this is required for proper JSON output. | ||
type scheduleListRow struct { | ||
WorkspaceName string `json:"workspace" table:"workspace,default_sort"` | ||
StartsAt string `json:"starts_at" table:"starts at"` | ||
StartsNext string `json:"starts_next" table:"starts next"` | ||
StopsAfter string `json:"stops_after" table:"stops after"` | ||
StopsNext string `json:"stops_next" table:"stops next"` | ||
} | ||
|
||
func scheduleListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) scheduleListRow { | ||
autostartDisplay := "" | ||
nextStartDisplay := "" | ||
if !ptr.NilOrEmpty(workspace.AutostartSchedule) { | ||
sched, err := cron.Weekly(ptr.NilToEmpty(workspace.AutostartSchedule)) | ||
if err != nil { | ||
// This should never happen. | ||
_, _ = fmt.Fprintf(out, "Invalid autostart schedule %q for workspace %s: %s\n", *workspace.AutostartSchedule, workspace.Name, err.Error()) | ||
return nil | ||
if sched, err := cron.Weekly(*workspace.AutostartSchedule); err == nil { | ||
autostartDisplay = sched.Humanize() | ||
nextStartDisplay = timeDisplay(sched.Next(now)) | ||
} | ||
schedNext := sched.Next(time.Now()).In(sched.Location()) | ||
schedStart = fmt.Sprintf("%s %s (%s)", sched.Time(), sched.DaysOfWeek(), sched.Location()) | ||
schedNextStart = schedNext.Format(timeFormat + " on " + dateFormat) | ||
} | ||
|
||
autostopDisplay := "" | ||
nextStopDisplay := "" | ||
if !ptr.NilOrZero(workspace.TTLMillis) { | ||
d := time.Duration(*workspace.TTLMillis) * time.Millisecond | ||
schedStop = durationDisplay(d) + " after start" | ||
} | ||
|
||
if !workspace.LatestBuild.Deadline.IsZero() { | ||
if workspace.LatestBuild.Transition != "start" { | ||
schedNextStop = "-" | ||
} else { | ||
schedNextStop = workspace.LatestBuild.Deadline.Time.In(loc).Format(timeFormat + " on " + dateFormat) | ||
schedNextStop = fmt.Sprintf("%s (in %s)", schedNextStop, durationDisplay(time.Until(workspace.LatestBuild.Deadline.Time))) | ||
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond | ||
autostopDisplay = durationDisplay(dur) | ||
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionStart { | ||
nextStopDisplay = timeDisplay(workspace.LatestBuild.Deadline.Time) | ||
} | ||
} | ||
|
||
tw := cliui.Table() | ||
tw.AppendRow(table.Row{"Starts at", schedStart}) | ||
tw.AppendRow(table.Row{"Starts next", schedNextStart}) | ||
tw.AppendRow(table.Row{"Stops at", schedStop}) | ||
tw.AppendRow(table.Row{"Stops next", schedNextStop}) | ||
|
||
_, _ = fmt.Fprintln(out, tw.Render()) | ||
return nil | ||
return scheduleListRow{ | ||
WorkspaceName: workspace.OwnerName + "/" + workspace.Name, | ||
StartsAt: autostartDisplay, | ||
StartsNext: nextStartDisplay, | ||
StopsAfter: autostopDisplay, | ||
StopsNext: nextStopDisplay, | ||
} | ||
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. Should we include the datapoints "calculated" here in the API response instead (obviously not the formatting, but timestamps, etc). I'm thinking it'd be better if our CLI was "dumber", and it might help other API consumers too. 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. What specific datapoints do you want included? 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. I suppose I'm thinking of something like this:
Maybe more fields if it makes sense. Basically I'm thinking if we can avoid the need to call Just an idea. 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. It's not a bad idea! Filed #10621 |
||
} |
Uh oh!
There was an error while loading. Please reload this page.