From 9ec50e80a37b88f26807fd32c7922a4527b13c1d Mon Sep 17 00:00:00 2001 From: Zhao Xiaojie Date: Wed, 2 Oct 2019 16:16:57 +0800 Subject: [PATCH] Add support to input pipeline job (#164) * Add support to input pipeline job * Try to fix the 400 error * Add support get input actions * Add test cases * Remove the new badges put them instead of another pr * Add the action as a argument --- app/cmd/job_input.go | 118 ++++++++++++++++++++++++++++++++++ app/cmd/job_input_test.go | 132 ++++++++++++++++++++++++++++++++++++++ client/job.go | 52 +++++++++++++++ client/job_test.go | 18 ++++++ client/job_test_common.go | 47 ++++++++++++++ client/test_methods.go | 4 +- go.mod | 1 + 7 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 app/cmd/job_input.go create mode 100644 app/cmd/job_input_test.go create mode 100644 client/job_test_common.go diff --git a/app/cmd/job_input.go b/app/cmd/job_input.go new file mode 100644 index 0000000..15b0d3d --- /dev/null +++ b/app/cmd/job_input.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "fmt" + "log" + "net/http" + "strconv" + "encoding/json" + + "github.com/AlecAivazis/survey/v2" + "github.com/jenkins-zh/jenkins-cli/client" + "github.com/spf13/cobra" + "github.com/AlecAivazis/survey/v2/terminal" +) + +// JobInputOption is the job delete option +type JobInputOption struct { + BatchOption + + Action string + + RoundTripper http.RoundTripper + Stdio terminal.Stdio +} + +var jobInputOption JobInputOption + +func init() { + jobCmd.AddCommand(jobInputCmd) + jobInputCmd.Flags().StringVarP(&jobInputOption.Action, "action", "", "", "The action wether you want to process or abort.") +} + +var jobInputCmd = &cobra.Command{ + Use: "input [buildID]", + Short: "Input a job in your Jenkins", + Long: `Input a job in your Jenkins`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Help() + return + } + + jobName := args[0] + buildID := -1 + + if len(args) >= 2 { + var err error + if buildID, err = strconv.Atoi(args[1]); err != nil { + cmd.PrintErrln(err) + } + } + + jclient := &client.JobClient{ + JenkinsCore: client.JenkinsCore{ + RoundTripper: jobInputOption.RoundTripper, + Debug: rootOptions.Debug, + }, + } + getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) + + if inputActions, err := jclient.GetJobInputActions(jobName, buildID); err != nil { + log.Fatal(err) + } else if len(inputActions) >= 1 { + inputAction := inputActions[0] + params := make(map[string]string) + + if len(inputAction.Inputs) > 0 { + inputsJSON, _ := json.MarshalIndent(inputAction.Inputs, "", " ") + content := string(inputsJSON) + + prompt := &survey.Editor{ + Message: "Edit your pipeline input parameters", + FileName: "*.json", + Default: content, + HideDefault: true, + AppendDefault: true, + } + + if err = survey.AskOne(prompt, &content); err != nil { + log.Fatal(err) + } + + if err = json.Unmarshal([]byte(content), &(inputAction.Inputs)); err != nil { + log.Fatal(err) + } + + for _, input := range inputAction.Inputs { + params[input.Name] = input.Value + } + } + + render := &survey.Renderer{} + render.WithStdio(jobInputOption.Stdio) + + // allow users make their choice through cli arguments + action := jobInputOption.Action + if action == "" { + prompt := &survey.Input{ + Renderer: *render, + Message: fmt.Sprintf("Are you going to process or abort this input: %s?", inputAction.Message), + } + survey.AskOne(prompt, &action) + } + + if action == "process" { + err = jclient.JobInputSubmit(jobName, inputAction.ID, buildID, false, params) + } else if action == "abort" { + err = jclient.JobInputSubmit(jobName, inputAction.ID, buildID, true, params) + } else { + cmd.PrintErrln("Only process or abort is accepted!") + } + + if err != nil { + cmd.PrintErrln(err) + } + } + }, +} diff --git a/app/cmd/job_input_test.go b/app/cmd/job_input_test.go new file mode 100644 index 0000000..d2302e7 --- /dev/null +++ b/app/cmd/job_input_test.go @@ -0,0 +1,132 @@ +package cmd + +import ( + "bytes" + "io/ioutil" + "os" + "fmt" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" + "github.com/jenkins-zh/jenkins-cli/client" + // "github.com/AlecAivazis/survey/v2/core" + // "github.com/AlecAivazis/survey/v2/terminal" +) + +var _ = Describe("job input command", func() { + var ( + ctrl *gomock.Controller + roundTripper *mhttp.MockRoundTripper + jenkinsRoot string + username string + token string + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + roundTripper = mhttp.NewMockRoundTripper(ctrl) + jobInputOption.RoundTripper = roundTripper + rootCmd.SetArgs([]string{}) + rootOptions.Jenkins = "" + rootOptions.ConfigFile = "test.yaml" + + jenkinsRoot = "http://localhost:8080/jenkins" + username = "admin" + token = "111e3a2f0231198855dceaff96f20540a9" + }) + + AfterEach(func() { + rootCmd.SetArgs([]string{}) + os.Remove(rootOptions.ConfigFile) + rootOptions.ConfigFile = "" + ctrl.Finish() + }) + + Context("basic cases", func() { + It("no params, will error",func(){ + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + rootCmd.SetArgs([]string{"job", "input"}) + + jobInputCmd.SetHelpFunc(func(cmd *cobra.Command, _ []string) { + cmd.Print("help") + }) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + + Expect(buf.String()).To(Equal("help")) + }) + + It("should success, abort without inputs", func() { + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + jobName := "test" + buildID := 1 + + client.PrepareForGetJobInputActions(roundTripper, jenkinsRoot, username, token, jobName, buildID) + client.PrepareForSubmitInput(roundTripper, jenkinsRoot, fmt.Sprintf("/job/%s", jobName) , username, token) + + // no idea how to let it works, just leave this here + // _, w, err := os.Pipe() + + // c, err := expect.NewConsole(expect.WithStdout(w)) + // Expect(err).To(BeNil()) + // jobInputOption.Stdio = terminal.Stdio{ + // In:c.Tty(), + // Out:c.Tty(), + // Err:c.Tty(), + // } + // defer c.Close() + + // go func() { + // c.ExpectString("Are you going to process or abort this input: message?") + // c.SendLine("abort\n") + // c.ExpectEOF() + // }() + + rootCmd.SetArgs([]string{"job", "input", jobName, "1", "--action", "abort"}) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + + Expect(buf.String()).To(Equal("")) + }) + + It("should success, process without inputs", func() { + data, err := generateSampleConfig() + Expect(err).To(BeNil()) + err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) + Expect(err).To(BeNil()) + + jobName := "test" + buildID := 1 + + client.PrepareForGetJobInputActions(roundTripper, jenkinsRoot, username, token, jobName, buildID) + client.PrepareForSubmitProcessInput(roundTripper, jenkinsRoot, fmt.Sprintf("/job/%s", jobName) , username, token) + + rootCmd.SetArgs([]string{"job", "input", jobName, "1", "--action", "process"}) + + buf := new(bytes.Buffer) + rootCmd.SetOutput(buf) + _, err = rootCmd.ExecuteC() + Expect(err).To(BeNil()) + + Expect(buf.String()).To(Equal("")) + }) + }) +}) diff --git a/client/job.go b/client/job.go index 7d946fc..f33afd0 100644 --- a/client/job.go +++ b/client/job.go @@ -297,6 +297,47 @@ func (q *JobClient) Delete(jobName string) (err error) { return } +// GetJobInputActions returns the all pending actions +func (q *JobClient) GetJobInputActions(jobName string, buildID int) (actions []JobInputItem, err error) { + path := parseJobPath(jobName) + err = q.RequestWithData("GET", fmt.Sprintf("%s/%d/wfapi/pendingInputActions", path, buildID), nil, nil, 200, &actions) + return +} + +// JenkinsInputParametersRequest represents the parameters for the Jenkins input request +type JenkinsInputParametersRequest struct { + Parameter []ParameterDefinition `json:"parameter"` +} + +// JobInputSubmit submit the pending input request +func (q *JobClient) JobInputSubmit(jobName, inputID string, buildID int, abort bool, params map[string]string) (err error) { + jobPath := parseJobPath(jobName) + var api string + if abort { + api = fmt.Sprintf("%s/%d/input/%s/abort", jobPath, buildID, inputID) + } else { + api = fmt.Sprintf("%s/%d/input/%s/proceed", jobPath, buildID, inputID) + } + + request := JenkinsInputParametersRequest{ + Parameter: make([]ParameterDefinition, 0), + } + + for k, v := range params { + request.Parameter = append(request.Parameter, ParameterDefinition{ + Name: k, + Value: v, + }) + } + + paramData, _ := json.Marshal(request) + + api = fmt.Sprintf("%s?json=%s", api, string(paramData)) + _, err = q.RequestWithoutData("POST", api, nil, nil, 200) + + return +} + // parseJobPath leads with slash func parseJobPath(jobName string) (path string) { jobItems := strings.Split(jobName, " ") @@ -405,3 +446,14 @@ type JobCategoryItem struct { Order int Class string } + +// JobInputItem represents a job input action +type JobInputItem struct { + ID string + AbortURL string + Message string + ProceedText string + ProceedURL string + RedirectApprovalURL string + Inputs []ParameterDefinition +} diff --git a/client/job_test.go b/client/job_test.go index b903a95..f42ba11 100644 --- a/client/job_test.go +++ b/client/job_test.go @@ -305,4 +305,22 @@ var _ = Describe("job test", func() { Expect(err).To(BeNil()) }) }) + + Context("GetJobInputActions", func() { + It("simple case, should success", func() { + PrepareForGetJobInputActions(roundTripper, jobClient.URL, "", "", "jobName", 1) + actions, err := jobClient.GetJobInputActions("jobName", 1) + Expect(err).To(BeNil()) + Expect(len(actions)).To(Equal(1)) + Expect(actions[0].Message).To(Equal("message")) + }) + }) + + Context("JobInputSubmit", func() { + It("simple case, should success", func() { + PrepareForSubmitInput(roundTripper, jobClient.URL, "/job/jobName", "", "") + err := jobClient.JobInputSubmit("jobName", "Eff7d5dba32b4da32d9a67a519434d3f", 1, true, nil) + Expect(err).To(BeNil()) + }) + }) }) diff --git a/client/job_test_common.go b/client/job_test_common.go new file mode 100644 index 0000000..624310c --- /dev/null +++ b/client/job_test_common.go @@ -0,0 +1,47 @@ +package client + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" +) + +// PrepareForGetJobInputActions only for test +func PrepareForGetJobInputActions(roundTripper *mhttp.MockRoundTripper, rootURL, user, passwd, jobName string, buildID int) ( + request *http.Request, response *http.Response) { + request, _ = http.NewRequest("GET", fmt.Sprintf("%s/job/%s/%d/wfapi/pendingInputActions", rootURL, jobName, buildID), nil) + response = &http.Response{ + StatusCode: 200, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(` +[{"id":"Eff7d5dba32b4da32d9a67a519434d3f","proceedText":"继续","message":"message","inputs":[], +"proceedUrl":"/job/test/5/wfapi/inputSubmit?inputId=Eff7d5dba32b4da32d9a67a519434d3f", +"abortUrl":"/job/test/5/input/Eff7d5dba32b4da32d9a67a519434d3f/abort","redirectApprovalUrl":"/job/test/5/input/"}]`)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) + + if user != "" && passwd != "" { + request.SetBasicAuth(user, passwd) + } + return +} + +// PrepareForSubmitInput only for test +func PrepareForSubmitInput(roundTripper *mhttp.MockRoundTripper, rootURL, jobPath, user, passwd string) ( + request *http.Request, response *http.Response) { + request, _ = http.NewRequest("POST", fmt.Sprintf("%s%s/%d/input/%s/abort", rootURL, jobPath, 1, "Eff7d5dba32b4da32d9a67a519434d3f"), nil) + PrepareCommonPost(request, roundTripper, user, passwd, rootURL) + return +} + +// PrepareForSubmitProcessInput only for test +func PrepareForSubmitProcessInput(roundTripper *mhttp.MockRoundTripper, rootURL, jobPath, user, passwd string) ( + request *http.Request, response *http.Response) { + request, _ = http.NewRequest("POST", fmt.Sprintf("%s%s/%d/input/%s/proceed", rootURL, jobPath, 1, "Eff7d5dba32b4da32d9a67a519434d3f"), nil) + PrepareCommonPost(request, roundTripper, user, passwd, rootURL) + return +} diff --git a/client/test_methods.go b/client/test_methods.go index 9b3d0cf..fcfbb17 100644 --- a/client/test_methods.go +++ b/client/test_methods.go @@ -12,8 +12,6 @@ import ( "path/filepath" "strings" - "github.com/jenkins-zh/jenkins-cli/util" - "github.com/jenkins-zh/jenkins-cli/mock/mhttp" ) @@ -509,7 +507,7 @@ func PrepareForDeleteUser(roundTripper *mhttp.MockRoundTripper, rootURL, userNam // PrepareCommonPost only for test func PrepareCommonPost(request *http.Request, roundTripper *mhttp.MockRoundTripper, user, passwd, rootURL string) { request.Header.Add("CrumbRequestField", "Crumb") - request.Header.Add(util.ContentType, util.ApplicationForm) + // request.Header.Add(util.ContentType, util.ApplicationForm) response := &http.Response{ StatusCode: 200, Proto: "HTTP/1.1", diff --git a/go.mod b/go.mod index 89f3ef6..62c6b10 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/AlecAivazis/survey/v2 v2.0.4 + github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 github.com/Pallinder/go-randomdata v1.2.0 github.com/atotto/clipboard v0.1.2 github.com/golang/mock v1.3.1 -- GitLab