mirror of https://github.com/coder/coder.git
222 lines
5.4 KiB
Go
222 lines
5.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/schedule/cron"
|
|
"github.com/coder/coder/v2/coderd/util/tz"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
var (
|
|
errInvalidScheduleFormat = xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
|
|
errInvalidTimeFormat = xerrors.New("Start time must be in the format hh:mm[am|pm] or HH:MM")
|
|
errUnsupportedTimezone = xerrors.New("The location you provided looks like a timezone. Check https://ipinfo.io for your location.")
|
|
)
|
|
|
|
// userSetOption returns true if the option was set by the user.
|
|
// This is helpful if the zero value of a flag is meaningful, and you need
|
|
// to distinguish between the user setting the flag to the zero value and
|
|
// the user not setting the flag at all.
|
|
func userSetOption(inv *serpent.Invocation, flagName string) bool {
|
|
for _, opt := range inv.Command.Options {
|
|
if opt.Name == flagName {
|
|
return !(opt.ValueSource == serpent.ValueSourceNone || opt.ValueSource == serpent.ValueSourceDefault)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// timeDisplay formats a time in the local timezone
|
|
// in RFC3339 format.
|
|
func timeDisplay(t time.Time) string {
|
|
localTz, err := tz.TimezoneIANA()
|
|
if err != nil {
|
|
localTz = time.UTC
|
|
}
|
|
|
|
return t.In(localTz).Format(time.RFC3339)
|
|
}
|
|
|
|
// 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) (*cron.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 {
|
|
loc = time.UTC
|
|
}
|
|
}
|
|
|
|
sched, err := cron.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
|
|
}
|
|
|
|
func formatActiveDevelopers(n int) string {
|
|
developerText := "developer"
|
|
if n != 1 {
|
|
developerText = "developers"
|
|
}
|
|
|
|
var nStr string
|
|
if n < 0 {
|
|
nStr = "-"
|
|
} else {
|
|
nStr = strconv.Itoa(n)
|
|
}
|
|
|
|
return fmt.Sprintf("%s active %s", nStr, developerText)
|
|
}
|