Skip to content

Commit

Permalink
Finalize cron subcommand set.
Browse files Browse the repository at this point in the history
  • Loading branch information
lnsp committed Jul 15, 2024
1 parent a9dc637 commit b7a0f56
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 68 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ valar auth clear [path] [user] [read | write | invoke | manage ]
```bash
valar auth check [path] [user] [read | write | invoke | manage]
```
> In case of a public project, this means only the project owner has write, read and invoke access, while any person may invoke a service of the project.
### Cron
Expand All @@ -295,13 +296,13 @@ valar cron list
#### Schedule a service invocation
```bash
valar cron add [name] [schedule] [--path path] [--data payload] [--service service]
valar cron set [name] [schedule] [--path path] [--data payload] [--service service] [--enabled|--disabled]
```
#### Remove a scheduled service invocation
```bash
valar cron remove [name] [--service service]
valar cron delete [name] [--service service]
```
#### Trigger a cron invoke manually
Expand Down
50 changes: 36 additions & 14 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import (
"time"
)

type Error struct {
type JSONError struct {
Err string `json:"error"`
}

func (err Error) Error() string {
func (err JSONError) Error() string {
return err.Err
}

Expand Down Expand Up @@ -48,12 +48,21 @@ func (client *Client) check() error {
return nil
}

type Error struct {
StatusCode int
ServerError error
}

func (err Error) Error() string {
return fmt.Sprintf("%s: %v", http.StatusText(err.StatusCode), err.ServerError)
}

func parseErrorResponse(body []byte) error {
serverErr := Error{}
serverErr := JSONError{}
if err := json.Unmarshal(body, &serverErr); err != nil {
return fmt.Errorf("unmarshalling error response: %w", err)
}
return fmt.Errorf("server: %w", serverErr)
return serverErr
}

func (client *Client) streamRequest(method, path string, w io.Writer) error {
Expand All @@ -74,7 +83,10 @@ func (client *Client) streamRequest(method, path string, w io.Writer) error {
if err != nil {
return fmt.Errorf("fetching response: %w", err)
}
return parseErrorResponse(body)
return Error{
StatusCode: resp.StatusCode,
ServerError: parseErrorResponse(body),
}
}
if _, err := io.Copy(w, resp.Body); err != nil && err != io.EOF {
return fmt.Errorf("copying request: %w", err)
Expand All @@ -99,7 +111,10 @@ func (client *Client) request(method, path string, obj interface{}, post io.Read
return fmt.Errorf("fetching request: %w", err)
}
if resp.StatusCode != http.StatusOK {
return parseErrorResponse(body)
return Error{
StatusCode: resp.StatusCode,
ServerError: parseErrorResponse(body),
}
}
if obj != nil {
if err := json.Unmarshal(body, obj); err != nil {
Expand Down Expand Up @@ -424,15 +439,15 @@ func (client *Client) ListSchedules(project, service string) ([]Schedule, error)
return schedules, nil
}

func (client *Client) AddSchedule(project, service string, sched Schedule) error {
func (client *Client) SetSchedule(project, service string, sched Schedule) error {
var (
path = fmt.Sprintf("/projects/%s/services/%s/schedules", project, service)
payload, _ = json.Marshal(sched)
)
return client.request(http.MethodPost, path, nil, bytes.NewReader(payload))
}

func (client *Client) RemoveSchedule(project, service, schedule string) error {
func (client *Client) DeleteSchedule(project, service, schedule string) error {
var (
path = fmt.Sprintf("/projects/%s/services/%s/schedules/%s", project, service, schedule)
)
Expand All @@ -446,29 +461,36 @@ func (client *Client) TriggerSchedule(project, service, schedule string) error {
return client.request(http.MethodPost, path, nil, nil)
}

func (client *Client) InspectSchedule(project, service, schedule string) ([]ServiceInvocation, error) {
func (client *Client) InspectSchedule(project, service, schedule string) (*ScheduleDetails, error) {
var (
path = fmt.Sprintf("/projects/%s/services/%s/schedules/%s", project, service, schedule)
invocations []ServiceInvocation
path = fmt.Sprintf("/projects/%s/services/%s/schedules/%s", project, service, schedule)
details ScheduleDetails
)
if err := client.request(http.MethodGet, path, &invocations, nil); err != nil {
if err := client.request(http.MethodGet, path, &details, nil); err != nil {
return nil, err
}
return invocations, nil
return &details, nil
}

type Schedule struct {
Name string `json:"name"`
Timespec string `json:"timespec"`
Path string `json:"path"`
Payload string `json:"payload"`
Status string `json:"status"`
}

type ScheduleDetails struct {
LastRun *ServiceInvocation `json:"invocation"`
Schedule *Schedule `json:"schedule"`
}

type ServiceInvocation struct {
ID string `json:"id"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime,omitempty"`
Status string `json:"status"`
TriggerSource string `json:"triggered_by"`
TriggerSource string `json:"triggeredBy"`
}
type Domain struct {
Project string `json:"project"`
Expand Down
4 changes: 2 additions & 2 deletions cmd/builds.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ func colorize(status string) string {
return color.HiYellowString("%s", status)
case "building", "releasing", "binding", "running":
return color.YellowString("%s", status)
case "done", "succeeded":
case "done", "succeeded", "enabled":
return color.GreenString("%s", status)
case "failed":
case "failed", "disabled":
return color.RedString("%s", status)
default:
return status
Expand Down
125 changes: 81 additions & 44 deletions cmd/cron.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package cmd

import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"text/tabwriter"

"github.com/dustin/go-humanize"
"github.com/spf13/cobra"
"github.com/valar/cli/api"
"github.com/valar/cli/config"
Expand All @@ -17,12 +18,12 @@ var (

cronCmd = &cobra.Command{
Use: "cron",
Short: "Manage scheduled invocations of a service",
Short: "Manage scheduled invocations of a service.",
}

cronListCmd = &cobra.Command{
Use: "list",
Short: "List all cron schedules for a service",
Short: "List all cron schedules for a service.",
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &cronService, globalConfiguration)
if err != nil {
Expand All @@ -37,21 +38,23 @@ var (
return err
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(tw, "NAME\tTIMESPEC\tPATH\tPAYLOAD")
fmt.Fprintln(tw, "NAME\tTIMESPEC\tPATH\tSTATUS")
for _, sched := range schedules {
fmt.Fprintln(tw, strings.Join([]string{sched.Name, sched.Timespec, sched.Path, sched.Payload}, "\t"))
fmt.Fprintln(tw, strings.Join([]string{sched.Name, sched.Timespec, sched.Path, colorize(sched.Status)}, "\t"))
}
tw.Flush()
return nil
}),
}

cronAddPayload string
cronAddPath string
cronAddCmd = &cobra.Command{
Use: "add [--payload payload] [--path path] name timespec",
Short: "Add or edit a service invocation schedule",
Args: cobra.ExactArgs(2),
cronSetPayload string
cronSetPath string
cronSetEnabledCount int
cronSetDisabledCount int
cronSetCmd = &cobra.Command{
Use: "set [--payload payload] [--path path] [--enable|--disable] name [timespec]",
Short: "Set a service invocation schedule.",
Args: cobra.RangeArgs(1, 2),
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &cronService, globalConfiguration)
if err != nil {
Expand All @@ -61,12 +64,49 @@ var (
if err != nil {
return err
}
if err := client.AddSchedule(cfg.Project(), cfg.Service(), api.Schedule{
Name: args[0],
Timespec: args[1],
Payload: cronAddPayload,
Path: cronAddPath,
}); err != nil {
// Attempt to fetch the schedule already present.
var apiError api.Error
schedule := api.Schedule{}
existing, err := client.InspectSchedule(cfg.Project(), cfg.Service(), args[0])
if err != nil && !(errors.As(err, &apiError) && apiError.StatusCode == http.StatusNotFound) {
return err
}
if existing != nil {
schedule = *existing.Schedule
if len(args) == 2 {
schedule.Timespec = args[1]
}
if cmd.Flags().Changed("payload") {
schedule.Payload = cronSetPayload
}
if cmd.Flags().Changed("path") {
schedule.Path = cronSetPath
}
if cmd.Flags().Changed("enabled") || cmd.Flags().Changed("disabled") {
if cronSetEnabledCount-cronSetDisabledCount >= 0 {
schedule.Status = "enabled"
} else {
schedule.Status = "disabled"
}
}
} else {
if len(args) != 2 {
fmt.Fprintln(os.Stderr, "Timespec must be specified when setting a schedule for the first time.")
os.Exit(1)
}
schedule = api.Schedule{
Name: args[0],
Timespec: args[1],
Path: cronSetPath,
Payload: cronSetPayload,
}
if cronSetEnabledCount-cronSetDisabledCount >= 0 {
schedule.Status = "enabled"
} else {
schedule.Status = "disabled"
}
}
if err := client.SetSchedule(cfg.Project(), cfg.Service(), schedule); err != nil {
return err
}
return nil
Expand All @@ -75,7 +115,7 @@ var (

cronTriggerCmd = &cobra.Command{
Use: "trigger schedule",
Short: "Manually triggers a scheduled invocation",
Short: "Manually triggers a scheduled invocation.",
Args: cobra.ExactArgs(1),
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &cronService, globalConfiguration)
Expand All @@ -92,9 +132,9 @@ var (
return nil
}),
}
cronRemoveCmd = &cobra.Command{
Use: "remove schedule",
Short: "Remove a service invocation schedule",
cronDeleteCmd = &cobra.Command{
Use: "delete schedule",
Short: "Delete a service invocation schedule.",
Args: cobra.ExactArgs(1),
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &cronService, globalConfiguration)
Expand All @@ -105,15 +145,15 @@ var (
if err != nil {
return err
}
if err := client.RemoveSchedule(cfg.Project(), cfg.Service(), args[0]); err != nil {
if err := client.DeleteSchedule(cfg.Project(), cfg.Service(), args[0]); err != nil {
return err
}
return nil
}),
}
cronInspectCmd = &cobra.Command{
Use: "inspect schedule",
Short: "Inspect the invocation history of a service schedule",
Short: "Inspect the details of a service schedule.",
Args: cobra.ExactArgs(1),
Run: runAndHandle(func(cmd *cobra.Command, args []string) error {
cfg, err := config.NewServiceConfigWithFallback(functionConfiguration, &cronService, globalConfiguration)
Expand All @@ -124,28 +164,23 @@ var (
if err != nil {
return err
}
invocations, err := client.InspectSchedule(cfg.Project(), cfg.Service(), args[0])
details, err := client.InspectSchedule(cfg.Project(), cfg.Service(), args[0])
if err != nil {
return err
}
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
fmt.Fprintln(tw, "ID\tSTATUS\tSTARTED\tENDED")
for _, inv := range invocations {
startTimeStr := humanize.Time(inv.StartTime)
if inv.StartTime.IsZero() {
startTimeStr = "-"
}
endTimeStr := humanize.Time(inv.EndTime)
if inv.EndTime.IsZero() {
endTimeStr = "-"
}
fmt.Fprintln(tw, strings.Join(
[]string{
inv.ID,
colorize(inv.Status),
startTimeStr,
endTimeStr,
}, "\t"))
fmt.Fprintln(tw, "Name:\t", details.Schedule.Name)
fmt.Fprintln(tw, "Timespec:\t", details.Schedule.Timespec)
fmt.Fprintln(tw, "Path:\t", details.Schedule.Path)
fmt.Fprintln(tw, "Payload:\t", details.Schedule.Payload)
fmt.Fprintln(tw, "Status:\t", colorize(details.Schedule.Status))
if details.LastRun == nil {
fmt.Fprintln(tw, "Last Run:\t", "-")
} else {
fmt.Fprintln(tw, "Last Run:\t", "")
fmt.Fprintln(tw, " Start:\t", details.LastRun.StartTime)
fmt.Fprintln(tw, " End:\t", details.LastRun.EndTime)
fmt.Fprintln(tw, " Status:\t", colorize(details.LastRun.Status))
}
tw.Flush()
return nil
Expand All @@ -156,7 +191,9 @@ var (
func initCronCmd() {
rootCmd.AddCommand(cronCmd)
cronCmd.PersistentFlags().StringVarP(&cronService, "service", "s", "", "The service to manage cron schedules for")
cronCmd.AddCommand(cronListCmd, cronAddCmd, cronTriggerCmd, cronRemoveCmd, cronInspectCmd)
cronAddCmd.Flags().StringVar(&cronAddPath, "path", "/", "The service path to send a request to")
cronAddCmd.Flags().StringVar(&cronAddPayload, "payload", "", "The body payload to send in a request")
cronCmd.AddCommand(cronListCmd, cronSetCmd, cronTriggerCmd, cronDeleteCmd, cronInspectCmd)
cronSetCmd.Flags().StringVar(&cronSetPath, "path", "/", "The service path to send a request to")
cronSetCmd.Flags().StringVar(&cronSetPayload, "payload", "", "The body payload to send in a request")
cronSetCmd.Flags().CountVar(&cronSetEnabledCount, "enabled", "Enables the specified schedule")
cronSetCmd.Flags().CountVar(&cronSetDisabledCount, "disabled", "Disables the specified schedule to prevent it from triggering")
}
Loading

0 comments on commit b7a0f56

Please sign in to comment.