diff --git a/.gitignore b/.gitignore index c042e0bf..0360e0a2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -bin/ +bin/**/jcli release/ jcli diff --git a/.jenkins-cli.yaml b/.jenkins-cli.yaml new file mode 100644 index 00000000..1afa1c61 --- /dev/null +++ b/.jenkins-cli.yaml @@ -0,0 +1,12 @@ +current: test +jenkins_servers: +- name: yourServer + url: http://localhost:8080/jenkins + username: test + token: 211e3a2f0231198856dceaff96f2v75ce3 + insecureSkipVerify: true +#mirrors: +#- name: default +# url: http://mirrors.jenkins.io/ +# Language context is accept-language for HTTP header, It contains zh-CN/zh-TW/en/en-US/ja and so on +# Goto 'http://localhost:8080/jenkins/me/configure', then you can generate your token. diff --git a/Dockerfile b/Dockerfile index 5b44014c..7302b581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,10 @@ COPY . . RUN CGO_ENABLED=0 go build -v -a -o jcli . FROM alpine:3.10 -COPY --from=builder /work/jcli /usr/bin/jcli +ENV JOB_NAME "test" +COPY --from=builder /work/bin/linux/jcli /usr/bin/jcli RUN jcli config generate -i=false > ~/.jenkins-cli.yaml +COPY bin/build.sh /usr/bin/jclih +RUN chmod +x /usr/bin/jclih -ENTRYPOINT ["jcli"] +ENTRYPOINT ["jclih"] diff --git a/Makefile b/Makefile index fcfc9aa4..f533d873 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ gen-data-darwin: go-bindata-download-darwin verify: dep tools lint -pre-build: +pre-build: fmt vet export GO111MODULE=on export GOPROXY=https://goproxy.io go mod tidy diff --git a/app/cmd/backup.go b/app/cmd/backup.go index 628464a8..976b953b 100644 --- a/app/cmd/backup.go +++ b/app/cmd/backup.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -//BackupOption is an option for backup +// BackupOption is an option for backup type BackupOption struct { RoundTripper http.RoundTripper BackupDir string @@ -38,7 +38,7 @@ var backupCmd = &cobra.Command{ RunE: backupOption.Backup, } -//Check will find out whether Thin Backup Plugin installed or not +// Check will find out whether Thin Backup Plugin installed or not func (o *BackupOption) Check() (err error) { opt := PluginOptions{ Option: common.Option{RoundTripper: o.RoundTripper}, @@ -47,7 +47,7 @@ func (o *BackupOption) Check() (err error) { return } -//Backup will trigger thinBackup plugin to make a backup +// Backup will trigger thinBackup plugin to make a backup func (o *BackupOption) Backup(cmd *cobra.Command, _ []string) (err error) { jClient := &client.CoreClient{ JenkinsCore: client.JenkinsCore{ @@ -88,7 +88,7 @@ func (o *BackupOption) Backup(cmd *cobra.Command, _ []string) (err error) { return } -//ThinBackupAPI requests backupManual api +// ThinBackupAPI requests backupManual api func ThinBackupAPI(client *client.CoreClient) (err error) { _, err = client.RequestWithoutData(http.MethodGet, "/thinBackup/backupManual", nil, nil, 200) return err diff --git a/app/cmd/center_list.go b/app/cmd/center_list.go index c08d5e70..bbaab50e 100644 --- a/app/cmd/center_list.go +++ b/app/cmd/center_list.go @@ -14,31 +14,31 @@ import ( "github.com/spf13/cobra" ) -//LtsURL is the URL of stable Jenkins RSS +// LtsURL is the URL of stable Jenkins RSS const LtsURL = "https://www.jenkins.io/changelog-stable/rss.xml" -//WidthOfDescription is the width of the description column +// WidthOfDescription is the width of the description column const WidthOfDescription = 60 -//ASCIIOfLineFeed is the ASCII of line feed +// ASCIIOfLineFeed is the ASCII of line feed const ASCIIOfLineFeed = 10 -//ASCIIOfSpace is the ASCII of space +// ASCIIOfSpace is the ASCII of space const ASCIIOfSpace = 32 -//CenterListOption as options for Jenkins RSS +// CenterListOption as options for Jenkins RSS type CenterListOption struct { Channel Channel `xml:"channel"` // RoundTripper http.RoundTripper } -//Channel as part of CenterListOption +// Channel as part of CenterListOption type Channel struct { Title string `xml:"title"` Items []Item `xml:"item"` } -//Item as a option for information of newly-released Jenkins +// Item as a option for information of newly-released Jenkins type Item struct { Title string `xml:"title"` Description string `xml:"description"` diff --git a/app/cmd/common/plugin.go b/app/cmd/common/plugin.go index e89a27f3..ead76276 100644 --- a/app/cmd/common/plugin.go +++ b/app/cmd/common/plugin.go @@ -2,7 +2,7 @@ package common import "fmt" -//GetJCLIPluginPath returns the path of a jcli plugin +// GetJCLIPluginPath returns the path of a jcli plugin func GetJCLIPluginPath(userHome, name string, binary bool) string { suffix := ".yaml" if binary { diff --git a/app/cmd/condition/plugin_dep.go b/app/cmd/condition/plugin_dep.go index 86983b3e..e057141e 100644 --- a/app/cmd/condition/plugin_dep.go +++ b/app/cmd/condition/plugin_dep.go @@ -14,7 +14,7 @@ type PluginDepCheck struct { pluginName, targetVersion string } -//NewChecker returns a plugin dep checker +// NewChecker returns a plugin dep checker func NewChecker(jenkins *appCfg.JenkinsServer, roundTripper http.RoundTripper, pluginName, targetVersion string) ( checker *PluginDepCheck) { checker = &PluginDepCheck{ diff --git a/app/cmd/job_build.go b/app/cmd/job_build.go index 9a963f90..6429ff0b 100644 --- a/app/cmd/job_build.go +++ b/app/cmd/job_build.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/jenkins-zh/jenkins-cli/app/cmd/common" "github.com/jenkins-zh/jenkins-cli/app/i18n" @@ -18,15 +19,18 @@ type JobBuildOption struct { common.Option cobra_ext.OutputOption - Param string - ParamArray []string + Param string + ParamArray []string + ParamJsonString string ParamFilePathArray []string - Wait bool - WaitTime int - Delay int - Cause string + Wait bool + WaitTime int + WaitInterval int + Delay int + Cause string + LogConsole bool } var jobBuildOption JobBuildOption @@ -40,23 +44,28 @@ func init() { jobCmd.AddCommand(jobBuildCmd) jobBuildCmd.Flags().BoolVarP(&jobBuildOption.Batch, "batch", "b", false, "Batch mode, no need confirm") jobBuildCmd.Flags().StringVarP(&jobBuildOption.Param, "param", "", "", - i18n.T("Parameters of the job which is JSON format")) + i18n.T("Parameters of the job which is JSON format, for example: --param '{\"limit\":\"2\",\"timeoutLimit\":\"10\"}'")) jobBuildCmd.Flags().StringArrayVar(&jobBuildOption.ParamArray, "param-entry", nil, - i18n.T("Parameters of the job which are the entry format, for example: --param-entry name=value")) + i18n.T("Parameters of the job which are the entry format, for example: --param-entry name1=value1, --param-entry name2=value2")) jobBuildCmd.Flags().StringArrayVar(&jobBuildOption.ParamFilePathArray, "param-file", nil, i18n.T("Parameters of the job which is file path, for example: --param-file name=filename")) jobBuildCmd.Flags().BoolVarP(&jobBuildOption.Wait, "wait", "", false, i18n.T("If you want to wait for the build ID from Jenkins. You need to install plugin pipeline-restful-api first")) - jobBuildCmd.Flags().IntVarP(&jobBuildOption.WaitTime, "wait-timeout", "", 30, + jobBuildCmd.Flags().IntVarP(&jobBuildOption.WaitTime, "wait-timeout", "", 60, i18n.T("The timeout of seconds when you wait for the build ID")) + jobBuildCmd.Flags().IntVarP(&jobBuildOption.WaitInterval, "wait-interval", "", 10, + i18n.T("The interval of seconds when you want to wait for buildID... query, use with wait")) jobBuildCmd.Flags().IntVarP(&jobBuildOption.Delay, "delay", "", 0, i18n.T("Delay when trigger a Jenkins job")) jobBuildCmd.Flags().StringVarP(&jobBuildOption.Cause, "cause", "", "triggered by jcli", i18n.T("The cause of a job build")) + jobBuildCmd.Flags().BoolVarP(&jobBuildOption.LogConsole, "log", "l", false, + i18n.T("If you want to wait for build log and wait log output end")) jobBuildOption.SetFlagWithHeaders(jobBuildCmd, "Number,URL") jobBuildOption.BatchOption.Stdio = common.GetSystemStdio() jobBuildOption.Option.Stdio = common.GetSystemStdio() + } var jobBuildCmd = &cobra.Command{ @@ -66,15 +75,28 @@ var jobBuildCmd = &cobra.Command{ You need to give the parameters if your pipeline has them. Learn more about it from https://jenkins.io/doc/book/pipeline/syntax/#parameters.`), Args: cobra.MinimumNArgs(1), PreRunE: func(_ *cobra.Command, _ []string) (err error) { - if jobBuildOption.ParamArray == nil && jobBuildOption.ParamFilePathArray == nil { + if jobBuildOption.ParamArray == nil && jobBuildOption.ParamFilePathArray == nil && jobBuildOption.Param == "" { return } paramDefs := make([]client.ParameterDefinition, 0) if jobBuildOption.Param != "" { - if err = json.Unmarshal([]byte(jobBuildOption.Param), ¶mDefs); err != nil { + paramMap := make(map[string]interface{}) + if err = json.Unmarshal([]byte(jobBuildOption.Param), ¶mMap); err != nil { + logger.Error(fmt.Sprintf("build param unmarshal error %v", err.Error())) return } + for key, value := range paramMap { + if key == "" || value == nil { + logger.Error("build param key or value empty") + return + } + paramDefs = append(paramDefs, client.ParameterDefinition{ + Name: key, + Value: fmt.Sprintf("%v", value), + Type: client.StringParameterDefinition, + }) + } } for _, paramEntry := range jobBuildOption.ParamArray { @@ -113,6 +135,7 @@ You need to give the parameters if your pipeline has them. Learn more about it f jclient := &client.JobClient{ JenkinsCore: client.JenkinsCore{ RoundTripper: jobBuildOption.RoundTripper, + Timeout: time.Duration(jobBuildOption.WaitTime) * time.Second, }, } getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) @@ -150,13 +173,18 @@ You need to give the parameters if your pipeline has them. Learn more about it f } if err == nil { + options := client.JobCmdOptionsCommon{ + Wait: jobBuildOption.Wait, + WaitTime: jobBuildOption.WaitTime, + WaitInterval: jobBuildOption.WaitInterval, + LogConsole: jobBuildOption.LogConsole, + } + if hasParam { - err = jclient.BuildWithParams(name, paramDefs) - } else if jobBuildOption.Wait { - var build client.IdentityBuild - if build, err = jclient.BuildAndReturn(name, jobBuildOption.Cause, jobBuildOption.WaitTime, jobBuildOption.Delay); err == nil { - jobBuildOption.Writer = cmd.OutOrStdout() - err = jobBuildOption.OutputV2([1]client.SimpleJobBuild{build.Build.SimpleJobBuild}) + var jobState client.JenkinsBuildState + jobState, err = jclient.BuildWithParamsGetResponse(name, paramDefs, options) + if err == nil && jobBuildOption.LogConsole && jobState.RunId > 0 { + err = printLogRunFunc(name, JobLogOptionGetDefault(int(jobState.RunId)), cmd) } } else { err = jclient.Build(name) diff --git a/app/cmd/job_delete_test.go b/app/cmd/job_delete_test.go index dba9cca9..bcb145ab 100644 --- a/app/cmd/job_delete_test.go +++ b/app/cmd/job_delete_test.go @@ -13,7 +13,6 @@ import ( "github.com/AlecAivazis/survey/v2/terminal" "github.com/golang/mock/gomock" - "github.com/hinshun/vt10x" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/stretchr/testify/require" @@ -180,9 +179,9 @@ func RunTest(t *testing.T, test func(terminal.Stdio) error, procedures ...func(* // Multiplex output to a buffer as well for the raw bytes. buf := new(bytes.Buffer) - //c, err := expect.NewConsole(expect.WithStdout(buf)) + c, err := expect.NewConsole(expect.WithStdout(buf)) //c, err := expect.NewConsole(expect.WithStdout(os.Stdout)) - c, _, err := vt10x.NewVT10XConsole(expect.WithStdout(buf)) + //c, _, err := vt10x.New(expect.WithStdout(buf)) require.Nil(t, err) defer c.Close() diff --git a/app/cmd/job_edit.go b/app/cmd/job_edit.go index 2f893986..75f1d7e7 100644 --- a/app/cmd/job_edit.go +++ b/app/cmd/job_edit.go @@ -148,7 +148,7 @@ func (j *JobEditOption) getSampleJenkinsfile() string { ` } -//func getPipeline(name string) (script string, err error) { +// func getPipeline(name string) (script string, err error) { func (j *JobEditOption) getPipeline(jClient *client.JobClient, name string) (script string, err error) { script = j.Script //we take the script from input firstly if script != "" { diff --git a/app/cmd/job_log.go b/app/cmd/job_log.go index dc69f7a1..310b4779 100644 --- a/app/cmd/job_log.go +++ b/app/cmd/job_log.go @@ -142,8 +142,8 @@ func printLogRunFunc(jobName string, jobLogOption JobLogOption, cmd *cobra.Comma if lastBuildID != jobLogOption.LastBuildID { lastBuildID = jobLogOption.LastBuildID - cmd.Println("Current build number:", jobLogOption.LastBuildID) - cmd.Println("Current build url:", jobLogOption.LastBuildURL) + cmd.Println("[INFO] Current build number:", jobLogOption.LastBuildID) + cmd.Printf("[INFO] Current build url: %sconsole\n", jobLogOption.LastBuildURL) err = printLog(jclient, cmd, name, jobLogOption.History, 0, jobLogOption.NumberOfLines) } @@ -151,7 +151,7 @@ func printLogRunFunc(jobName string, jobLogOption JobLogOption, cmd *cobra.Comma if err != nil || !jobLogOption.Watch { if err.Error() == LogFinishMsg { err = nil - cmd.Println("[INFO] current log finish output") + cmd.Println("[INFO] current log finish output... exit") if jobLogOption.ExitCode { if jobBuild, err = jclient.GetBuild(name, jobLogOption.History); err == nil { if jobBuild.Result == JobResultFailed { @@ -169,3 +169,17 @@ func printLogRunFunc(jobName string, jobLogOption JobLogOption, cmd *cobra.Comma } return } + +// JobLogOptionGetDefault get default config for job log +func JobLogOptionGetDefault(runId int) JobLogOption { + return JobLogOption{ + History: runId, + WatchOption: common.WatchOption{ + Watch: true, + Interval: jobBuildOption.WaitInterval, + Count: 9999, + }, + NumberOfLines: 9999, + ExitCode: true, + } +} diff --git a/app/cmd/plugin_apitest.go b/app/cmd/plugin_apitest.go index 2e5e30e0..3ecd7805 100644 --- a/app/cmd/plugin_apitest.go +++ b/app/cmd/plugin_apitest.go @@ -159,7 +159,7 @@ func (o *pluginAPITestOption) test(cmd *cobra.Command, args []string) (err error return err } -//CheckFileExists returns true if exits and returns false if not +// CheckFileExists returns true if exits and returns false if not func CheckFileExists(path string) (exist bool, err error) { _, err = os.Stat(path) if err == nil { diff --git a/app/cmd/plugin_center.go b/app/cmd/plugin_center.go index 21bfda4b..28cf651d 100644 --- a/app/cmd/plugin_center.go +++ b/app/cmd/plugin_center.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" ) -//NewPluginOption consists of four options +// NewPluginOption consists of four options type NewPluginOption struct { Name string `json:"name"` Version string `json:"gav"` diff --git a/app/i18n/bindata.go b/app/i18n/bindata.go index 574289c7..f50115b3 100644 --- a/app/i18n/bindata.go +++ b/app/i18n/bindata.go @@ -178,11 +178,13 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"} // AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("foo.txt") and AssetDir("notexist") would return an error diff --git a/bin/build.sh b/bin/build.sh new file mode 100644 index 00000000..96bbcad2 --- /dev/null +++ b/bin/build.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +BIN_PATH=/usr/bin/jcli + +# If we run make directly, any files created on the bind mount +# will have awkward ownership. So we switch to a user with the +# same user and group IDs as source directory. We have to set a +# few things up so that sudo works without complaining later on. +${BIN_PATH} job build ${JOB_NAME} \ + -b --url https://xxxx.com \ + --config-load false \ + --wait -l \ + --logger-level info $* \ No newline at end of file diff --git a/client/common.go b/client/common.go index b593959a..b494585e 100644 --- a/client/common.go +++ b/client/common.go @@ -5,14 +5,16 @@ import ( "encoding/base64" "encoding/json" "fmt" - "go.uber.org/zap" "io" "io/ioutil" "log" - "moul.io/http2curl" "net/http" + "strconv" "time" + "go.uber.org/zap" + "moul.io/http2curl" + "github.com/jenkins-zh/jenkins-cli/util" ext "github.com/linuxsuren/cobra-extension/version" httpdownloader "github.com/linuxsuren/http-downloader/pkg/net" @@ -167,9 +169,45 @@ func (j *JenkinsCore) RequestWithoutData(method, api string, headers map[string] statusCode != successCode { err = j.ErrorHandle(statusCode, data) } + return } +// RequestWithoutData requests the api without handling data +func (j *JenkinsCore) RequestWithDataResponse(method, api string, headers map[string]string, + payload io.Reader, successCode int) (JenkinsBuildState, error) { + var ( + data []byte + state JenkinsBuildState + ) + + if response, err := j.RequestWithResponse(method, api, headers, payload); err == nil { + statusCode := response.StatusCode + data, _ = ioutil.ReadAll(response.Body) + if statusCode == successCode { + state.BodyData = data + state.StatusCode = response.StatusCode + if len(response.Header.Get("Location")) > 0 { + locationSlice := util.ArraySplitAndDeleteEmpty(response.Header.Get("Location"), "/") + queueId := locationSlice[len(locationSlice)-1] + if len(queueId) > 0 { + if state.QueueId, err = strconv.ParseInt(queueId, 10, 64); err != nil { + logger.Error("request job run queue error", zap.String("queue id", queueId)) + return state, err + } + } + } + } else { + err = j.ErrorHandle(statusCode, data) + return state, err + } + } else { + return state, err + } + + return state, nil +} + // ErrorHandle handles the error cases func (j *JenkinsCore) ErrorHandle(statusCode int, data []byte) (err error) { if statusCode >= 400 && statusCode < 500 { diff --git a/client/job.go b/client/job.go index b743aafb..d962aa73 100644 --- a/client/job.go +++ b/client/job.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - httpdownloader "github.com/linuxsuren/http-downloader/pkg" "io" "io/ioutil" "mime/multipart" @@ -14,6 +13,9 @@ import ( "path/filepath" "strconv" "strings" + "time" + + httpdownloader "github.com/linuxsuren/http-downloader/pkg" "go.uber.org/zap" "moul.io/http2curl" @@ -24,6 +26,8 @@ const ( StringParameterDefinition = "StringParameterDefinition" // FileParameterDefinition is the definition for file parameter FileParameterDefinition = "FileParameterDefinition" + // QueueWaitDefinition is the definition for file queue state wait define + QueueWaitDefinition = "hudson.model.Queue$WaitingItem" ) // JobClient is client for operate jobs @@ -185,7 +189,7 @@ func (q *JobClient) BuildWithParams(jobName string, parameters []ParameterDefini formData := url.Values{"json": {fmt.Sprintf("{\"parameter\": %s}", string(paramJSON))}} payload := strings.NewReader(formData.Encode()) - _, err = q.RequestWithoutData(http.MethodPost, api, + _, err = q.RequestWithDataResponse(http.MethodPost, api, map[string]string{httpdownloader.ContentType: httpdownloader.ApplicationForm}, payload, 201) } return @@ -483,6 +487,115 @@ func ParseJobPath(jobName string) (path string) { return } +// BuildWithParamsGetResponse get params request response with run id... +func (q *JobClient) BuildWithParamsGetResponse(jobName string, parameters []ParameterDefinition, options JobCmdOptionsCommon) (resp JenkinsBuildState, err error) { + path := ParseJobPath(jobName) + api := fmt.Sprintf("%s/buildWithParameters?", path) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + defer writer.Close() + + hasFileParam := false + stringParameters := make([]ParameterDefinition, 0, len(parameters)) + formData := url.Values{} + for _, parameter := range parameters { + if parameter.Type == FileParameterDefinition { + hasFileParam = true + var file *os.File + file, err = os.Open(parameter.Filepath) + if err != nil { + return + } + defer file.Close() + + var fWriter io.Writer + fWriter, err = writer.CreateFormFile(parameter.Filepath, filepath.Base(parameter.Filepath)) + if err != nil { + return + } + _, err = io.Copy(fWriter, file) + } else { + stringParameters = append(stringParameters, parameter) + formData.Set(parameter.Name, parameter.Value) + } + } + + var paramJSON []byte + if len(stringParameters) == 1 { + paramJSON, err = json.Marshal(stringParameters[0]) + } else { + paramJSON, err = json.Marshal(stringParameters) + } + if err != nil { + return + } + + if hasFileParam { + if err = writer.WriteField("json", fmt.Sprintf("{\"parameter\": %s}", string(paramJSON))); err != nil { + return + } + + if err = writer.Close(); err != nil { + return + } + + _, err = q.RequestWithoutData(http.MethodPost, api, + map[string]string{httpdownloader.ContentType: writer.FormDataContentType()}, body, 201) + } else { + payload := strings.NewReader(formData.Encode()) + var jobRespState JenkinsBuildState + var queueResp JenkinsBuildExecutable + + jobRespState, err = q.RequestWithDataResponse(http.MethodPost, api, + map[string]string{httpdownloader.ContentType: httpdownloader.ApplicationForm}, payload, 201) + logger.Info("Build job trigger response msg...", + zap.Int("statusCode", jobRespState.StatusCode), + zap.Int64("queueId", jobRespState.QueueId), + zap.Bool("isWaitForRunID", options.Wait)) + + // if wait will query runId + if options.Wait { + if jobRespState.QueueId > 0 { + if queueResp, err = q.GetBuildQueueIdResponseWait(jobRespState.QueueId, options.WaitInterval); err == nil { + jobRespState.RunId = queueResp.Executable.Number + jobRespState.BuildUrl = queueResp.Executable.URL + } + resp = jobRespState + logger.Info("Build job state msg", + zap.Int64("runId", jobRespState.RunId), + zap.String("buildUrl", jobRespState.BuildUrl), + zap.Int("statusCode", jobRespState.StatusCode), + ) + } + } + } + return +} + +// GetBuildQueueIdResponse get queue api by queue id +func (q *JobClient) GetBuildQueueIdResponseWait(queueId int64, interval int) (resp JenkinsBuildExecutable, err error) { + if queueId > 0 { + for { + time.Sleep(time.Duration(interval) * time.Second) + logger.Info("Waiting seconds for query run id by queue id...", zap.Int("interval", interval), zap.Int64("queueId", queueId)) + if err = q.RequestWithData(http.MethodGet, GetQueueApi(queueId), nil, nil, 200, &resp); err != nil { + logger.Error("failed to get queue item", zap.Error(err)) + return + } + if !resp.isWaitItem() { + break + } + } + } + return +} + +// GetQueue api uri +func GetQueueApi(queueId int64) string { + return fmt.Sprintf("queue/item/%d/api/json", queueId) +} + // JobLog holds the log text type JobLog struct { HasMore bool @@ -572,6 +685,39 @@ type JobBuild struct { NextBuild SimpleJobBuild } +// Jenkins build state for job +type JenkinsBuildState struct { + StatusCode int `json:"-"` + RunId int64 `json:"run_id,omitempty"` + BuildUrl string `json:"build_url,omitempty"` + QueueId int64 `json:"queue_id,omitempty"` + BodyData []byte `json:"-"` +} + +// Jenkins build executable for response +type JenkinsBuildExecutable struct { + Class string `json:"_class,omitempty"` + Executable JenkinsBuildExecutableInline `json:"executable,omitempty"` +} + +// Jenkins build executable inline for response +type JenkinsBuildExecutableInline struct { + Class string `json:"_class,omitempty"` + Number int64 `json:"number,omitempty"` + URL string `json:"url,omitempty"` +} + +func (j *JenkinsBuildExecutable) isWaitItem() bool { + return j.Class == QueueWaitDefinition +} + +type JobCmdOptionsCommon struct { + Wait bool + WaitTime int + LogConsole bool + WaitInterval int +} + // Pipeline represents a pipeline type Pipeline struct { Script string diff --git a/client/status_test_common.go b/client/status_test_common.go index 9d69a967..ed3b7275 100644 --- a/client/status_test_common.go +++ b/client/status_test_common.go @@ -8,7 +8,7 @@ import ( "net/http" ) -//PrepareGetStatus only for test +// PrepareGetStatus only for test func PrepareGetStatus(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) { request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/json", rootURL), nil) response := &http.Response{ diff --git a/docs/book/zh/job.md b/docs/book/zh/job.md index e4856487..d4528d76 100644 --- a/docs/book/zh/job.md +++ b/docs/book/zh/job.md @@ -26,6 +26,10 @@ weight: 80 `jcli job build job/test -b --wait` +触发并等待日志输出,失败退出日志exit 1,成功exit 0: + +`jcli job build job/test -b --url https://xxxx.com --username xxx --token xxxx --param '{"limit":"3","timeoutLimit":"10"}' --config-load false --wait -l --logger-level info` + ## 交互式输入 执行到 Jenkins 流水线中的 `input` 指令时,会有交互式输入的提示。下面是一个样例: diff --git a/go.mod b/go.mod index fda52d92..3d935bf4 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v29 v29.0.3 github.com/hashicorp/go-version v1.2.1 - github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec github.com/jedib0t/go-pretty/v6 v6.6.1 github.com/jenkins-zh/jenkins-client v0.0.15 github.com/jenkins-zh/jenkins-formulas v0.0.5 diff --git a/pkg/docker/start_jenkins.go b/pkg/docker/start_jenkins.go index 5326a95c..c765eb16 100644 --- a/pkg/docker/start_jenkins.go +++ b/pkg/docker/start_jenkins.go @@ -17,7 +17,7 @@ import ( "github.com/spf13/cobra" ) -//RunOption contains some of the options used to create a docker image and run a container +// RunOption contains some of the options used to create a docker image and run a container type RunOption struct { ImageName string Tag string @@ -32,17 +32,17 @@ type RunOption struct { TmpDir string } -//DockerRunOption is an option for starting a container in docker +// DockerRunOption is an option for starting a container in docker var DockerRunOption RunOption -//ConnectToDocker returns a client which is used to connect to a local or remote docker host +// ConnectToDocker returns a client which is used to connect to a local or remote docker host func (o *RunOption) ConnectToDocker() (cli *client.Client, err error) { tcp := fmt.Sprintf("tcp://%s:%d", o.IP, o.DockerPort) cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation(), client.WithHost(tcp)) return cli, err } -//CreateImageAndRunContainer is to create an docker image and run a container from the image +// CreateImageAndRunContainer is to create an docker image and run a container from the image func (o *RunOption) CreateImageAndRunContainer(cmd *cobra.Command, args []string) (err error) { ctx := context.Background() cli, err := o.ConnectToDocker() @@ -87,7 +87,7 @@ func (o *RunOption) CreateImageAndRunContainer(cmd *cobra.Command, args []string return nil } -//BuildImage is used to build an image +// BuildImage is used to build an image func (o *RunOption) BuildImage(cmd *cobra.Command) error { ctx := context.Background() cli, _ := o.ConnectToDocker() @@ -114,7 +114,7 @@ func (o *RunOption) BuildImage(cmd *cobra.Command) error { return nil } -//GetTarReader creates tarReader for args in function BuildImage +// GetTarReader creates tarReader for args in function BuildImage func (o *RunOption) GetTarReader(cmd *cobra.Command) (*bytes.Reader, error) { src := []string{o.DockerfilePath, o.WarPath, o.Script} buf := new(bytes.Buffer) @@ -147,7 +147,7 @@ func (o *RunOption) GetTarReader(cmd *cobra.Command) (*bytes.Reader, error) { return dockerFileTarReader, nil } -//CreateDockerfile will create a docker file for running a jenkins war contains plugins hpi +// CreateDockerfile will create a docker file for running a jenkins war contains plugins hpi func (o *RunOption) CreateDockerfile(cmd *cobra.Command, args []string) (err error) { dir, _ := ioutil.TempDir("", "jenkins-cli") o.TmpDir = dir diff --git a/util/url.go b/util/url.go index 6c94bbbc..e7ebb7cf 100644 --- a/util/url.go +++ b/util/url.go @@ -3,6 +3,7 @@ package util import ( "net/url" "path" + "strings" ) // URLJoin is a util function to join host URL and API URL @@ -14,7 +15,7 @@ func URLJoin(host, api string) (targetURL *url.URL, err error) { return } -//URLJoinAsString is a util function to join host URL and API URL +// URLJoinAsString is a util function to join host URL and API URL func URLJoinAsString(host, api string) (targetURLStr string, err error) { var targetURL *url.URL if targetURL, err = URLJoin(host, api); err == nil { @@ -22,3 +23,15 @@ func URLJoinAsString(host, api string) (targetURLStr string, err error) { } return } + +// ArraySplitAndDeleteEmpty split string and delete empty element +func ArraySplitAndDeleteEmpty(s, sep string) []string { + var r []string + stringList := strings.Split(s, sep) + for _, str := range stringList { + if str != "" { + r = append(r, str) + } + } + return r +}