mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
autostart/autostop: move to traditional 5-valued cron string for compatibility (#1049)
This PR modfies the original 3-valued cron strings used in package schedule to be traditional 5-valued cron strings. - schedule.Weekly will validate that the month and dom fields are equal to * - cli autostart/autostop will attempt to detect local timezone using TZ env var, defaulting to UTC - cli autostart/autostop no longer accepts a raw schedule -- instead use the --minute, --hour, --dow, and --tz arguments. - Default schedules are provided that should suffice for most users. Fixes #993
This commit is contained in:
@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -11,20 +12,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
|
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
|
||||||
When enabling autostart, provide a schedule. This schedule is in cron format except only
|
When enabling autostart, provide the minute, hour, and day(s) of week.
|
||||||
the following fields are allowed:
|
The default schedule is at 09:00 in your local timezone (TZ env, UTC by default).
|
||||||
- minute
|
`
|
||||||
- hour
|
|
||||||
- day of week
|
|
||||||
|
|
||||||
For example, to start your workspace every weekday at 9.30 am, provide the schedule '30 9 1-5'.`
|
|
||||||
|
|
||||||
func workspaceAutostart() *cobra.Command {
|
func workspaceAutostart() *cobra.Command {
|
||||||
autostartCmd := &cobra.Command{
|
autostartCmd := &cobra.Command{
|
||||||
Use: "autostart enable <workspace> <schedule>",
|
Use: "autostart enable <workspace>",
|
||||||
Short: "schedule a workspace to automatically start at a regular time",
|
Short: "schedule a workspace to automatically start at a regular time",
|
||||||
Long: autostartDescriptionLong,
|
Long: autostartDescriptionLong,
|
||||||
Example: "coder workspaces autostart enable my-workspace '30 9 1-5'",
|
Example: "coder workspaces autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
|
||||||
Hidden: true, // TODO(cian): un-hide when autostart scheduling implemented
|
Hidden: true, // TODO(cian): un-hide when autostart scheduling implemented
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,22 +32,28 @@ func workspaceAutostart() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func workspaceAutostartEnable() *cobra.Command {
|
func workspaceAutostartEnable() *cobra.Command {
|
||||||
return &cobra.Command{
|
// yes some of these are technically numbers but the cron library will do that work
|
||||||
|
var autostartMinute string
|
||||||
|
var autostartHour string
|
||||||
|
var autostartDayOfWeek string
|
||||||
|
var autostartTimezone string
|
||||||
|
cmd := &cobra.Command{
|
||||||
Use: "enable <workspace_name> <schedule>",
|
Use: "enable <workspace_name> <schedule>",
|
||||||
ValidArgsFunction: validArgsWorkspaceName,
|
ValidArgsFunction: validArgsWorkspaceName,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := createClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
|
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostartTimezone, autostartMinute, autostartHour, autostartDayOfWeek)
|
||||||
|
validSchedule, err := schedule.Weekly(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
validSchedule, err := schedule.Weekly(args[1])
|
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -67,6 +70,16 @@ func workspaceAutostartEnable() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&autostartMinute, "minute", "0", "autostart minute")
|
||||||
|
cmd.Flags().StringVar(&autostartHour, "hour", "9", "autostart hour")
|
||||||
|
cmd.Flags().StringVar(&autostartDayOfWeek, "days", "1-5", "autostart day(s) of week")
|
||||||
|
tzEnv := os.Getenv("TZ")
|
||||||
|
if tzEnv == "" {
|
||||||
|
tzEnv = "UTC"
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&autostartTimezone, "tz", tzEnv, "autostart timezone")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func workspaceAutostartDisable() *cobra.Command {
|
func workspaceAutostartDisable() *cobra.Command {
|
||||||
|
@ -3,6 +3,8 @@ package cli_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -27,11 +29,13 @@ func TestWorkspaceAutostart(t *testing.T) {
|
|||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||||
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
|
tz = "Europe/Dublin"
|
||||||
|
cmdArgs = []string{"workspaces", "autostart", "enable", workspace.Name, "--minute", "30", "--hour", "9", "--days", "1-5", "--tz", tz}
|
||||||
|
sched = "CRON_TZ=Europe/Dublin 30 9 * * 1-5"
|
||||||
stdoutBuf = &bytes.Buffer{}
|
stdoutBuf = &bytes.Buffer{}
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
|
cmd, root := clitest.New(t, cmdArgs...)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
cmd.SetOut(stdoutBuf)
|
cmd.SetOut(stdoutBuf)
|
||||||
|
|
||||||
@ -68,10 +72,9 @@ func TestWorkspaceAutostart(t *testing.T) {
|
|||||||
user = coderdtest.CreateFirstUser(t, client)
|
user = coderdtest.CreateFirstUser(t, client)
|
||||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist", sched)
|
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist")
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
@ -96,34 +99,7 @@ func TestWorkspaceAutostart(t *testing.T) {
|
|||||||
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
|
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Enable_InvalidSchedule", func(t *testing.T) {
|
t.Run("Enable_DefaultSchedule", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
ctx = context.Background()
|
|
||||||
client = coderdtest.New(t, nil)
|
|
||||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
|
||||||
user = coderdtest.CreateFirstUser(t, client)
|
|
||||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
|
||||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
|
||||||
sched = "sdfasdfasdf asdf asdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
|
|
||||||
clitest.SetupConfig(t, client, root)
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")
|
|
||||||
|
|
||||||
// Ensure nothing happened
|
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
|
||||||
require.NoError(t, err, "fetch updated workspace")
|
|
||||||
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Enable_NoSchedule", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -137,15 +113,21 @@ func TestWorkspaceAutostart(t *testing.T) {
|
|||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// check current TZ env var
|
||||||
|
currTz := os.Getenv("TZ")
|
||||||
|
if currTz == "" {
|
||||||
|
currTz = "UTC"
|
||||||
|
}
|
||||||
|
expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 9 * * 1-5", currTz)
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name)
|
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")
|
require.NoError(t, err, "unexpected error")
|
||||||
|
|
||||||
// Ensure nothing happened
|
// Ensure nothing happened
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err, "fetch updated workspace")
|
require.NoError(t, err, "fetch updated workspace")
|
||||||
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
|
require.Equal(t, expectedSchedule, updated.AutostartSchedule, "expected default autostart schedule")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -11,20 +12,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop.
|
const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop.
|
||||||
When enabling autostop, provide a schedule. This schedule is in cron format except only
|
When enabling autostop, provide the minute, hour, and day(s) of week.
|
||||||
the following fields are allowed:
|
The default autostop schedule is at 18:00 in your local timezone (TZ env, UTC by default).
|
||||||
- minute
|
`
|
||||||
- hour
|
|
||||||
- day of week
|
|
||||||
|
|
||||||
For example, to stop your workspace every weekday at 5.30 pm, provide the schedule '30 17 1-5'.`
|
|
||||||
|
|
||||||
func workspaceAutostop() *cobra.Command {
|
func workspaceAutostop() *cobra.Command {
|
||||||
autostopCmd := &cobra.Command{
|
autostopCmd := &cobra.Command{
|
||||||
Use: "autostop enable <workspace> <schedule>",
|
Use: "autostop enable <workspace>",
|
||||||
Short: "schedule a workspace to automatically start at a regular time",
|
Short: "schedule a workspace to automatically stop at a regular time",
|
||||||
Long: autostopDescriptionLong,
|
Long: autostopDescriptionLong,
|
||||||
Example: "coder workspaces autostop enable my-workspace '30 17 1-5'",
|
Example: "coder workspaces autostop enable my-workspace --minute 0 --hour 18 --days 1-5 -tz Europe/Dublin",
|
||||||
Hidden: true, // TODO(cian): un-hide when autostop scheduling implemented
|
Hidden: true, // TODO(cian): un-hide when autostop scheduling implemented
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,22 +32,28 @@ func workspaceAutostop() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func workspaceAutostopEnable() *cobra.Command {
|
func workspaceAutostopEnable() *cobra.Command {
|
||||||
return &cobra.Command{
|
// yes some of these are technically numbers but the cron library will do that work
|
||||||
|
var autostopMinute string
|
||||||
|
var autostopHour string
|
||||||
|
var autostopDayOfWeek string
|
||||||
|
var autostopTimezone string
|
||||||
|
cmd := &cobra.Command{
|
||||||
Use: "enable <workspace_name> <schedule>",
|
Use: "enable <workspace_name> <schedule>",
|
||||||
ValidArgsFunction: validArgsWorkspaceName,
|
ValidArgsFunction: validArgsWorkspaceName,
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
client, err := createClient(cmd)
|
client, err := createClient(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
|
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostopTimezone, autostopMinute, autostopHour, autostopDayOfWeek)
|
||||||
|
validSchedule, err := schedule.Weekly(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
validSchedule, err := schedule.Weekly(args[1])
|
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -67,6 +70,16 @@ func workspaceAutostopEnable() *cobra.Command {
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&autostopMinute, "minute", "0", "autostop minute")
|
||||||
|
cmd.Flags().StringVar(&autostopHour, "hour", "18", "autostop hour")
|
||||||
|
cmd.Flags().StringVar(&autostopDayOfWeek, "days", "1-5", "autostop day(s) of week")
|
||||||
|
tzEnv := os.Getenv("TZ")
|
||||||
|
if tzEnv == "" {
|
||||||
|
tzEnv = "UTC"
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&autostopTimezone, "tz", tzEnv, "autostop timezone")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func workspaceAutostopDisable() *cobra.Command {
|
func workspaceAutostopDisable() *cobra.Command {
|
||||||
|
@ -3,6 +3,8 @@ package cli_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -27,11 +29,12 @@ func TestWorkspaceAutostop(t *testing.T) {
|
|||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||||
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
|
cmdArgs = []string{"workspaces", "autostop", "enable", workspace.Name, "--minute", "30", "--hour", "17", "--days", "1-5", "--tz", "Europe/Dublin"}
|
||||||
|
sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5"
|
||||||
stdoutBuf = &bytes.Buffer{}
|
stdoutBuf = &bytes.Buffer{}
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name, sched)
|
cmd, root := clitest.New(t, cmdArgs...)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
cmd.SetOut(stdoutBuf)
|
cmd.SetOut(stdoutBuf)
|
||||||
|
|
||||||
@ -68,10 +71,9 @@ func TestWorkspaceAutostop(t *testing.T) {
|
|||||||
user = coderdtest.CreateFirstUser(t, client)
|
user = coderdtest.CreateFirstUser(t, client)
|
||||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||||
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", "doesnotexist", sched)
|
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", "doesnotexist")
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
@ -96,34 +98,7 @@ func TestWorkspaceAutostop(t *testing.T) {
|
|||||||
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
|
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Enable_InvalidSchedule", func(t *testing.T) {
|
t.Run("Enable_DefaultSchedule", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var (
|
|
||||||
ctx = context.Background()
|
|
||||||
client = coderdtest.New(t, nil)
|
|
||||||
_ = coderdtest.NewProvisionerDaemon(t, client)
|
|
||||||
user = coderdtest.CreateFirstUser(t, client)
|
|
||||||
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
||||||
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
|
||||||
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
|
||||||
sched = "sdfasdfasdf asdf asdf"
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name, sched)
|
|
||||||
clitest.SetupConfig(t, client, root)
|
|
||||||
|
|
||||||
err := cmd.Execute()
|
|
||||||
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")
|
|
||||||
|
|
||||||
// Ensure nothing happened
|
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
|
||||||
require.NoError(t, err, "fetch updated workspace")
|
|
||||||
require.Empty(t, updated.AutostopSchedule, "expected autostop schedule to be empty")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Enable_NoSchedule", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -137,15 +112,22 @@ func TestWorkspaceAutostop(t *testing.T) {
|
|||||||
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// check current TZ env var
|
||||||
|
currTz := os.Getenv("TZ")
|
||||||
|
if currTz == "" {
|
||||||
|
currTz = "UTC"
|
||||||
|
}
|
||||||
|
expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 18 * * 1-5", currTz)
|
||||||
|
|
||||||
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name)
|
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name)
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, root)
|
||||||
|
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")
|
require.NoError(t, err, "unexpected error")
|
||||||
|
|
||||||
// Ensure nothing happened
|
// Ensure nothing happened
|
||||||
updated, err := client.Workspace(ctx, workspace.ID)
|
updated, err := client.Workspace(ctx, workspace.ID)
|
||||||
require.NoError(t, err, "fetch updated workspace")
|
require.NoError(t, err, "fetch updated workspace")
|
||||||
require.Empty(t, updated.AutostopSchedule, "expected autostop schedule to be empty")
|
require.Equal(t, expectedSchedule, updated.AutostopSchedule, "expected default autostop schedule")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
package schedule
|
package schedule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
@ -10,16 +11,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// For the purposes of this library, we only need minute, hour, and
|
// For the purposes of this library, we only need minute, hour, and
|
||||||
// day-of-week.
|
// day-of-week. However to ensure interoperability we will use the standard
|
||||||
const parserFormatWeekly = cron.Minute | cron.Hour | cron.Dow
|
// five-valued cron format. Descriptors are not supported.
|
||||||
|
const parserFormat = cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow
|
||||||
|
|
||||||
var defaultParser = cron.NewParser(parserFormatWeekly)
|
var defaultParser = cron.NewParser(parserFormat)
|
||||||
|
|
||||||
// Weekly parses a Schedule from spec scoped to a recurring weekly event.
|
// Weekly parses a Schedule from spec scoped to a recurring weekly event.
|
||||||
// Spec consists of the following space-delimited fields, in the following order:
|
// Spec consists of the following space-delimited fields, in the following order:
|
||||||
// - timezone e.g. CRON_TZ=US/Central (optional)
|
// - timezone e.g. CRON_TZ=US/Central (optional)
|
||||||
// - minutes of hour e.g. 30 (required)
|
// - minutes of hour e.g. 30 (required)
|
||||||
// - hour of day e.g. 9 (required)
|
// - hour of day e.g. 9 (required)
|
||||||
|
// - day of month (must be *)
|
||||||
|
// - month (must be *)
|
||||||
// - day of week e.g. 1 (required)
|
// - day of week e.g. 1 (required)
|
||||||
//
|
//
|
||||||
// Example Usage:
|
// Example Usage:
|
||||||
@ -31,6 +35,10 @@ var defaultParser = cron.NewParser(parserFormatWeekly)
|
|||||||
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
|
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
|
||||||
// // Output: 2022-04-04T14:30:00Z
|
// // Output: 2022-04-04T14:30:00Z
|
||||||
func Weekly(spec string) (*Schedule, error) {
|
func Weekly(spec string) (*Schedule, error) {
|
||||||
|
if err := validateWeeklySpec(spec); err != nil {
|
||||||
|
return nil, xerrors.Errorf("validate weekly schedule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
specSched, err := defaultParser.Parse(spec)
|
specSched, err := defaultParser.Parse(spec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Errorf("parse schedule: %w", err)
|
return nil, xerrors.Errorf("parse schedule: %w", err)
|
||||||
@ -65,3 +73,19 @@ func (s Schedule) String() string {
|
|||||||
func (s Schedule) Next(t time.Time) time.Time {
|
func (s Schedule) Next(t time.Time) time.Time {
|
||||||
return s.sched.Next(t)
|
return s.sched.Next(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateWeeklySpec ensures that the day-of-month and month options of
|
||||||
|
// spec are both set to *
|
||||||
|
func validateWeeklySpec(spec string) error {
|
||||||
|
parts := strings.Fields(spec)
|
||||||
|
if len(parts) < 5 {
|
||||||
|
return xerrors.Errorf("expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix")
|
||||||
|
}
|
||||||
|
if len(parts) == 6 {
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
if parts[2] != "*" || parts[3] != "*" {
|
||||||
|
return xerrors.Errorf("expected month and dom to be *")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -20,14 +20,14 @@ func Test_Weekly(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "with timezone",
|
name: "with timezone",
|
||||||
spec: "CRON_TZ=US/Central 30 9 1-5",
|
spec: "CRON_TZ=US/Central 30 9 * * 1-5",
|
||||||
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
|
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
|
||||||
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
|
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "without timezone",
|
name: "without timezone",
|
||||||
spec: "30 9 1-5",
|
spec: "30 9 * * 1-5",
|
||||||
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
|
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
|
||||||
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
|
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
@ -37,15 +37,43 @@ func Test_Weekly(t *testing.T) {
|
|||||||
spec: "asdfasdfasdfsd",
|
spec: "asdfasdfasdfsd",
|
||||||
at: time.Time{},
|
at: time.Time{},
|
||||||
expectedNext: time.Time{},
|
expectedNext: time.Time{},
|
||||||
expectedError: "parse schedule: expected exactly 3 fields, found 1: [asdfasdfasdfsd]",
|
expectedError: "validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid location",
|
name: "invalid location",
|
||||||
spec: "CRON_TZ=Fictional/Country 30 9 1-5",
|
spec: "CRON_TZ=Fictional/Country 30 9 * * 1-5",
|
||||||
at: time.Time{},
|
at: time.Time{},
|
||||||
expectedNext: time.Time{},
|
expectedNext: time.Time{},
|
||||||
expectedError: "parse schedule: provided bad location Fictional/Country: unknown time zone Fictional/Country",
|
expectedError: "parse schedule: provided bad location Fictional/Country: unknown time zone Fictional/Country",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid schedule with 3 fields",
|
||||||
|
spec: "CRON_TZ=Fictional/Country 30 9 1-5",
|
||||||
|
at: time.Time{},
|
||||||
|
expectedNext: time.Time{},
|
||||||
|
expectedError: "validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid schedule with 3 fields and no timezone",
|
||||||
|
spec: "30 9 1-5",
|
||||||
|
at: time.Time{},
|
||||||
|
expectedNext: time.Time{},
|
||||||
|
expectedError: "validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid schedule with 5 fields but month and dom not set to *",
|
||||||
|
spec: "30 9 1 1 1-5",
|
||||||
|
at: time.Time{},
|
||||||
|
expectedNext: time.Time{},
|
||||||
|
expectedError: "validate weekly schedule: expected month and dom to be *",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid schedule with 5 fields and timezone but month and dom not set to *",
|
||||||
|
spec: "CRON_TZ=Europe/Dublin 30 9 1 1 1-5",
|
||||||
|
at: time.Time{},
|
||||||
|
expectedNext: time.Time{},
|
||||||
|
expectedError: "validate weekly schedule: expected month and dom to be *",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
@ -205,7 +205,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "friday to monday",
|
name: "friday to monday",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 9 1-5",
|
schedule: "CRON_TZ=Europe/Dublin 30 9 * * 1-5",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc),
|
||||||
@ -213,7 +213,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "monday to tuesday",
|
name: "monday to tuesday",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 9 1-5",
|
schedule: "CRON_TZ=Europe/Dublin 30 9 * * 1-5",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc),
|
||||||
@ -222,7 +222,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
||||||
name: "DST start",
|
name: "DST start",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
schedule: "CRON_TZ=Europe/Dublin 30 9 * * *",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc),
|
||||||
@ -231,7 +231,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
||||||
name: "DST end",
|
name: "DST end",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
schedule: "CRON_TZ=Europe/Dublin 30 9 * * *",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc),
|
||||||
@ -239,13 +239,18 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid location",
|
name: "invalid location",
|
||||||
schedule: "CRON_TZ=Imaginary/Place 30 9 1-5",
|
schedule: "CRON_TZ=Imaginary/Place 30 9 * * 1-5",
|
||||||
expectedError: "status code 500: invalid autostart schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
expectedError: "status code 500: invalid autostart schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid schedule",
|
name: "invalid schedule",
|
||||||
schedule: "asdf asdf asdf ",
|
schedule: "asdf asdf asdf ",
|
||||||
expectedError: `status code 500: invalid autostart schedule: parse schedule: failed to parse int from asdf: strconv.Atoi: parsing "asdf": invalid syntax`,
|
expectedError: `status code 500: invalid autostart schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only 3 values",
|
||||||
|
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
||||||
|
expectedError: `status code 500: invalid autostart schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +339,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "friday to monday",
|
name: "friday to monday",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
|
schedule: "CRON_TZ=Europe/Dublin 30 17 * * 1-5",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 5, 6, 17, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 5, 6, 17, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 5, 9, 17, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 5, 9, 17, 30, 0, 0, dublinLoc),
|
||||||
@ -342,7 +347,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "monday to tuesday",
|
name: "monday to tuesday",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 17 1-5",
|
schedule: "CRON_TZ=Europe/Dublin 30 17 * * 1-5",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 5, 9, 17, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 5, 9, 17, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 5, 10, 17, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 5, 10, 17, 30, 0, 0, dublinLoc),
|
||||||
@ -351,7 +356,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
|
||||||
name: "DST start",
|
name: "DST start",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
|
schedule: "CRON_TZ=Europe/Dublin 30 17 * * *",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 3, 26, 17, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 3, 26, 17, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 3, 27, 17, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 3, 27, 17, 30, 0, 0, dublinLoc),
|
||||||
@ -360,7 +365,7 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
|
||||||
name: "DST end",
|
name: "DST end",
|
||||||
schedule: "CRON_TZ=Europe/Dublin 30 17 *",
|
schedule: "CRON_TZ=Europe/Dublin 30 17 * * *",
|
||||||
expectedError: "",
|
expectedError: "",
|
||||||
at: time.Date(2022, 10, 29, 17, 31, 0, 0, dublinLoc),
|
at: time.Date(2022, 10, 29, 17, 31, 0, 0, dublinLoc),
|
||||||
expectedNext: time.Date(2022, 10, 30, 17, 30, 0, 0, dublinLoc),
|
expectedNext: time.Date(2022, 10, 30, 17, 30, 0, 0, dublinLoc),
|
||||||
@ -368,13 +373,18 @@ func TestWorkspaceUpdateAutostop(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid location",
|
name: "invalid location",
|
||||||
schedule: "CRON_TZ=Imaginary/Place 30 17 1-5",
|
schedule: "CRON_TZ=Imaginary/Place 30 17 * * 1-5",
|
||||||
expectedError: "status code 500: invalid autostop schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
expectedError: "status code 500: invalid autostop schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid schedule",
|
name: "invalid schedule",
|
||||||
schedule: "asdf asdf asdf ",
|
schedule: "asdf asdf asdf ",
|
||||||
expectedError: `status code 500: invalid autostop schedule: parse schedule: failed to parse int from asdf: strconv.Atoi: parsing "asdf": invalid syntax`,
|
expectedError: `status code 500: invalid autostop schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only 3 values",
|
||||||
|
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
|
||||||
|
expectedError: `status code 500: invalid autostop schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import Box from "@material-ui/core/Box"
|
|||||||
import Typography from "@material-ui/core/Typography"
|
import Typography from "@material-ui/core/Typography"
|
||||||
import cronstrue from "cronstrue"
|
import cronstrue from "cronstrue"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { expandScheduleCronString, extractTimezone } from "../../util/schedule"
|
import { extractTimezone, stripTimezone } from "../../util/schedule"
|
||||||
import { WorkspaceSection } from "./WorkspaceSection"
|
import { WorkspaceSection } from "./WorkspaceSection"
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
@ -26,7 +26,7 @@ const Language = {
|
|||||||
},
|
},
|
||||||
cronHumanDisplay: (schedule: string): string => {
|
cronHumanDisplay: (schedule: string): string => {
|
||||||
if (schedule) {
|
if (schedule) {
|
||||||
return cronstrue.toString(expandScheduleCronString(schedule), { throwExceptionOnParseError: false })
|
return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
|
||||||
}
|
}
|
||||||
return "Manual"
|
return "Manual"
|
||||||
},
|
},
|
||||||
|
@ -68,7 +68,7 @@ export const MockWorkspaceAutostartDisabled: WorkspaceAutostartRequest = {
|
|||||||
export const MockWorkspaceAutostartEnabled: WorkspaceAutostartRequest = {
|
export const MockWorkspaceAutostartEnabled: WorkspaceAutostartRequest = {
|
||||||
// Runs at 9:30am Monday through Friday using Canada/Eastern
|
// Runs at 9:30am Monday through Friday using Canada/Eastern
|
||||||
// (America/Toronto) time
|
// (America/Toronto) time
|
||||||
schedule: "CRON_TZ=Canada/Eastern 30 9 1-5",
|
schedule: "CRON_TZ=Canada/Eastern 30 9 * * 1-5",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockWorkspaceAutostopDisabled: WorkspaceAutostartRequest = {
|
export const MockWorkspaceAutostopDisabled: WorkspaceAutostartRequest = {
|
||||||
@ -77,7 +77,7 @@ export const MockWorkspaceAutostopDisabled: WorkspaceAutostartRequest = {
|
|||||||
|
|
||||||
export const MockWorkspaceAutostopEnabled: WorkspaceAutostartRequest = {
|
export const MockWorkspaceAutostopEnabled: WorkspaceAutostartRequest = {
|
||||||
// Runs at 9:30pm Monday through Friday using America/Toronto
|
// Runs at 9:30pm Monday through Friday using America/Toronto
|
||||||
schedule: "CRON_TZ=America/Toronto 30 21 1-5",
|
schedule: "CRON_TZ=America/Toronto 30 21 * * 1-5",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MockWorkspace: Workspace = {
|
export const MockWorkspace: Workspace = {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { expandScheduleCronString, extractTimezone, stripTimezone } from "./schedule"
|
import { extractTimezone, stripTimezone } from "./schedule"
|
||||||
|
|
||||||
describe("util/schedule", () => {
|
describe("util/schedule", () => {
|
||||||
describe("stripTimezone", () => {
|
describe("stripTimezone", () => {
|
||||||
@ -20,14 +20,4 @@ describe("util/schedule", () => {
|
|||||||
expect(extractTimezone(input)).toBe(expected)
|
expect(extractTimezone(input)).toBe(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("expandScheduleCronString", () => {
|
|
||||||
it.each<[string, string]>([
|
|
||||||
["CRON_TZ=Canada/Eastern 30 9 1-5", "30 9 * * 1-5"],
|
|
||||||
["CRON_TZ=America/Central 0 8 1,2,4,5", "0 8 * * 1,2,4,5"],
|
|
||||||
["30 9 1-5", "30 9 * * 1-5"],
|
|
||||||
])(`expandScheduleCronString(%p) returns %p`, (input, expected) => {
|
|
||||||
expect(expandScheduleCronString(input)).toBe(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
@ -30,25 +30,3 @@ export const extractTimezone = (raw: string): string => {
|
|||||||
return DEFAULT_TIMEZONE
|
return DEFAULT_TIMEZONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* expandScheduleCronString ensures a Schedule is expanded to a valid 5-value
|
|
||||||
* cron string by inserting '*' in month and day positions. If there is a
|
|
||||||
* leading timezone, it is removed.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* expandScheduleCronString("30 9 1-5") // -> "30 9 * * 1-5"
|
|
||||||
*/
|
|
||||||
export const expandScheduleCronString = (schedule: string): string => {
|
|
||||||
const prepared = stripTimezone(schedule).trim()
|
|
||||||
|
|
||||||
const parts = prepared.split(" ")
|
|
||||||
|
|
||||||
while (parts.length < 5) {
|
|
||||||
// insert '*' in the second to last position
|
|
||||||
// ie [a, b, c] --> [a, b, *, c]
|
|
||||||
parts.splice(parts.length - 1, 0, "*")
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.join(" ")
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user