mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: cli: consolidate schedule-related commands (#2402)
* feat: cli: consolidate schedule-related commands This commit makes the following changes: - renames autostart -> schedule starat - renames ttl -> schedule stop - renames bump -> schedule override - adds schedule show command - moves some cli-related stuff to util.go
This commit is contained in:
+177
@@ -0,0 +1,177 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/autobuild/schedule"
|
||||
"github.com/coder/coder/coderd/util/tz"
|
||||
)
|
||||
|
||||
var errInvalidScheduleFormat = xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
|
||||
var errInvalidTimeFormat = xerrors.New("Start time must be in the format hh:mm[am|pm] or HH:MM")
|
||||
var errUnsupportedTimezone = xerrors.New("The location you provided looks like a timezone. Check https://ipinfo.io for your location.")
|
||||
|
||||
// durationDisplay formats a duration for easier display:
|
||||
// * Durations of 24 hours or greater are displays as Xd
|
||||
// * Durations less than 1 minute are displayed as <1m
|
||||
// * Duration is truncated to the nearest minute
|
||||
// * Empty minutes and seconds are truncated
|
||||
// * The returned string is the absolute value. Use sign()
|
||||
// if you need to indicate if the duration is positive or
|
||||
// negative.
|
||||
func durationDisplay(d time.Duration) string {
|
||||
duration := d
|
||||
sign := ""
|
||||
if duration == 0 {
|
||||
return "0s"
|
||||
}
|
||||
if duration < 0 {
|
||||
duration *= -1
|
||||
}
|
||||
// duration > 0 now
|
||||
if duration < time.Minute {
|
||||
return sign + "<1m"
|
||||
}
|
||||
if duration > 24*time.Hour {
|
||||
duration = duration.Truncate(time.Hour)
|
||||
}
|
||||
if duration > time.Minute {
|
||||
duration = duration.Truncate(time.Minute)
|
||||
}
|
||||
days := 0
|
||||
for duration.Hours() >= 24 {
|
||||
days++
|
||||
duration -= 24 * time.Hour
|
||||
}
|
||||
durationDisplay := duration.String()
|
||||
if days > 0 {
|
||||
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
|
||||
}
|
||||
for _, suffix := range []string{"m0s", "h0m", "d0s", "d0h"} {
|
||||
if strings.HasSuffix(durationDisplay, suffix) {
|
||||
durationDisplay = durationDisplay[:len(durationDisplay)-2]
|
||||
}
|
||||
}
|
||||
return sign + durationDisplay
|
||||
}
|
||||
|
||||
// relative relativizes a duration with the prefix "ago" or "in"
|
||||
func relative(d time.Duration) string {
|
||||
if d > 0 {
|
||||
return "in " + durationDisplay(d)
|
||||
}
|
||||
if d < 0 {
|
||||
return durationDisplay(d) + " ago"
|
||||
}
|
||||
return "now"
|
||||
}
|
||||
|
||||
// parseCLISchedule parses a schedule in the format HH:MM{AM|PM} [DOW] [LOCATION]
|
||||
func parseCLISchedule(parts ...string) (*schedule.Schedule, error) {
|
||||
// If the user was careful and quoted the schedule, un-quote it.
|
||||
// In the case that only time was specified, this will be a no-op.
|
||||
if len(parts) == 1 {
|
||||
parts = strings.Fields(parts[0])
|
||||
}
|
||||
var loc *time.Location
|
||||
dayOfWeek := "*"
|
||||
t, err := parseTime(parts[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hour, minute := t.Hour(), t.Minute()
|
||||
|
||||
// Any additional parts get ignored.
|
||||
switch len(parts) {
|
||||
case 3:
|
||||
dayOfWeek = parts[1]
|
||||
loc, err = time.LoadLocation(parts[2])
|
||||
if err != nil {
|
||||
_, err = time.Parse("MST", parts[2])
|
||||
if err == nil {
|
||||
return nil, errUnsupportedTimezone
|
||||
}
|
||||
return nil, xerrors.Errorf("Invalid timezone %q specified: a valid IANA timezone is required", parts[2])
|
||||
}
|
||||
case 2:
|
||||
// Did they provide day-of-week or location?
|
||||
if maybeLoc, err := time.LoadLocation(parts[1]); err != nil {
|
||||
// Assume day-of-week.
|
||||
dayOfWeek = parts[1]
|
||||
} else {
|
||||
loc = maybeLoc
|
||||
}
|
||||
case 1: // already handled
|
||||
default:
|
||||
return nil, errInvalidScheduleFormat
|
||||
}
|
||||
|
||||
// If location was not specified, attempt to automatically determine it as a last resort.
|
||||
if loc == nil {
|
||||
loc, err = tz.TimezoneIANA()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("Could not automatically determine your timezone")
|
||||
}
|
||||
}
|
||||
|
||||
sched, err := schedule.Weekly(fmt.Sprintf(
|
||||
"CRON_TZ=%s %d %d * * %s",
|
||||
loc.String(),
|
||||
minute,
|
||||
hour,
|
||||
dayOfWeek,
|
||||
))
|
||||
if err != nil {
|
||||
// This will either be an invalid dayOfWeek or an invalid timezone.
|
||||
return nil, xerrors.Errorf("Invalid schedule: %w", err)
|
||||
}
|
||||
|
||||
return sched, nil
|
||||
}
|
||||
|
||||
// parseDuration parses a duration from a string.
|
||||
// If units are omitted, minutes are assumed.
|
||||
func parseDuration(raw string) (time.Duration, error) {
|
||||
// If the user input a raw number, assume minutes
|
||||
if isDigit(raw) {
|
||||
raw = raw + "m"
|
||||
}
|
||||
d, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func isDigit(s string) bool {
|
||||
return strings.IndexFunc(s, func(c rune) bool {
|
||||
return c < '0' || c > '9'
|
||||
}) == -1
|
||||
}
|
||||
|
||||
// parseTime attempts to parse a time (no date) from the given string using a number of layouts.
|
||||
func parseTime(s string) (time.Time, error) {
|
||||
// Try a number of possible layouts.
|
||||
for _, layout := range []string{
|
||||
time.Kitchen, // 03:04PM
|
||||
"03:04pm",
|
||||
"3:04PM",
|
||||
"3:04pm",
|
||||
"15:04",
|
||||
"1504",
|
||||
"03PM",
|
||||
"03pm",
|
||||
"3PM",
|
||||
"3pm",
|
||||
} {
|
||||
t, err := time.Parse(layout, s)
|
||||
if err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, errInvalidTimeFormat
|
||||
}
|
||||
Reference in New Issue
Block a user