Skip to content

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

Merged
merged 7 commits into from
Nov 10, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
ensure tz remains the same
  • Loading branch information
johnstcn committed Nov 10, 2023
commit 9a98fbf1711d8e27ff721c47121e3dfdf5277b4a
85 changes: 39 additions & 46 deletions cli/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbfake"
"github.com/coder/coder/v2/coderd/schedule/cron"
"github.com/coder/coder/v2/coderd/util/tz"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
Expand All @@ -30,6 +31,8 @@ import (
// It returns the owner and member clients, the database, and the workspaces.
// The workspaces are returned in the same order as they are created.
func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberClient *codersdk.Client, db database.Store, ws []codersdk.Workspace) {
t.Helper()

ownerClient, db = coderdtest.NewWithDatabase(t, nil)
owner := coderdtest.CreateFirstUser(t, ownerClient)
memberClient, memberUser := coderdtest.CreateAnotherUserMutators(t, ownerClient, owner.OrganizationID, nil, func(r *codersdk.CreateUserRequest) {
Expand Down Expand Up @@ -75,18 +78,20 @@ func setupTestSchedule(t *testing.T, sched *cron.Schedule) (ownerClient, memberC
return ownerClient, memberClient, db, ws
}

//nolint:paralleltest // t.Setenv
func TestScheduleShow(t *testing.T) {
t.Parallel()

// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, memberClient, _, ws := setupTestSchedule(t, sched)
now := time.Now()

t.Run("OwnerNoArgs", func(t *testing.T) {
t.Parallel()

// When: owner specifies no args
inv, root := clitest.New(t, "schedule", "show")
//nolint:gocritic // Testing that owner user sees all
Expand All @@ -98,18 +103,16 @@ func TestScheduleShow(t *testing.T) {
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})

t.Run("OwnerAll", func(t *testing.T) {
t.Parallel()

// When: owner lists all workspaces
inv, root := clitest.New(t, "schedule", "show", "--all")
//nolint:gocritic // Testing that owner user sees all
Expand All @@ -121,24 +124,22 @@ func TestScheduleShow(t *testing.T) {
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
// 3rd workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})

t.Run("OwnerSearchByName", func(t *testing.T) {
t.Parallel()

// When: owner specifies a search query
inv, root := clitest.New(t, "schedule", "show", "--search", "name:"+ws[1].Name)
//nolint:gocritic // Testing that owner user sees all
Expand All @@ -150,12 +151,10 @@ func TestScheduleShow(t *testing.T) {
// 2nd workspace: b-owner-ws2 has only autostart enabled.
pty.ExpectMatch(ws[1].OwnerName + "/" + ws[1].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})

t.Run("OwnerOneArg", func(t *testing.T) {
t.Parallel()

// When: owner asks for a specific workspace by name
inv, root := clitest.New(t, "schedule", "show", ws[2].OwnerName+"/"+ws[2].Name)
//nolint:gocritic // Testing that owner user sees all
Expand All @@ -167,12 +166,10 @@ func TestScheduleShow(t *testing.T) {
// 3rd workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})

t.Run("MemberNoArgs", func(t *testing.T) {
t.Parallel()

// When: a member specifies no args
inv, root := clitest.New(t, "schedule", "show")
clitest.SetupConfig(t, memberClient, root)
Expand All @@ -183,14 +180,12 @@ func TestScheduleShow(t *testing.T) {
// 1st workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})

t.Run("MemberAll", func(t *testing.T) {
t.Parallel()

// When: a member lists all workspaces
inv, root := clitest.New(t, "schedule", "show", "--all")
clitest.SetupConfig(t, memberClient, root)
Expand All @@ -206,14 +201,12 @@ func TestScheduleShow(t *testing.T) {
// 1st workspace: c-member-ws3 has only autostop enabled.
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
// 2nd workspace: d-member-ws4 has neither autostart nor autostop enabled.
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
})

t.Run("JSON", func(t *testing.T) {
t.Parallel()

// When: owner lists all workspaces in JSON format
inv, root := clitest.New(t, "schedule", "show", "--all", "--output", "json")
var buf bytes.Buffer
Expand All @@ -239,21 +232,21 @@ func TestScheduleShow(t *testing.T) {
// 1st workspace: a-owner-ws1 has both autostart and autostop enabled.
assert.Equal(t, ws[0].OwnerName+"/"+ws[0].Name, parsed[0]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[0]["starts_at"])
assert.Equal(t, sched.Next(now).Format(time.RFC3339), parsed[0]["starts_next"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[0]["starts_next"])
assert.Equal(t, "8h", parsed[0]["stops_after"])
assert.Equal(t, ws[0].LatestBuild.Deadline.Time.Format(time.RFC3339), parsed[0]["stops_next"])
assert.Equal(t, ws[0].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[0]["stops_next"])
// 2nd workspace: b-owner-ws2 has only autostart enabled.
assert.Equal(t, ws[1].OwnerName+"/"+ws[1].Name, parsed[1]["workspace"])
assert.Equal(t, sched.Humanize(), parsed[1]["starts_at"])
assert.Equal(t, sched.Next(now).Format(time.RFC3339), parsed[1]["starts_next"])
assert.Equal(t, sched.Next(now).In(loc).Format(time.RFC3339), parsed[1]["starts_next"])
assert.Empty(t, parsed[1]["stops_after"])
assert.Empty(t, parsed[1]["stops_next"])
// 3rd workspace: c-member-ws3 has only autostop enabled.
assert.Equal(t, ws[2].OwnerName+"/"+ws[2].Name, parsed[2]["workspace"])
assert.Empty(t, parsed[2]["starts_at"])
assert.Empty(t, parsed[2]["starts_next"])
assert.Equal(t, "8h", parsed[2]["stops_after"])
assert.Equal(t, ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339), parsed[2]["stops_next"])
assert.Equal(t, ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339), parsed[2]["stops_next"])
// 4th workspace: d-member-ws4 has neither autostart nor autostop enabled.
assert.Equal(t, ws[3].OwnerName+"/"+ws[3].Name, parsed[3]["workspace"])
assert.Empty(t, parsed[3]["starts_at"])
Expand All @@ -262,18 +255,20 @@ func TestScheduleShow(t *testing.T) {
})
}

//nolint:paralleltest // t.Setenv
func TestScheduleModify(t *testing.T) {
t.Parallel()

// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
now := time.Now()

t.Run("SetStart", func(t *testing.T) {
t.Parallel()

// When: we set the start schedule
inv, root := clitest.New(t,
"schedule", "start", ws[3].OwnerName+"/"+ws[3].Name, "7:30AM", "Mon-Fri", "Europe/Dublin",
Expand All @@ -286,12 +281,10 @@ func TestScheduleModify(t *testing.T) {
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[3].OwnerName + "/" + ws[3].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
})

t.Run("SetStop", func(t *testing.T) {
t.Parallel()

// When: we set the stop schedule
inv, root := clitest.New(t,
"schedule", "stop", ws[2].OwnerName+"/"+ws[2].Name, "8h30m",
Expand All @@ -304,12 +297,10 @@ func TestScheduleModify(t *testing.T) {
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[2].OwnerName + "/" + ws[2].Name)
pty.ExpectMatch("8h30m")
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.Format(time.RFC3339))
pty.ExpectMatch(ws[2].LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339))
})

t.Run("UnsetStart", func(t *testing.T) {
t.Parallel()

// When: we unset the start schedule
inv, root := clitest.New(t,
"schedule", "start", ws[1].OwnerName+"/"+ws[1].Name, "manual",
Expand All @@ -324,8 +315,6 @@ func TestScheduleModify(t *testing.T) {
})

t.Run("UnsetStop", func(t *testing.T) {
t.Parallel()

// When: we unset the stop schedule
inv, root := clitest.New(t,
"schedule", "stop", ws[0].OwnerName+"/"+ws[0].Name, "manual",
Expand All @@ -340,10 +329,14 @@ func TestScheduleModify(t *testing.T) {
})
}

//nolint:paralleltest // t.Setenv
func TestScheduleOverride(t *testing.T) {
t.Parallel()

// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
Expand All @@ -363,7 +356,7 @@ func TestScheduleOverride(t *testing.T) {
// Then: the updated schedule should be shown
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).Format(time.RFC3339))
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(expectedDeadline)
}