Skip to content

feat: cli: consolidate schedule-related commands #2402

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

Merged
merged 12 commits into from
Jun 16, 2022
Merged
Prev Previous commit
Next Next commit
show time until deadline
  • Loading branch information
johnstcn committed Jun 16, 2022
commit dad6695c4f4251b94baf8e23cf00dc09971d171b
10 changes: 6 additions & 4 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func list() *cobra.Command {
}})
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))

now := time.Now()
for _, workspace := range workspaces {
status := ""
inProgress := false
Expand Down Expand Up @@ -85,7 +86,7 @@ func list() *cobra.Command {
status = "Failed"
}

duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
lastBuilt := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
Expand All @@ -97,8 +98,9 @@ func list() *cobra.Command {
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if has, ext := hasExtension(workspace); has {
autostopDisplay += fmt.Sprintf(" (%s%s)", sign(ext), durationDisplay(ext))
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.After(now) && status == "Running" {
remaining := time.Until(workspace.LatestBuild.Deadline)
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
}
}

Expand All @@ -107,7 +109,7 @@ func list() *cobra.Command {
user.Username + "/" + workspace.Name,
workspace.TemplateName,
status,
durationDisplay(duration),
durationDisplay(lastBuilt),
workspace.Outdated,
autostartDisplay,
autostopDisplay,
Expand Down
4 changes: 1 addition & 3 deletions cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,7 @@ func displaySchedule(workspace codersdk.Workspace, out io.Writer) error {
schedNextStop = "-"
} else {
schedNextStop = workspace.LatestBuild.Deadline.In(loc).Format(timeFormat + " on " + dateFormat)
if found, dur := hasExtension(workspace); found {
schedNextStop += fmt.Sprintf(" (%s%s from schedule)", sign(dur), durationDisplay(dur))
}
schedNextStop = fmt.Sprintf("%s (in %s)", schedNextStop, durationDisplay(time.Until(workspace.LatestBuild.Deadline)))
}
}

Expand Down
2 changes: 1 addition & 1 deletion cli/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestScheduleShow(t *testing.T) {
assert.Contains(t, lines[0], "Starts at : 7:30AM Mon-Fri (Europe/Dublin)")
assert.Contains(t, lines[1], "Starts next : 7:30AM IST on ")
assert.Contains(t, lines[2], "Stops at : 8h after start")
assert.Contains(t, lines[3], "Stops next : ")
assert.NotContains(t, lines[3], "Stops next : -")
}
})

Expand Down
44 changes: 7 additions & 37 deletions cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@ import (
"strings"
"time"

"golang.org/x/exp/constraints"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/coderd/util/tz"

"github.com/coder/coder/codersdk"
)

var errInvalidScheduleFormat = xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
Expand Down Expand Up @@ -63,41 +59,15 @@ func durationDisplay(d time.Duration) string {
return sign + durationDisplay
}

// Sign returns the sign of n. 0 is considered positive.
func sign[T constraints.Integer | constraints.Float | time.Duration](n T) string {
if n < 0 {
return "-"
}
return "+"
}

// hasExtension returns the deadline extension of ws, if it is present.
// This is calculated from the time the provisioner job was completed.
// Note that the extension may be negative.
func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return false, 0
}
if ws.LatestBuild.Job.CompletedAt == nil || ws.LatestBuild.Job.CompletedAt.IsZero() {
return false, 0
// relative relativizes a duration with the prefix "ago" or "in"
func relative(d time.Duration) string {
if d > 0 {
return "in " + durationDisplay(d)
}
if ws.LatestBuild.Deadline.IsZero() {
return false, 0
if d < 0 {
return durationDisplay(d) + " ago"
}
if ptr.NilOrZero(ws.TTLMillis) {
return false, 0
}
ttl := time.Duration(*ws.TTLMillis) * time.Millisecond
delta := ws.LatestBuild.Deadline.Add(-ttl).Sub(*ws.LatestBuild.Job.CompletedAt)
cutoff := time.Minute
if delta < -cutoff {
return true, delta
}
if delta > cutoff {
return true, delta
}

return false, 0
return "now"
}

// parseCLISchedule parses a schedule in the format HH:MM{AM|PM} [DOW] [LOCATION]
Expand Down
128 changes: 4 additions & 124 deletions cli/util_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)

func TestDurationDisplay(t *testing.T) {
Expand Down Expand Up @@ -44,126 +41,9 @@ func TestDurationDisplay(t *testing.T) {
}
}

func TestSign(t *testing.T) {
t.Parallel()
assert.Equal(t, sign(0), "+")
assert.Equal(t, sign(1), "+")
assert.Equal(t, sign(-1), "-")
}

func TestHasExtension(t *testing.T) {
func TestRelative(t *testing.T) {
t.Parallel()
for _, testCase := range []struct {
Name string
Workspace codersdk.Workspace
ExpectedFound bool
ExpectedDuration time.Duration
}{
{
Name: "Stopped",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStop,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "Building",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Job: codersdk.ProvisionerJob{
CompletedAt: nil,
},
Transition: codersdk.WorkspaceTransitionStart,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "BuiltNoDeadline",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Time{},
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "BuiltNoTTL",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(8 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: nil, // explicit
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "PositiveDelta",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(9*time.Hour + 30*time.Second),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: true,
ExpectedDuration: time.Hour,
},
{
Name: "NegativeDelta",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(7 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: true,
ExpectedDuration: -time.Hour,
},
{
Name: "Epsilon",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(8 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: false,
ExpectedDuration: 0,
},
} {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
actualFound, actualDuration := hasExtension(testCase.Workspace)
if assert.Equal(t, testCase.ExpectedFound, actualFound) {
assert.InDelta(t, testCase.ExpectedDuration, actualDuration, float64(time.Minute))
}
})
}
assert.Equal(t, relative(time.Minute), "in 1m")
assert.Equal(t, relative(-time.Minute), "1m ago")
assert.Equal(t, relative(0), "now")
}