From 5bd0719f6c3bebe54a76554b50dd6944b0981097 Mon Sep 17 00:00:00 2001 From: "roy.peter" Date: Fri, 21 Sep 2018 17:12:55 +0530 Subject: [PATCH] Introduced authorization in Proctor Resolves #18 1. Constantized all the common error messages printed out to user 2. Refactored tests to use constants 3. Fixes in travis.yml to use updated package names, verbose output for debugging 4. Introduces Email and Access token, checking for same in http headers Email-Id and Access-Token 5. Fixes in error output via websocket, printing new line at end of output so that follow up messages to console don't have to do it 6. Introduces environment config ENVIRONMENT to enable testing of configs 7. HTTP testing library httpmock --- .travis.yml | 3 +- cmd/procs/description/descriptor.go | 9 +- cmd/procs/description/descriptor_test.go | 14 +- cmd/procs/execution/executioner.go | 12 +- cmd/procs/execution/executioner_test.go | 29 +- cmd/procs/list/lister.go | 9 +- cmd/procs/list/lister_test.go | 14 +- config/config.go | 29 +- config/config_test.go | 49 ++-- daemon/client.go | 49 +++- daemon/client_test.go | 355 +++++++++++++++++++++++ glide.lock | 14 +- glide.yaml | 2 + proctord/glide.lock | 4 +- proctord/utility/utils.go | 9 +- 15 files changed, 543 insertions(+), 58 deletions(-) create mode 100644 daemon/client_test.go diff --git a/.travis.yml b/.travis.yml index 973afd75..e29f82d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ go: env: global: + - ENVIRONMENT="test" - PROCTOR_KUBE_CONFIG="out-of-cluster" - PROCTOR_LOG_LEVEL="debug" - PROCTOR_APP_PORT="5000" @@ -54,4 +55,4 @@ jobs: # testing proctor cli - cd ../. - glide install - - go test -race -cover ./cmd/... ./config/... ./io/... ./proc/... ./engine/... . + - go test -race -cover ./cmd/... ./config/... ./io/... ./proc/... ./daemon/... . -v diff --git a/cmd/procs/description/descriptor.go b/cmd/procs/description/descriptor.go index c68bcf85..6c2c18b7 100644 --- a/cmd/procs/description/descriptor.go +++ b/cmd/procs/description/descriptor.go @@ -2,11 +2,13 @@ package description import ( "fmt" + "net/http" "github.com/fatih/color" "github.com/gojektech/proctor/daemon" "github.com/gojektech/proctor/io" "github.com/gojektech/proctor/proc" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" ) @@ -18,7 +20,12 @@ func NewCmd(printer io.Printer, proctorEngineClient daemon.Client) *cobra.Comman Run: func(cmd *cobra.Command, args []string) { procList, err := proctorEngineClient.ListProcs() if err != nil { - printer.Println("Error fetching list of procs. Please check configuration and network connectivity", color.FgRed) + if err.Error() == http.StatusText(http.StatusUnauthorized) { + printer.Println(utility.UnauthorizedError, color.FgRed) + return + } + + printer.Println(utility.GenericDescribeCmdError, color.FgRed) return } diff --git a/cmd/procs/description/descriptor_test.go b/cmd/procs/description/descriptor_test.go index c5a787eb..5baff1b3 100644 --- a/cmd/procs/description/descriptor_test.go +++ b/cmd/procs/description/descriptor_test.go @@ -3,6 +3,7 @@ package description import ( "errors" "fmt" + "net/http" "testing" "github.com/fatih/color" @@ -10,6 +11,7 @@ import ( "github.com/gojektech/proctor/io" "github.com/gojektech/proctor/proc" "github.com/gojektech/proctor/proc/env" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -76,7 +78,7 @@ func (s *DescribeCmdTestSuite) TestDescribeCmdRun() { func (s *DescribeCmdTestSuite) TestDescribeCmdRunProctorEngineClientFailure() { s.mockProctorEngineClient.On("ListProcs").Return([]proc.Metadata{}, errors.New("error")).Once() - s.mockPrinter.On("Println", "Error fetching list of procs. Please check configuration and network connectivity", color.FgRed).Once() + s.mockPrinter.On("Println", utility.GenericDescribeCmdError, color.FgRed).Once() s.testDescribeCmd.Run(&cobra.Command{}, []string{}) @@ -94,6 +96,16 @@ func (s *DescribeCmdTestSuite) TestDescribeCmdRunProcNotSupported() { s.mockPrinter.AssertExpectations(s.T()) } +func (s *DescribeCmdTestSuite) TestDescribeCmdRunProcForUnauthorizedUser() { + s.mockProctorEngineClient.On("ListProcs").Return([]proc.Metadata{}, errors.New(http.StatusText(http.StatusUnauthorized))).Once() + s.mockPrinter.On("Println", utility.UnauthorizedError, color.FgRed).Once() + + s.testDescribeCmd.Run(&cobra.Command{}, []string{"any-proc"}) + + s.mockProctorEngineClient.AssertExpectations(s.T()) + s.mockPrinter.AssertExpectations(s.T()) +} + func TestDescribeCmdTestSuite(t *testing.T) { suite.Run(t, new(DescribeCmdTestSuite)) } diff --git a/cmd/procs/execution/executioner.go b/cmd/procs/execution/executioner.go index 3260defe..549a4525 100644 --- a/cmd/procs/execution/executioner.go +++ b/cmd/procs/execution/executioner.go @@ -2,11 +2,13 @@ package execution import ( "fmt" + "net/http" "strings" "github.com/fatih/color" "github.com/gojektech/proctor/daemon" "github.com/gojektech/proctor/io" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" ) @@ -47,18 +49,22 @@ func NewCmd(printer io.Printer, proctorEngineClient daemon.Client) *cobra.Comman executedProcName, err := proctorEngineClient.ExecuteProc(procName, procArgs) if err != nil { - printer.Println("\nError executing proc. Please check configuration and network connectivity", color.FgRed) + if err.Error() == http.StatusText(http.StatusUnauthorized) { + printer.Println(utility.UnauthorizedError, color.FgRed) + return + } + printer.Println(utility.GenericProcCmdError, color.FgRed) return } printer.Println("Proc execution successful. \nStreaming logs:", color.FgGreen) err = proctorEngineClient.StreamProcLogs(executedProcName) if err != nil { - printer.Println("\nError Streaming Logs", color.FgRed) + printer.Println("Error Streaming Logs", color.FgRed) return } - printer.Println("\nLog stream of proc completed.", color.FgGreen) + printer.Println("Log stream of proc completed.", color.FgGreen) }, } } diff --git a/cmd/procs/execution/executioner_test.go b/cmd/procs/execution/executioner_test.go index b66e8a19..3666be99 100644 --- a/cmd/procs/execution/executioner_test.go +++ b/cmd/procs/execution/executioner_test.go @@ -3,11 +3,13 @@ package execution import ( "errors" "fmt" + "net/http" "testing" "github.com/fatih/color" "github.com/gojektech/proctor/daemon" "github.com/gojektech/proctor/io" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -51,7 +53,7 @@ func (s *ExecutionCmdTestSuite) TestExecutionCmd() { s.mockPrinter.On("Println", "Proc execution successful. \nStreaming logs:", color.FgGreen).Once() s.mockProctorEngineClient.On("StreamProcLogs", "executed-proc-name").Return(nil).Once() - s.mockPrinter.On("Println", "\nLog stream of proc completed.", color.FgGreen).Once() + s.mockPrinter.On("Println", "Log stream of proc completed.", color.FgGreen).Once() s.testExecutionCmd.Run(&cobra.Command{}, args) @@ -79,7 +81,7 @@ func (s *ExecutionCmdTestSuite) TestExecutionCmdForNoProcVariables() { s.mockPrinter.On("Println", "Proc execution successful. \nStreaming logs:", color.FgGreen).Once() s.mockProctorEngineClient.On("StreamProcLogs", "executed-proc-name").Return(nil).Once() - s.mockPrinter.On("Println", "\nLog stream of proc completed.", color.FgGreen).Once() + s.mockPrinter.On("Println", "Log stream of proc completed.", color.FgGreen).Once() s.testExecutionCmd.Run(&cobra.Command{}, args) @@ -100,7 +102,7 @@ func (s *ExecutionCmdTestSuite) TestExecutionCmdForIncorrectVariableFormat() { s.mockPrinter.On("Println", "Proc execution successful. \nStreaming logs:", color.FgGreen).Once() s.mockProctorEngineClient.On("StreamProcLogs", "executed-proc-name").Return(nil).Once() - s.mockPrinter.On("Println", "\nLog stream of proc completed.", color.FgGreen).Once() + s.mockPrinter.On("Println", "Log stream of proc completed.", color.FgGreen).Once() s.testExecutionCmd.Run(&cobra.Command{}, args) @@ -117,7 +119,24 @@ func (s *ExecutionCmdTestSuite) TestExecutionCmdForProctorEngineExecutionFailure procArgs := make(map[string]string) s.mockProctorEngineClient.On("ExecuteProc", "say-hello-world", procArgs).Return("", errors.New("error")).Once() - s.mockPrinter.On("Println", "\nError executing proc. Please check configuration and network connectivity", color.FgRed).Once() + s.mockPrinter.On("Println", utility.GenericProcCmdError, color.FgRed).Once() + + s.testExecutionCmd.Run(&cobra.Command{}, args) + + s.mockProctorEngineClient.AssertExpectations(s.T()) + s.mockPrinter.AssertExpectations(s.T()) +} + +func (s *ExecutionCmdTestSuite) TestExecutionCmdForProctorEngineExecutionForUnauthorizedUser() { + args := []string{"say-hello-world"} + + s.mockPrinter.On("Println", fmt.Sprintf("%-40s %-100s", "Executing Proc", "say-hello-world"), color.Reset).Once() + s.mockPrinter.On("Println", "With No Variables", color.FgRed).Once() + + procArgs := make(map[string]string) + s.mockProctorEngineClient.On("ExecuteProc", "say-hello-world", procArgs).Return("", errors.New(http.StatusText(http.StatusUnauthorized))).Once() + + s.mockPrinter.On("Println", utility.UnauthorizedError, color.FgRed).Once() s.testExecutionCmd.Run(&cobra.Command{}, args) @@ -137,7 +156,7 @@ func (s *ExecutionCmdTestSuite) TestExecutionCmdForProctorEngineLogStreamingFail s.mockPrinter.On("Println", "Proc execution successful. \nStreaming logs:", color.FgGreen).Once() s.mockProctorEngineClient.On("StreamProcLogs", "executed-proc-name").Return(errors.New("error")).Once() - s.mockPrinter.On("Println", "\nError Streaming Logs", color.FgRed).Once() + s.mockPrinter.On("Println", "Error Streaming Logs", color.FgRed).Once() s.testExecutionCmd.Run(&cobra.Command{}, args) diff --git a/cmd/procs/list/lister.go b/cmd/procs/list/lister.go index 2b8fff6d..54be00bc 100644 --- a/cmd/procs/list/lister.go +++ b/cmd/procs/list/lister.go @@ -2,10 +2,12 @@ package list import ( "fmt" + "net/http" "github.com/fatih/color" "github.com/gojektech/proctor/daemon" "github.com/gojektech/proctor/io" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" ) @@ -17,7 +19,12 @@ func NewCmd(printer io.Printer, proctorEngineClient daemon.Client) *cobra.Comman Run: func(cmd *cobra.Command, args []string) { procList, err := proctorEngineClient.ListProcs() if err != nil { - printer.Println("Error fetching list of procs. Please check configuration and network connectivity", color.FgRed) + if err.Error() == http.StatusText(http.StatusUnauthorized) { + printer.Println(utility.UnauthorizedError, color.FgRed) + return + } + + printer.Println(utility.GenericListCmdError, color.FgRed) return } diff --git a/cmd/procs/list/lister_test.go b/cmd/procs/list/lister_test.go index e3b513f4..74f0e6ce 100644 --- a/cmd/procs/list/lister_test.go +++ b/cmd/procs/list/lister_test.go @@ -3,12 +3,14 @@ package list import ( "errors" "fmt" + "net/http" "testing" "github.com/fatih/color" "github.com/gojektech/proctor/daemon" "github.com/gojektech/proctor/io" "github.com/gojektech/proctor/proc" + "github.com/gojektech/proctor/proctord/utility" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -62,7 +64,17 @@ func (s *ListCmdTestSuite) TestListCmdRun() { func (s *ListCmdTestSuite) TestListCmdRunProctorEngineClientFailure() { s.mockProctorEngineClient.On("ListProcs").Return([]proc.Metadata{}, errors.New("error")).Once() - s.mockPrinter.On("Println", "Error fetching list of procs. Please check configuration and network connectivity", color.FgRed).Once() + s.mockPrinter.On("Println", utility.GenericListCmdError, color.FgRed).Once() + + s.testListCmd.Run(&cobra.Command{}, []string{}) + + s.mockProctorEngineClient.AssertExpectations(s.T()) + s.mockPrinter.AssertExpectations(s.T()) +} + +func (s *ListCmdTestSuite) TestListCmdRunProctorEngineClientForUnauthorizedUser() { + s.mockProctorEngineClient.On("ListProcs").Return([]proc.Metadata{}, errors.New(http.StatusText(http.StatusUnauthorized))).Once() + s.mockPrinter.On("Println", utility.UnauthorizedError, color.FgRed).Once() s.testListCmd.Run(&cobra.Command{}, []string{}) diff --git a/config/config.go b/config/config.go index 958f4c05..3eb12930 100644 --- a/config/config.go +++ b/config/config.go @@ -9,15 +9,20 @@ import ( func InitConfig() { viper.SetConfigType("yaml") + viper.AutomaticEnv() + var configFileDir string - home := "$HOME/.proctor" - viper.AddConfigPath(home) + if viper.GetString("ENVIRONMENT") == "test" { + configFileDir = "/tmp" + } else { + configFileDir = "$HOME/.proctor" + } + viper.AddConfigPath(configFileDir) viper.SetConfigName("proctor") - viper.AutomaticEnv() - err := viper.ReadInConfig() + if err != nil { fmt.Println("Error reading proctor config") os.Exit(1) @@ -27,9 +32,17 @@ func InitConfig() { func ProctorURL() string { InitConfig() proctorUrl := viper.GetString("PROCTOR_URL") - if len(proctorUrl) == 0 { - fmt.Println("proctor url not configured") - os.Exit(1) - } return proctorUrl } + +func EmailId() string { + InitConfig() + emailId := viper.GetString("EMAIL_ID") + return emailId +} + +func AccessToken() string { + InitConfig() + accessToken := viper.GetString("ACCESS_TOKEN") + return accessToken +} diff --git a/config/config_test.go b/config/config_test.go index ec128435..4f02cd1e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,34 +10,41 @@ import ( ) func TestProctorURL(t *testing.T) { - proctorConfigFileExistedBeforeTest := true - - home := os.Getenv("HOME") - proctorConfigDir := home + "/.proctor" - proctorConfigFilePath := proctorConfigDir + "/proctor.yaml" - existingConfigFileData, err := ioutil.ReadFile(proctorConfigFilePath) - if err != nil { - proctorConfigFileExistedBeforeTest = false - os.Mkdir(proctorConfigDir, os.ModePerm) - } - + proctorConfigFilePath := "/tmp/proctor.yaml" proctorUrl := []byte("PROCTOR_URL: any-random-url.com") - err = ioutil.WriteFile(proctorConfigFilePath, proctorUrl, 0644) + err := ioutil.WriteFile(proctorConfigFilePath, proctorUrl, 0644) + defer os.Remove(proctorConfigFilePath) assert.NoError(t, err) config.InitConfig() configuredProctorURL := config.ProctorURL() assert.Equal(t, "any-random-url.com", configuredProctorURL) +} + +func TestProctorEmailId(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + EmailId := []byte("EMAIL_ID: foobar@gmail.com") + err := ioutil.WriteFile(proctorConfigFilePath, EmailId, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + + config.InitConfig() + configuredEmailId := config.EmailId() - if proctorConfigFileExistedBeforeTest { - err = ioutil.WriteFile(proctorConfigFilePath, existingConfigFileData, 0644) - assert.NoError(t, err) - } else { - err = os.Remove(proctorConfigFilePath) - assert.NoError(t, err) + assert.Equal(t, "foobar@gmail.com", configuredEmailId) +} + +func TestProctorAccessToken(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + + AccessToken := []byte("ACCESS_TOKEN: access-token") + err := ioutil.WriteFile(proctorConfigFilePath, AccessToken, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + + config.InitConfig() + configuredAccessToken := config.AccessToken() - err = os.Remove(proctorConfigDir) - assert.NoError(t, err) - } + assert.Equal(t, "access-token", configuredAccessToken) } diff --git a/daemon/client.go b/daemon/client.go index d083a44e..7af1ad56 100644 --- a/daemon/client.go +++ b/daemon/client.go @@ -3,6 +3,7 @@ package daemon import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -14,6 +15,7 @@ import ( "github.com/fatih/color" "github.com/gojektech/proctor/config" "github.com/gojektech/proctor/proc" + "github.com/gojektech/proctor/proctord/utility" "github.com/gorilla/websocket" ) @@ -25,6 +27,8 @@ type Client interface { type client struct { proctorEngineURL string + emailId string + accessToken string } type ProcToExecute struct { @@ -35,15 +39,26 @@ type ProcToExecute struct { func NewClient() Client { return &client{ proctorEngineURL: config.ProctorURL(), + emailId: config.EmailId(), + accessToken: config.AccessToken(), } } func (c *client) ListProcs() ([]proc.Metadata, error) { - resp, err := http.Get("http://" + c.proctorEngineURL + "/jobs/metadata") - if err != nil || resp.StatusCode != http.StatusOK { - return []proc.Metadata{}, err + client := &http.Client{} + req, err := http.NewRequest("GET", "http://"+c.proctorEngineURL+"/jobs/metadata", nil) + req.Header.Add(utility.UserEmailHeaderKey, c.emailId) + req.Header.Add(utility.AccessTokenHeaderKey, c.accessToken) + resp, err := client.Do(req) + + if err != nil { + return []proc.Metadata{}, errors.New(err.Error()) } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return []proc.Metadata{}, errors.New(http.StatusText(resp.StatusCode)) + } var procList []proc.Metadata err = json.NewDecoder(resp.Body).Decode(&procList) @@ -61,12 +76,22 @@ func (c *client) ExecuteProc(name string, args map[string]string) (string, error return "", err } - resp, err := http.Post("http://"+c.proctorEngineURL+"/jobs/execute", "application/json", bytes.NewReader(requestBody)) - if err != nil || resp.StatusCode != http.StatusCreated { - return "", err + client := &http.Client{} + req, err := http.NewRequest("POST", "http://"+c.proctorEngineURL+"/jobs/execute", bytes.NewReader(requestBody)) + req.Header.Add("Content-Type", "application/json") + req.Header.Add(utility.UserEmailHeaderKey, c.emailId) + req.Header.Add(utility.AccessTokenHeaderKey, c.accessToken) + resp, err := client.Do(req) + + if err != nil { + return "", errors.New(err.Error()) } defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + return "", errors.New(http.StatusText(resp.StatusCode)) + } + var executedProc ProcToExecute err = json.NewDecoder(resp.Body).Decode(&executedProc) @@ -84,9 +109,18 @@ func (c *client) StreamProcLogs(name string) error { proctorEngineWebsocketURL := url.URL{Scheme: "ws", Host: c.proctorEngineURL, Path: "/jobs/logs"} proctorEngineWebsocketURLWithProcName := proctorEngineWebsocketURL.String() + "?" + "job_name=" + name - wsConn, _, err := websocket.DefaultDialer.Dial(proctorEngineWebsocketURLWithProcName, nil) + headers := make(map[string][]string) + token := []string{c.accessToken} + emailId := []string{c.emailId} + headers[utility.AccessTokenHeaderKey] = token + headers[utility.UserEmailHeaderKey] = emailId + + wsConn, response, err := websocket.DefaultDialer.Dial(proctorEngineWebsocketURLWithProcName, headers) if err != nil { animation.Stop() + if response.StatusCode == http.StatusUnauthorized { + return errors.New(http.StatusText(http.StatusUnauthorized)) + } return err } defer wsConn.Close() @@ -97,6 +131,7 @@ func (c *client) StreamProcLogs(name string) error { _, message, err := wsConn.ReadMessage() animation.Stop() if err != nil { + fmt.Println() logStreaming <- 0 return } diff --git a/daemon/client_test.go b/daemon/client_test.go new file mode 100644 index 00000000..366658d5 --- /dev/null +++ b/daemon/client_test.go @@ -0,0 +1,355 @@ +package daemon + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gojektech/proctor/config" + "github.com/gorilla/websocket" + "github.com/thingful/httpmock" + + "github.com/gojektech/proctor/proc/env" + + "github.com/gojektech/proctor/proc" + "github.com/gojektech/proctor/proctord/utility" + "github.com/stretchr/testify/assert" +) + +func TestListProcs(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + fmt.Println(err) + + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + body := `[ { "name": "job-1", "description": "job description", "image_name": "hub.docker.com/job-1:latest", "env_vars": { "secrets": [ { "name": "SECRET1", "description": "Base64 encoded secret for authentication." } ], "args": [ { "name": "ARG1", "description": "Argument name" } ] } } ]` + var args = []env.VarMetadata{env.VarMetadata{Name: "ARG1", Description: "Argument name"}} + var secrets = []env.VarMetadata{env.VarMetadata{Name: "SECRET1", Description: "Base64 encoded secret for authentication."}} + envVars := env.Vars{Secrets: secrets, Args: args} + var procListExpected = []proc.Metadata{proc.Metadata{Name: "job-1", Description: "job description", EnvVars: envVars}} + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "GET", + "http://"+config.ProctorURL()+"/jobs/metadata", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(200, body), nil + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + procList, err := proctorClient.ListProcs() + + assert.NoError(t, err) + assert.Equal(t, procListExpected, procList) +} + +func TestListProcsReturnInternalServerError(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + var procListExpected = []proc.Metadata{} + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "GET", + "http://"+config.ProctorURL()+"/jobs/metadata", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(500, `{}`), nil + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + procList, err := proctorClient.ListProcs() + + assert.Equal(t, procListExpected, procList) + assert.Error(t, err) +} + +func TestListProcsReturnClientSideConnectionError(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + connectionTimeOut := "Connection TimeOut" + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + var procListExpected = []proc.Metadata{} + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "GET", + "http://"+config.ProctorURL()+"/jobs/metadata", + func(req *http.Request) (*http.Response, error) { + return nil, errors.New(connectionTimeOut) + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + procList, err := proctorClient.ListProcs() + + assert.Equal(t, errors.New("Get http://proctor.example.com/jobs/metadata: Connection TimeOut"), err) + assert.Equal(t, procListExpected, procList) +} + +func TestListProcsForUnauthorizedUser(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + var procListExpected = []proc.Metadata{} + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "GET", + "http://"+config.ProctorURL()+"/jobs/metadata", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(401, `{}`), nil + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + procList, err := proctorClient.ListProcs() + + assert.Equal(t, procListExpected, procList) + assert.Equal(t, err.Error(), http.StatusText(http.StatusUnauthorized)) +} + +func TestExecuteProc(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + expectedProcResponse := "proctor-777b1dfb-ea27-46d9-b02c-839b75a542e2" + body := `{ "name": "proctor-777b1dfb-ea27-46d9-b02c-839b75a542e2"}` + procName := "run-sample" + procArgs := map[string]string{"SAMPLE_ARG1": "sample-value"} + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "POST", + "http://"+config.ProctorURL()+"/jobs/execute", + 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"}, + }, + ), + ) + + executeProcResponse, err := proctorClient.ExecuteProc(procName, procArgs) + + assert.NoError(t, err) + assert.Equal(t, expectedProcResponse, executeProcResponse) +} + +func TestExecuteProcInternalServerError(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + expectedProcResponse := "" + procName := "run-sample" + procArgs := map[string]string{"SAMPLE_ARG1": "sample-value"} + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "POST", + "http://"+config.ProctorURL()+"/jobs/execute", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(500, ""), nil + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + executeProcResponse, err := proctorClient.ExecuteProc(procName, procArgs) + + assert.Error(t, err) + assert.Equal(t, expectedProcResponse, executeProcResponse) +} + +func TestExecuteProcUnAuthorized(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + proctorConfig := []byte("PROCTOR_URL: proctor.example.com\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com") + expectedProcResponse := "" + procName := "run-sample" + procArgs := map[string]string{"SAMPLE_ARG1": "sample-value"} + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + httpmock.RegisterStubRequest( + httpmock.NewStubRequest( + "POST", + "http://"+config.ProctorURL()+"/jobs/execute", + func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(401, ""), nil + }, + ).WithHeader( + &http.Header{ + utility.UserEmailHeaderKey: []string{"proctor@example.com"}, + utility.AccessTokenHeaderKey: []string{"access-token"}, + }, + ), + ) + + executeProcResponse, err := proctorClient.ExecuteProc(procName, procArgs) + + assert.Equal(t, expectedProcResponse, executeProcResponse) + assert.Error(t, errors.New(http.StatusText(http.StatusUnauthorized)), err) +} + +func TestLogStreamForAuthorizedUser(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + logStreamAuthorizer := func(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{} + assert.Equal(t, "proctor@example.com", r.Header.Get(utility.UserEmailHeaderKey)) + assert.Equal(t, "access-token", r.Header.Get(utility.AccessTokenHeaderKey)) + conn, _ := upgrader.Upgrade(w, r, nil) + defer conn.Close() + } + } + testServer := httptest.NewServer(logStreamAuthorizer(t)) + defer testServer.Close() + + proctorConfig := []byte(fmt.Sprintf("PROCTOR_URL: %s\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com", makeHostname(testServer.URL))) + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + err = proctorClient.StreamProcLogs("test-job-id") + assert.NoError(t, err) +} + +func TestLogStreamForBadWebSocketHandshake(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + badWebSocketHandshakeHandler := func() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) {} + } + testServer := httptest.NewServer(badWebSocketHandshakeHandler()) + defer testServer.Close() + + proctorConfig := []byte(fmt.Sprintf("PROCTOR_URL: %s\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com", makeHostname(testServer.URL))) + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + errStreamLogs := proctorClient.StreamProcLogs("test-job-id") + assert.Equal(t, errors.New("websocket: bad handshake"), errStreamLogs) +} + +func TestLogStreamForUnauthorizedUser(t *testing.T) { + proctorConfigFilePath := "/tmp/proctor.yaml" + unauthorizedUserHandler := func() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + } + } + testServer := httptest.NewServer(unauthorizedUserHandler()) + defer testServer.Close() + + proctorConfig := []byte(fmt.Sprintf("PROCTOR_URL: %s\nACCESS_TOKEN: access-token\nEMAIL_ID: proctor@example.com", makeHostname(testServer.URL))) + err := ioutil.WriteFile(proctorConfigFilePath, proctorConfig, 0644) + defer os.Remove(proctorConfigFilePath) + assert.NoError(t, err) + config.InitConfig() + + proctorClient := NewClient() + errStreamLogs := proctorClient.StreamProcLogs("test-job-id") + assert.Error(t, errors.New(http.StatusText(http.StatusUnauthorized)), errStreamLogs) +} + +func makeHostname(s string) string { + return strings.TrimPrefix(s, "http://") +} diff --git a/glide.lock b/glide.lock index 9748ecf6..f27b5a6f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 88088db8f848869631297a4b699eabc19b658462bd3f918e19cfd1e3b622bfc3 -updated: 2018-03-20T12:30:54.642221+05:30 +hash: 99d5240e2827968d8fea8861618d19371699d856ce4634572dfab614345e80c7 +updated: 2018-09-28T15:53:00.690113+05:30 imports: - name: cloud.google.com/go version: 3b1ae45394a234c385be014e9a488f2bb6eef821 @@ -139,18 +139,20 @@ imports: - name: github.com/spf13/jwalterweatherman version: 12bd96e66386c1960ab0f74ced1362f66f552f7b - name: github.com/spf13/pflag - version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 + version: 9a97c102cda95a86cec2345a6f09f55a939babf5 - name: github.com/spf13/viper - version: b5e8006cbee93ec955a89ab31e0e3ce3204f3736 + version: a1b837276271029e31f796ae5d03ba9ffb017244 - name: github.com/stretchr/objx version: 8a3f7159479fbc75b30357fbc48f380b7320f08e - name: github.com/stretchr/testify - version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 + version: f35b8ab0b5a2cef36673838d662e249dd9c94686 subpackages: - assert - mock - require - suite +- name: github.com/thingful/httpmock + version: 4df9ac03f1f75f097c31dde17e969d212d3fb479 - name: github.com/tylerb/graceful version: 4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb - name: github.com/ugorji/go @@ -364,6 +366,8 @@ imports: - util/integer - util/jsonpath testImports: +- name: github.com/goware/urlx + version: 86bdc24560383254e8b977da31a823eddf904409 - name: github.com/jarcoal/httpmock version: 4442edb3db31196622da56482fd8d0fa375fba4d - name: github.com/pkg/errors diff --git a/glide.yaml b/glide.yaml index 32d92ea1..b93d0b40 100644 --- a/glide.yaml +++ b/glide.yaml @@ -14,3 +14,5 @@ import: version: "1.0" - package: github.com/spf13/cobra version: v0.0.1 +- package: github.com/thingful/httpmock + version: 0.0.2 diff --git a/proctord/glide.lock b/proctord/glide.lock index 53250433..5b9f25e2 100644 --- a/proctord/glide.lock +++ b/proctord/glide.lock @@ -1,5 +1,5 @@ -hash: 2746d89ca9aeaafd75f5c43f1ad15bd17c1c21d86432f3aaae854036e68ca138 -updated: 2018-09-27T10:39:34.763901+05:30 +hash: 3b9817f67f48cc115816769de3b02d8892a1b8a4342af83c43c1a6a0d3e91417 +updated: 2018-09-28T11:03:06.991749+05:30 imports: - name: cloud.google.com/go version: 3b1ae45394a234c385be014e9a488f2bb6eef821 diff --git a/proctord/utility/utils.go b/proctord/utility/utils.go index 98cf407d..9d3eec3e 100644 --- a/proctord/utility/utils.go +++ b/proctord/utility/utils.go @@ -1,9 +1,11 @@ package utility -const UserEmailHeaderKey = "Email-Id" - const ClientError = "malformed request" const ServerError = "Something went wrong" +const UnauthorizedError = "Unauthorized. Please check the email id and access token" +const GenericListCmdError = "Error fetching list of procs. Please check configuration and network connectivity" +const GenericProcCmdError = "Error executing proc. Please check configuration and network connectivity" +const GenericDescribeCmdError = "Error fetching description of proc. Please check configuration and network connectivity" const JobSubmissionSuccess = "success" const JobSubmissionClientError = "client_error" @@ -20,6 +22,9 @@ const ImageNameContextKey = "image_name" const JobNameSubmittedForExecutionContextKey = "job_name_submitted_for_execution" const JobSubmissionStatusContextKey = "job_sumission_status" +const UserEmailHeaderKey = "Email-Id" +const AccessTokenHeaderKey = "Access-Token" + func MergeMaps(mapOne, mapTwo map[string]string) map[string]string { result := make(map[string]string)