Skip to content

Commit

Permalink
Merge pull request #65 from jensoncs/master
Browse files Browse the repository at this point in the history
Add schedule functionality to proctor CLI
  • Loading branch information
olttwa authored Jan 17, 2019
2 parents 8ac000e + b3b12b3 commit c2a571a
Show file tree
Hide file tree
Showing 15 changed files with 361 additions and 6 deletions.
17 changes: 16 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package cmd

import (
"fmt"
"github.com/gojektech/proctor/cmd/schedule"
"github.com/gojektech/proctor/cmd/schedule/create"
"os"

"github.com/gojektech/proctor/cmd/config"
"github.com/gojektech/proctor/cmd/config/view"
"github.com/gojektech/proctor/cmd/description"
Expand Down Expand Up @@ -43,6 +44,20 @@ func Execute(printer io.Printer, proctorDClient daemon.Client) {
rootCmd.AddCommand(configCmd)
configCmd.AddCommand(configShowCmd)

scheduleCmd := schedule.NewCmd(printer)
rootCmd.AddCommand(scheduleCmd)
scheduleCreateCmd := create.NewCmd(printer, proctorDClient)
scheduleCmd.AddCommand(scheduleCreateCmd)

var Time, NotifyEmails, Tags string

scheduleCreateCmd.PersistentFlags().StringVarP(&Time, "time", "t", "", "Schedule time")
scheduleCreateCmd.MarkFlagRequired("time")
scheduleCreateCmd.PersistentFlags().StringVarP(&NotifyEmails, "notify", "n", "", "Notifier Email ID's")
scheduleCreateCmd.MarkFlagRequired("notify")
scheduleCreateCmd.PersistentFlags().StringVarP(&Tags, "tags", "T", "", "Tags")
scheduleCreateCmd.MarkFlagRequired("tags")

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
1 change: 1 addition & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ func TestRootCmdSubCommands(t *testing.T) {
assert.True(t, contains(rootCmd.Commands(), "list"))
assert.True(t, contains(rootCmd.Commands(), "config"))
assert.True(t, contains(rootCmd.Commands(), "version"))
assert.True(t,contains(rootCmd.Commands(), "schedule"))
}
67 changes: 67 additions & 0 deletions cmd/schedule/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package create

import (
"fmt"
"github.com/fatih/color"
"github.com/gojektech/proctor/daemon"
"github.com/gojektech/proctor/io"
"github.com/spf13/cobra"
"strings"
)

func NewCmd(printer io.Printer, proctorDClient daemon.Client) *cobra.Command {
return &cobra.Command{
Use: "create",
Short: "Create scheduled jobs",
Long: "This command helps to create scheduled jobs",
Example: fmt.Sprintf("proctor schedule create run-sample -t '0 2 * * *' -n 'username@mail.com' -T 'sample,proctor' ARG_ONE1=foobar"),
Args: cobra.MinimumNArgs(1),

Run: func(cmd *cobra.Command, args []string) {
procName := args[0]
printer.Println(fmt.Sprintf("%-40s %-100s", "Creating Scheduled Job", procName), color.Reset)
time, err := cmd.Flags().GetString("time")
if err != nil {
printer.Println(err.Error(),color.FgRed)
}

notificationEmails, err := cmd.Flags().GetString("notify")
if err != nil {
printer.Println(err.Error(),color.FgRed)
}

tags, err := cmd.Flags().GetString("tags")
if err != nil {
printer.Println(err.Error(),color.FgRed)
}

jobArgs := make(map[string]string)
if len(args) > 1 {
printer.Println("With Variables", color.FgMagenta)
for _, v := range args[1:] {
arg := strings.Split(v, "=")

if len(arg) < 2 {
printer.Println(fmt.Sprintf("%-40s %-100s", "\nIncorrect variable format\n", v), color.FgRed)
continue
}

combinedArgValue := strings.Join(arg[1:], "=")
jobArgs[arg[0]] = combinedArgValue

printer.Println(fmt.Sprintf("%-40s %-100s", arg[0], combinedArgValue), color.Reset)
}
} else {
printer.Println("With No Variables", color.FgRed)
}

scheduledJobID, err := proctorDClient.ScheduleJob(procName, tags, time, notificationEmails, jobArgs)
if err != nil {
printer.Println(err.Error(), color.FgRed)
print()
return
}
printer.Println(fmt.Sprintf("Scheduled Job UUID : %s", scheduledJobID), color.FgGreen)
},
}
}
34 changes: 34 additions & 0 deletions cmd/schedule/create/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package create

import (
"testing"

"github.com/gojektech/proctor/daemon"
"github.com/gojektech/proctor/io"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)

type ScheduleCreateCmdTestSuite struct {
suite.Suite
mockPrinter *io.MockPrinter
mockProctorDClient *daemon.MockClient
testScheduleCreateCmd *cobra.Command
}

func (s *ScheduleCreateCmdTestSuite) SetupTest() {
s.mockPrinter = &io.MockPrinter{}
s.mockProctorDClient = &daemon.MockClient{}
s.testScheduleCreateCmd = NewCmd(s.mockPrinter, s.mockProctorDClient)
}

func (s *ScheduleCreateCmdTestSuite) TestScheduleCreateCmdHelp() {
assert.Equal(s.T(), "Create scheduled jobs", s.testScheduleCreateCmd.Short)
assert.Equal(s.T(), "This command helps to create scheduled jobs", s.testScheduleCreateCmd.Long)
assert.Equal(s.T(), "proctor schedule create run-sample -t '0 2 * * *' -n 'username@mail.com' -T 'sample,proctor' ARG_ONE1=foobar", s.testScheduleCreateCmd.Example)
}

func TestScheduleCreateCmdTestSuite(t *testing.T) {
suite.Run(t, new(ScheduleCreateCmdTestSuite))
}
23 changes: 23 additions & 0 deletions cmd/schedule/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package schedule

import (
"fmt"
"github.com/fatih/color"
"github.com/gojektech/proctor/io"
"github.com/spf13/cobra"
)

func NewCmd(printer io.Printer) *cobra.Command {
return &cobra.Command{
Use: "schedule",
Short: "Schedule proctor jobs",
Long: "This command helps to maange scheduled proctor jobs",
Example: fmt.Sprintf("proctor schedule help"),
Args: cobra.MinimumNArgs(1),

Run: func(cmd *cobra.Command, args []string) {
printer.Println(fmt.Sprintf("Print:"), color.FgRed)
},
}
}

2 changes: 1 addition & 1 deletion cmd/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/spf13/cobra"
)

const ClientVersion = "v0.4.0"
const ClientVersion = "v0.5.0"

func NewCmd(printer io.Printer) *cobra.Command {
return &cobra.Command{
Expand Down
53 changes: 53 additions & 0 deletions daemon/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Client interface {
ExecuteProc(string, map[string]string) (string, error)
StreamProcLogs(string) error
GetDefinitiveProcExecutionStatus(string) (string, error)
ScheduleJob(string, string, string, string, map[string]string) (string, error)
}

type client struct {
Expand All @@ -47,6 +48,15 @@ type ProcToExecute struct {
Args map[string]string `json:"args"`
}

type ScheduleJobPayload struct {
ID string `json:"id"`
Name string `json:"name"`
Tags string `json:"tags"`
Time string `json:"time"`
NotificationEmails string `json:"notification_emails"`
Args map[string]string `json:"args"`
}

func NewClient(printer io.Printer, proctorConfigLoader config.Loader) Client {
return &client{
clientVersion: version.ClientVersion,
Expand All @@ -55,6 +65,49 @@ func NewClient(printer io.Printer, proctorConfigLoader config.Loader) Client {
}
}

func(c *client) ScheduleJob(name, tags, time, notificationEmails string,jobArgs map[string]string) (string, error){
err := c.loadProctorConfig()
if err != nil {
return "", err
}
jobPayload := ScheduleJobPayload{
Name: name,
Tags: tags,
Time: time,
NotificationEmails: notificationEmails,
Args: jobArgs,
}

requestBody, err := json.Marshal(jobPayload)
if err != nil {
return "", err
}

client := &http.Client{}
req, err := http.NewRequest("POST", "http://"+c.proctordHost+"/jobs/schedule", bytes.NewReader(requestBody))
req.Header.Add("Content-Type", "application/json")
req.Header.Add(utility.UserEmailHeaderKey, c.emailId)
req.Header.Add(utility.AccessTokenHeaderKey, c.accessToken)
req.Header.Add(utility.ClientVersionHeaderKey, c.clientVersion)
resp, err := client.Do(req)

if err != nil {
return "", buildNetworkError(err)
}

defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := ioutil.ReadAll(resp.Body)
bodyString := string(body)
return "", errors.New(bodyString)
}

var scheduledJob ScheduleJobPayload
err = json.NewDecoder(resp.Body).Decode(&scheduledJob)

return scheduledJob.ID, err
}

func (c *client) loadProctorConfig() error {
proctorConfig, err := c.proctorConfigLoader.Load()
if err != (config.ConfigError{}) {
Expand Down
5 changes: 5 additions & 0 deletions daemon/client_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ func (m *MockClient) GetDefinitiveProcExecutionStatus(name string) (string, erro
args := m.Called(name)
return args.Get(0).(string), args.Error(1)
}

func (m *MockClient) ScheduleJob(name, tags, time, notificationEmails string,jobArgs map[string]string) (string, error) {
args := m.Called(name, tags, time, notificationEmails, jobArgs)
return args.Get(0).(string), args.Error(1)
}
77 changes: 77 additions & 0 deletions daemon/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,83 @@ func (s *ClientTestSuite) TestExecuteProc() {
s.mockConfigLoader.AssertExpectations(t)
}

func (s *ClientTestSuite) TestSuccessScheduledJob() {
t := s.T()

proctorConfig := config.ProctorConfig{Host: "proctor.example.com", Email: "proctor@example.com", AccessToken: "access-token"}
expectedProcResponse := "8965fce9-5025-43b3-b21c-920c5ff41cd9"
procName := "run-sample"
time := "*/1 * * * *"
notificationEmails := "user@mail.com"
tags := "db,backup"
procArgs := map[string]string{"ARG_ONE": "sample-value"}

body := `{"id":"8965fce9-5025-43b3-b21c-920c5ff41cd9","name":"run-sample","args":{"ARG_ONE":"sample-value"},"notification_emails":"user@mail.com","time":"*/1 * * * *","tags":"db,backup"}`

httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterStubRequest(
httpmock.NewStubRequest(
"POST",
"http://"+proctorConfig.Host+"/jobs/schedule",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(201, body), nil
},
).WithHeader(
&http.Header{
utility.UserEmailHeaderKey: []string{"proctor@example.com"},
utility.AccessTokenHeaderKey: []string{"access-token"},
utility.ClientVersionHeaderKey: []string{version.ClientVersion},
},
),
)

s.mockConfigLoader.On("Load").Return(proctorConfig, config.ConfigError{}).Once()

executeProcResponse, err := s.testClient.ScheduleJob(procName,tags,time,notificationEmails,procArgs)

assert.NoError(t, err)
assert.Equal(t, expectedProcResponse, executeProcResponse)
s.mockConfigLoader.AssertExpectations(t)
}

func (s *ClientTestSuite) TestSchedulingAlreadyExistedScheduledJob() {
t := s.T()

proctorConfig := config.ProctorConfig{Host: "proctor.example.com", Email: "proctor@example.com", AccessToken: "access-token"}
procName := "run-sample"
time := "*/1 * * * *"
notificationEmails := "user@mail.com"
tags := "db,backup"
procArgs := map[string]string{"ARG_ONE": "sample-value"}

httpmock.Activate()
defer httpmock.DeactivateAndReset()

httpmock.RegisterStubRequest(
httpmock.NewStubRequest(
"POST",
"http://"+proctorConfig.Host+"/jobs/schedule",
func(req *http.Request) (*http.Response, error) {
return httpmock.NewStringResponse(409, "provided duplicate combination of job name and args for scheduling"), nil
},
).WithHeader(
&http.Header{
utility.UserEmailHeaderKey: []string{"proctor@example.com"},
utility.AccessTokenHeaderKey: []string{"access-token"},
utility.ClientVersionHeaderKey: []string{version.ClientVersion},
},
),
)

s.mockConfigLoader.On("Load").Return(proctorConfig, config.ConfigError{}).Once()

_, err := s.testClient.ScheduleJob(procName,tags,time,notificationEmails,procArgs)
assert.Equal(t,"provided duplicate combination of job name and args for scheduling", err.Error())
s.mockConfigLoader.AssertExpectations(t)
}

func (s *ClientTestSuite) TestExecuteProcInternalServerError() {
t := s.T()
proctorConfig := config.ProctorConfig{Host: "proctor.example.com", Email: "proctor@example.com", AccessToken: "access-token"}
Expand Down
6 changes: 4 additions & 2 deletions proctord/glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions proctord/glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ import:
- '...'
- package: github.com/robfig/cron
version: v1.1
- package: github.com/badoux/checkmail
21 changes: 21 additions & 0 deletions proctord/jobs/schedule/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package schedule

import (
"encoding/json"
"github.com/badoux/checkmail"
"net/http"
"strings"

"github.com/gojektech/proctor/proctord/jobs/metadata"
"github.com/gojektech/proctor/proctord/logger"
Expand Down Expand Up @@ -51,6 +53,25 @@ func (scheduler *scheduler) Schedule() http.HandlerFunc {
return
}

notificationEmails := strings.Split(scheduledJob.NotificationEmails, ",")

for _, notificationEmail := range notificationEmails {
err = checkmail.ValidateFormat(notificationEmail)
if err != nil {
logger.Error("Client provided invalid email address: ", notificationEmail)
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(utility.InvalidEmailIdClientError))
return
}
}

if scheduledJob.Tags == "" {
logger.Error("Tag(s) are missing")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(utility.InvalidTagError))
return
}

_, err = scheduler.metadataStore.GetJobMetadata(scheduledJob.Name)
if err != nil {
if err.Error() == "redigo: nil returned" {
Expand Down
Loading

0 comments on commit c2a571a

Please sign in to comment.