├── OWNERS ├── .gitbook.yaml ├── .github ├── FUNDING.yml ├── workflows │ ├── release-drafter.yml │ ├── generator.yaml │ ├── release.yaml │ ├── sonarqube.yaml.bak │ └── backup.yaml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── bug_report.md │ └── release-v0-0-x.md ├── PULL_REQUEST_TEMPLATE.md ├── release-drafter.yml └── settings.yml ├── main.go ├── docs └── book │ ├── zh │ ├── shell.md │ ├── proxy.md │ ├── hooks.md │ ├── doctor.md │ ├── completion.md │ ├── user.md │ ├── credential.md │ ├── open.md │ ├── agent.md │ └── plugin.md │ ├── advanced │ └── setup-config-path.md │ ├── en │ ├── proxy.md │ ├── completion.md │ ├── plugin.md │ └── job.md │ └── SUMMARY.md ├── util ├── env.go ├── logger_test.go ├── setup_test.go ├── cmd_test.go ├── logger.go ├── password.go ├── collect.go ├── url.go ├── password_test.go └── collect_test.go ├── client ├── setup_test.go ├── status_test_common.go ├── common_test_common.go ├── pluginManager_test_common.go ├── requestmatcher_test.go ├── queue_test.go ├── status_test.go ├── status.go ├── updateCenter_test_common.go ├── casc.go ├── artifacts_test_common.go ├── artifacts_test.go ├── queue.go ├── casc_test.go ├── artifacts.go ├── release.go ├── release_test.go ├── core.go └── casc_test_common.go ├── app ├── cmd │ ├── common │ │ ├── version_since.go │ │ ├── plugin.go │ │ ├── setup_test.go │ │ ├── cmd_valid.go │ │ └── completion.go │ ├── queue.go │ ├── setup_test.go │ ├── credential.go │ ├── plugin_formula_test.go │ ├── config_edit_test.go │ ├── job.go │ ├── config │ │ └── root.go │ ├── center_upgrade.go │ ├── crumbIssuer.go │ ├── plugin_create_test.go │ ├── plugin_release_test.go │ ├── computer_log.go │ ├── plugin_trend.go │ ├── center_start_test.go │ ├── computer_create.go │ ├── job_enable.go │ ├── center_labels.go │ ├── job_disable.go │ ├── computer_delete.go │ ├── plugin_uninstall.go │ ├── job_history_edit.go │ ├── queue_test.go │ ├── doc_test.go │ ├── plugin.go │ ├── user_create.go │ ├── center_list.txt │ ├── center_identity.go │ ├── user.go │ ├── config_select.go │ ├── queue_list.go │ ├── config_edit.go │ ├── job_delete.go │ ├── user_edit_test.go │ ├── user_edit.go │ ├── plugin_list.go │ ├── shell_test.go │ ├── plugin_open.go │ ├── plugin_build.go │ ├── plugin_open_test.go │ ├── user_delete.go │ ├── job_stop.go │ ├── keyring │ │ └── core.go │ ├── plugin_create.go │ ├── config_update.go │ ├── queue_cancel.go │ ├── plugin_trend_test.go │ ├── computer_list.go │ ├── job_enable_test.go │ ├── job_disable_test.go │ ├── plugin_checkout_test.go │ ├── job_artifact.go │ ├── plugin_check.go │ ├── center_mirror.go │ ├── crumbissuer_test.go │ ├── shutdown_test.go │ ├── job_history.go │ ├── restart.go │ ├── center_identity_test.go │ ├── config_remove.go │ ├── computer_create_test.go │ ├── credential_list.go │ ├── computer_delete_test.go │ ├── plugin_download_test.go │ ├── computer_log_test.go │ ├── user_token.go │ ├── runner_test.go │ ├── plugin_upload_test.go │ ├── config_add_test.go │ ├── config_list.go │ ├── job_log_test.go │ ├── plugin_run.go │ ├── credential_list_test.go │ ├── computer_list_test.go │ ├── computer.go │ ├── config_add.go │ ├── queue_cancel_test.go │ ├── job_history_test.go │ ├── config_data.go │ ├── job_type.go │ └── condition │ │ └── plugin_dep.go ├── i18n │ ├── setup_test.go │ └── i18n_test.go ├── helper │ ├── setup_test.go │ └── error.go ├── health │ ├── setup_test.go │ ├── core.go │ └── core_test.go └── config │ └── type.go ├── e2e ├── root_test.go ├── shutdown_test.go ├── withdependencies │ ├── credential_test.go │ ├── job_test.go │ └── setup_test.go ├── queue_test.go ├── doc_test.go ├── config_test.go ├── job_test.go ├── setup_test.go ├── computer_test.go ├── without_jenkins │ └── completion_test.go └── plugin_test.go ├── Dockerfile ├── .gitpod.yml ├── .jenkins-cli.yaml ├── bin └── build.sh ├── .gitignore ├── snapcraft.yaml ├── sonar-project.properties ├── .goreleaser.yml ├── LICENSE ├── .devcontainer └── devcontainer.json └── mock └── mhttp └── roundtripper.go /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - linuxsuren 3 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/book 2 | 3 | structure: 4 | readme: README.md 5 | summary: SUMMARY.md 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://space.bilibili.com/433584098'] 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /docs/book/zh/shell.md: -------------------------------------------------------------------------------- 1 | 创建一个子 Shell 并执行 `jcli` 命令。这时候,不管如何修改 `jcli` 的配置文件,退出后都不会影响之前的配置。 2 | 3 | 执行下面的命令,会将所选择的 Jenkins 配置 `local` 作为默认的值: 4 | 5 | `jcli shell local` 6 | 7 | 此时,我们执行命令 `jcli config` 的话能看出来。 8 | -------------------------------------------------------------------------------- /util/env.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "os" 4 | 5 | // GetEnvOrDefault returns a env or default value 6 | func GetEnvOrDefault(key, defaultVal string) string { 7 | if val, ok := os.LookupEnv(key); ok { 8 | return val 9 | } 10 | return defaultVal 11 | } 12 | -------------------------------------------------------------------------------- /client/setup_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestJenkinsClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "jenkins client test") 13 | } 14 | -------------------------------------------------------------------------------- /app/cmd/common/version_since.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | // VersionSince0028 represents v0.0.28 5 | VersionSince0028 = "v0.0.28" 6 | // VersionSince0024 represents v0.0.24 7 | VersionSince0024 = "v0.0.24" 8 | // VersionSince0031 represents v0.0.31 9 | VersionSince0031 = "v0.0.31" 10 | ) 11 | -------------------------------------------------------------------------------- /app/cmd/queue.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | rootCmd.AddCommand(queueCmd) 9 | } 10 | 11 | var queueCmd = &cobra.Command{ 12 | Use: "queue", 13 | Short: "Manage the queue of your Jenkins", 14 | Long: `Manage the queue of your Jenkins`, 15 | } 16 | -------------------------------------------------------------------------------- /app/cmd/common/plugin.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "fmt" 4 | 5 | // GetJCLIPluginPath returns the path of a jcli plugin 6 | func GetJCLIPluginPath(userHome, name string, binary bool) string { 7 | suffix := ".yaml" 8 | if binary { 9 | suffix = "" 10 | } 11 | return fmt.Sprintf("%s/.jenkins-cli/plugins/%s%s", userHome, name, suffix) 12 | } 13 | -------------------------------------------------------------------------------- /e2e/root_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os/exec" 6 | "testing" 7 | ) 8 | 9 | func TestRoot(t *testing.T) { 10 | cmd := exec.Command("jcli") 11 | data, err := cmd.CombinedOutput() 12 | assert.Nil(t, err) 13 | assert.Contains(t, string(data), "Jenkins CLI (jcli) manage your Jenkins") 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN_SECRETS }} 16 | -------------------------------------------------------------------------------- /docs/book/advanced/setup-config-path.md: -------------------------------------------------------------------------------- 1 | # Setup config file 2 | 3 | In some cases, you might want to give it a specific configuration path instead of the default one. You can do it by add an environment variable `JCLI_CONFIG`. 4 | 5 | For example, put the following config into your shell profile: 6 | 7 | ```text 8 | JCLI_CONFIG=/var/yourPath/jenkins-cli.yaml 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /util/logger_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("logger test", func() { 9 | Context("InitLogger", func() { 10 | It("basic test", func() { 11 | logger, err := InitLogger("warn") 12 | Expect(err).To(BeNil()) 13 | Expect(logger).NotTo(BeNil()) 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | pull-request-branch-name: 8 | separator: "-" 9 | - package-ecosystem: "gomod" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | pull-request-branch-name: 14 | separator: "-" 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23 AS builder 2 | 3 | WORKDIR /work 4 | COPY . . 5 | RUN CGO_ENABLED=0 go build -v -a -o jcli . 6 | 7 | FROM alpine:3.10 8 | ENV JOB_NAME "test" 9 | COPY --from=builder /work/bin/linux/jcli /usr/bin/jcli 10 | RUN jcli config generate -i=false > ~/.jenkins-cli.yaml 11 | COPY bin/build.sh /usr/bin/jclih 12 | RUN chmod +x /usr/bin/jclih 13 | 14 | ENTRYPOINT ["jclih"] 15 | -------------------------------------------------------------------------------- /e2e/shutdown_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func TestShutdown(t *testing.T) { 11 | cmd := exec.Command("jcli", "shutdown", "--url", GetJenkinsURL()) 12 | data, err := cmd.CombinedOutput() 13 | assert.Nil(t, err, fmt.Sprintf("failed in shutdown Jenkins, output is %s", string(data))) 14 | } 15 | -------------------------------------------------------------------------------- /app/cmd/setup_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestCmd(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-app.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "app/cmd", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /util/setup_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestUtils(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-utils.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "util", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /app/i18n/setup_test.go: -------------------------------------------------------------------------------- 1 | package i18n_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestI18n(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-i18n.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "app", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /e2e/withdependencies/credential_test.go: -------------------------------------------------------------------------------- 1 | package withdependencies 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestListCredentials(t *testing.T) { 12 | cmd := exec.Command("jcli", "credential", "list", "--url", GetJenkinsURL()) 13 | data, err := cmd.CombinedOutput() 14 | assert.Nil(t, err) 15 | 16 | fmt.Println(string(data)) 17 | } 18 | -------------------------------------------------------------------------------- /app/helper/setup_test.go: -------------------------------------------------------------------------------- 1 | package helper_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestApp(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("helper.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "app/helper", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /docs/book/zh/proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 代理 3 | weight: 90 4 | --- 5 | 6 | # 代理设置 7 | 8 | 你可能需要设置代理才可以访问到 Jenkins,这时候,可以给 `jcli` 配置代理服务器的信息。 执行命令:`jcli config edit` 就会打开配置文件,参考下面的配置: 9 | 10 | ```text 11 | jenkins_servers: 12 | - name: dev 13 | url: http://192.168.1.10 14 | username: admin 15 | token: 11132c9ae4b20edbe56ac3e09cb5a3c8c2 16 | proxy: http://192.168.10.10:47586 17 | proxyAuth: username:password 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: | 3 | [[ ! -z "${DOCKER_USER}" && ! -z "${DOCKER_PASSWD}" ]] && docker login -u${DOCKER_USER} -p${DOCKER_PASSWD} 4 | git config --global user.name $GIT_AUTHOR_NAME 5 | git config --global user.email $GIT_COMMITTER_EMAIL 6 | gh repo fork --remote 7 | 8 | vscode: 9 | extensions: 10 | - golang.go 11 | - github.vscode-pull-request-github 12 | - ms-azuretools.vscode-docker 13 | -------------------------------------------------------------------------------- /app/cmd/common/setup_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestCmd(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-app-cmd-common.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "app/cmd/common", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /app/health/setup_test.go: -------------------------------------------------------------------------------- 1 | package health_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/onsi/ginkgo/reporters" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestHealth(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | junitReporter := reporters.NewJUnitReporter("test-health.xml") 15 | RunSpecsWithDefaultAndCustomReporters(t, "command health check", []Reporter{junitReporter}) 16 | } 17 | -------------------------------------------------------------------------------- /e2e/queue_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func TestListQueue(t *testing.T) { 11 | cmd := exec.Command("jcli", "queue", "list", "--url", GetJenkinsURL()) 12 | data, err := cmd.CombinedOutput() 13 | assert.Nil(t, err, fmt.Sprintf("failed in cmd queue list, output is %s", string(data))) 14 | 15 | assert.Contains(t, string(data), "ID Why URL") 16 | } 17 | -------------------------------------------------------------------------------- /app/health/core.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | // CommandHealth is the interface for register a command checker 4 | type CommandHealth interface { 5 | Check() error 6 | } 7 | 8 | // CheckRegister is the register container 9 | type CheckRegister struct { 10 | Member map[string]CommandHealth 11 | } 12 | 13 | // Register can register a command and function 14 | func (c *CheckRegister) Register(path string, health CommandHealth) { 15 | c.Member[path] = health 16 | } 17 | -------------------------------------------------------------------------------- /e2e/doc_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestDoc(t *testing.T) { 13 | tempDir := os.TempDir() 14 | defer os.RemoveAll(tempDir) 15 | 16 | cmd := exec.Command("jcli", "doc", tempDir) 17 | _, err := cmd.CombinedOutput() 18 | assert.Nil(t, err) 19 | 20 | _, err = os.Stat(path.Join(tempDir, "jcli.md")) 21 | assert.Nil(t, err) 22 | } 23 | -------------------------------------------------------------------------------- /.jenkins-cli.yaml: -------------------------------------------------------------------------------- 1 | current: test 2 | jenkins_servers: 3 | - name: yourServer 4 | url: http://localhost:8080/jenkins 5 | username: test 6 | token: 211e3a2f0231198856dceaff96f2v75ce3 7 | insecureSkipVerify: true 8 | #mirrors: 9 | #- name: default 10 | # url: http://mirrors.jenkins.io/ 11 | # Language context is accept-language for HTTP header, It contains zh-CN/zh-TW/en/en-US/ja and so on 12 | # Goto 'http://localhost:8080/jenkins/me/configure', then you can generate your token. 13 | -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | BIN_PATH=/usr/bin/jcli 6 | 7 | # If we run make directly, any files created on the bind mount 8 | # will have awkward ownership. So we switch to a user with the 9 | # same user and group IDs as source directory. We have to set a 10 | # few things up so that sudo works without complaining later on. 11 | ${BIN_PATH} job build ${JOB_NAME} \ 12 | -b --url https://xxxx.com \ 13 | --config-load false \ 14 | --wait -l \ 15 | --logger-level info $* -------------------------------------------------------------------------------- /docs/book/zh/hooks.md: -------------------------------------------------------------------------------- 1 | 命令钩子,允许你在执行命令前后,执行特定的命令;钩子包括有前置和后置命令。 2 | 3 | 例如:我们可以给执行上传插件的命令添加钩子,上传前构建插件项目,上传完成后重启 Jenkins 4 | 5 | ``` 6 | preHooks: 7 | - path: plugin.upload 8 | cmd: mvn clean package -DskipTests -Dmaven.test.skip 9 | postHooks: 10 | - path: plugin.upload 11 | cmd: jcli center watch --util-install-complete 12 | - path: plugin.upload 13 | cmd: jcli restart -b 14 | - path: plugin.upload 15 | cmd: mvn clean 16 | ``` 17 | 18 | 所谓前置钩子也就是 `preHooks`,后置钩子为 `postHooks`。字段 `path` 为以点(.)链接的命令。 19 | 其中,钩子命令依照所配置的顺序执行。 20 | -------------------------------------------------------------------------------- /util/cmd_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var _ = Describe("Test open browser", func() { 11 | It("should success", func() { 12 | err := Open("fake://url", "", FakeExecCommandSuccess) 13 | Expect(err).To(BeNil()) 14 | }) 15 | }) 16 | 17 | // TestShellProcessSuccess only for test 18 | func TestShellProcessSuccess(t *testing.T) { 19 | if os.Getenv("GO_TEST_PROCESS") != "1" { 20 | return 21 | } 22 | //os.Exit(0) 23 | } 24 | -------------------------------------------------------------------------------- /docs/book/zh/doctor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 诊断 3 | weight: 900 4 | since: v0.0.24 5 | --- 6 | 7 | # 诊断 8 | 9 | 由于错误配置或者是缺少相应插件,可能会导致 `jcli` 无法正常工作。然而,有时候想要快速地找到问题所在, 是一件不容易而且费时的事情。这里要介绍的`诊断`功能,就是为了解决这样的问题而存在的。 10 | 11 | ## 插件依赖 12 | 13 | 例如,命令 `jcli job search` 要依赖插件 [pipeline-restful-api](https://plugins.jenkins.io/pipeline-restful-api)。其他部分插件也有类似的依赖。有的情况下,还对插件的版本有要求。 14 | 15 | 在执行命令时,如果发现无法使用,可以尝试使用诊断参数来检查是否缺少依赖: 16 | 17 | `jcli job search --doctor` 18 | 19 | 其中 `--doctor` 是一个全局参数。当有依赖不满足等情况发生时,会有相应的错误提示信息输出。例如: `Error: lack of plugin pipeline-restful-api`。 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin/**/jcli 15 | release/ 16 | jcli 17 | 18 | *.xml 19 | 20 | mock 21 | 22 | # goland 23 | .idea 24 | */**/.DS_Store 25 | .DS_Store 26 | 27 | # test files 28 | app/cmd/test.yaml 29 | 30 | # language 31 | #app/i18n/bindata.go 32 | app/i18n/jcli/zh_CN/LC_MESSAGES/jcli.mo 33 | app/i18n/jcli.pot 34 | 35 | # snap package 36 | *.snap -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: jcli 2 | version: git 3 | summary: Jenkins CLI allows you manage your Jenkins in an easy way. 4 | description: | 5 | Jenkins CLI allows you manage your Jenkins in an easy way. No matter if you're a plugin developer, administrator or just a regular user, it is made for you! 6 | 7 | confinement: devmode 8 | base: core18 9 | license: MIT 10 | grade: devel 11 | 12 | parts: 13 | jcli: 14 | plugin: dump 15 | source: https://cdn.jsdelivr.net/gh/jenkins-zh/jcli-repo/jcli-linux-amd64.tar.gz 16 | source-type: tar 17 | apps: 18 | jcli: 19 | command: jcli 20 | -------------------------------------------------------------------------------- /docs/book/en/proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Proxy" 3 | weight: 90 4 | --- 5 | 6 | Jenkins might be stay in behind a firewall. So we cannot connect it directly. You can give `jcli` a proxy setting. It's also very simple to support a proxy setting. You just need to execute: `jcli config edit`. Then find the item which you want to add a proxy. Like the below demo: 7 | 8 | ``` 9 | jenkins_servers: 10 | - name: dev 11 | url: http://192.168.1.10 12 | username: admin 13 | token: 11132c9ae4b20edbe56ac3e09cb5a3c8c2 14 | proxy: http://192.168.10.10:47586 15 | proxyAuth: username:password 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/book/zh/completion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 自动补全 3 | weight: 102 4 | --- 5 | 6 | # 自动补全 7 | 8 | 如果你已经在 mac 或 linux 上使用的是 `oh-my-zsh`,你可以尝试以下步骤: 9 | 10 | ```text 11 | # cd ~/.oh-my-zsh/plugins 12 | // 创建 incr 文件夹 13 | # mkdir incr 14 | // 下载 incr 插件 15 | # wget https://mimosa-pudica.net/src/incr-0.2.zsh 16 | // 对 incr 进行授权 17 | # chmod 777 ~/.oh-my-zsh/plugins/incr/incr-0.2.zsh 18 | # vim ~/.zshrc,然后在 “~/.zshrc” 文件中加入 “source ~/.oh-my-zsh/plugins/incr/incr-0.2.zsh”,保存退出 19 | // 更新配置 20 | # source ~/.zshrc 21 | ``` 22 | 23 | 接下来,就可以使用 jcli 的自动补全功能了,而且你可能发现不仅仅只有 jcli 可以自动补全,很多命令都可以自动补全了 24 | 25 | -------------------------------------------------------------------------------- /app/cmd/credential.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(credentialCmd) 11 | } 12 | 13 | var credentialCmd = &cobra.Command{ 14 | Use: "credential", 15 | Aliases: []string{"secret", "cred"}, 16 | Short: i18n.T("Manage the credentials of your Jenkins"), 17 | Long: i18n.T(`Manage the credentials of your Jenkins`), 18 | Annotations: map[string]string{ 19 | common.Since: common.VersionSince0024, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /app/cmd/plugin_formula_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/magiconair/properties/assert" 5 | "testing" 6 | 7 | jenkinsFormula "github.com/jenkins-zh/jenkins-formulas/pkg/common" 8 | ) 9 | 10 | func TestSortPlugins(t *testing.T) { 11 | plugins := []jenkinsFormula.Plugin{{ 12 | GroupId: "a", 13 | ArtifactId: "b", 14 | }, { 15 | GroupId: "a", 16 | ArtifactId: "a", 17 | }, { 18 | GroupId: "b", 19 | ArtifactId: "c", 20 | }} 21 | 22 | plugins = SortPlugins(plugins) 23 | assert.Equal(t, plugins[0].GroupId, "a") 24 | assert.Equal(t, plugins[0].ArtifactId, "a") 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/generator.yaml: -------------------------------------------------------------------------------- 1 | name: generator 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | if: "!contains(github.event.head_commit.message, 'ci skip')" 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Update readme 17 | uses: linuxsuren/yaml-readme@v0.0.17 18 | env: 19 | GH_TOKEN: ${{ secrets.GH_TOKEN_SECRETS }} 20 | with: 21 | pattern: '' 22 | username: linuxsuren 23 | org: jenkins-zh 24 | repo: jenkins-cli 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Unshallow 15 | run: git fetch --prune --unshallow 16 | - name: Set up Go 17 | uses: actions/setup-go@v5.2.0 18 | with: 19 | go-version: 1.23.x 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v6.1.0 22 | with: 23 | args: release --clean 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN_SECRETS }} 26 | -------------------------------------------------------------------------------- /e2e/withdependencies/job_test.go: -------------------------------------------------------------------------------- 1 | package withdependencies 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSearchJobs(t *testing.T) { 12 | cmd := exec.Command("jcli", "job", "search", "--url", GetJenkinsURL()) 13 | data, err := cmd.CombinedOutput() 14 | assert.Nil(t, err) 15 | 16 | fmt.Println(string(data)) 17 | } 18 | 19 | func TestCreateJob(t *testing.T) { 20 | cmd := exec.Command("jcli", "job", "create", "fake", 21 | "--type", "com.cloudbees.hudson.plugins.folder.Folder", "--url", GetJenkinsURL()) 22 | _, err := cmd.CombinedOutput() 23 | assert.Nil(t, err) 24 | } 25 | -------------------------------------------------------------------------------- /docs/book/zh/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 用户 3 | weight: 100 4 | --- 5 | 6 | # 用户 7 | 8 | `jcli` 可以完成用户的创建、删除以及生成令牌(Token)的操作, 9 | 10 | ## 创建用户 11 | 12 | ```text 13 | jcli user create [password] [flags] 14 | ``` 15 | 16 | 在创建用户的时候,可以指定一个密码或者随机生成。 17 | 18 | ## 生成令牌 19 | 20 | Jenkins 的 Web API 必须是通过令牌(Token)来访问,`jcli` 支持给当前用户或者 指定用户生成令牌。给当前用户生成令牌的命令如下: 21 | 22 | `jcli user token -g` 23 | 24 | 如果希望通过管理员给其他的 Jenkins 用户生成令牌的话,需要在启动 Jenkins 时给定一些参数, 具体参考下面的命令: 25 | 26 | ```text 27 | jcli center start --admin-can-generate-new-tokens 28 | jcli user token -g --target-user target-user-name 29 | ``` 30 | 31 | 上面的第一条命令会启动 Jenkins 并设置为允许有管理员权限的用户为其他用户生成令牌。 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/sonarqube.yaml.bak: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | types: [opened, synchronize, reopened] 7 | name: Sonarqube 8 | jobs: 9 | sonarcloud: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Test 14 | run: | 15 | export PATH=$PATH:${PWD}/bin:$GOPATH/bin:/home/runner/go/bin 16 | make test 17 | - name: SonarCloud Scan 18 | uses: sonarsource/sonarcloud-github-action@master 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | SONAR_TOKEN: 3caad1285eb0edf2b4f65ee07b3cd8edde6c5176 22 | -------------------------------------------------------------------------------- /util/logger.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | // InitLogger returns a logger 10 | func InitLogger(level string) (logger *zap.Logger, err error) { 11 | rawJSON := []byte(fmt.Sprintf(`{ 12 | "level": "%s", 13 | "encoding": "json", 14 | "outputPaths": ["stdout"], 15 | "errorOutputPaths": ["stderr"], 16 | "encoderConfig": { 17 | "messageKey": "message", 18 | "levelKey": "level", 19 | "levelEncoder": "lowercase" 20 | } 21 | }`, level)) 22 | var cfg zap.Config 23 | if err = json.Unmarshal(rawJSON, &cfg); err == nil { 24 | logger, err = cfg.Build() 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /docs/book/zh/credential.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 凭据 3 | weight: 101 4 | since: v0.0.24 5 | --- 6 | 7 | # 凭据 8 | 9 | 通过 `jcli` 可以在 Jenkins 上创建凭据(Credentials),下面介绍使用方法。 10 | 11 | ## 创建 12 | 13 | Jenkins 中的凭据有多种类型,下面的命令会创建一个用户名和密码类型的凭据: 14 | 15 | ```text 16 | jcli credential create --credential-username your-username \ 17 | --credential-password your-password --desc your-credential-remark 18 | ``` 19 | 20 | 下面的命令创建一个只包含单一加密文本的凭据: 21 | 22 | `jcli credential create --secret my-secret --type secret` 23 | 24 | ## 列表 25 | 26 | `jcli credential list` 27 | 28 | ## 删除 29 | 30 | 我们可以根据 Jenkins 凭据的唯一标示来删除: 31 | 32 | `jcli credential delete --id b0b0f865-f0c0-477c-a5ba-9fae88477f9e` 33 | 34 | -------------------------------------------------------------------------------- /app/cmd/common/cmd_valid.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | "os" 7 | ) 8 | 9 | // ExistsRegularFile returns a function to check if target file is a regular file 10 | func ExistsRegularFile(flagName string) cobra.PositionalArgs { 11 | return func(cmd *cobra.Command, args []string) (err error) { 12 | flag := cmd.Flag(flagName) 13 | val := flag.Value.String() 14 | 15 | switch val { 16 | case "": 17 | err = fmt.Errorf("argument '%s' cannot be empty", flagName) 18 | default: 19 | if _, err = os.Stat(val); os.IsNotExist(err) { 20 | err = fmt.Errorf("'%s' is not a regular file", val) 21 | } 22 | } 23 | return 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/cmd/common/completion.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | // NoFileCompletion avoid completion with files 6 | func NoFileCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { 7 | return nil, cobra.ShellCompDirectiveNoFileComp 8 | } 9 | 10 | // ArrayCompletion return a completion which base on an array 11 | func ArrayCompletion(array ...string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 12 | return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 13 | return array, cobra.ShellCompDirectiveNoFileComp 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/book/en/completion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "completion" 3 | weight: 102 4 | --- 5 | 6 | ## install auto-completion for zsh 7 | 8 | if you install iterm2 on your macOS or linux,and you use `oh-my-zsh`,you can follow the steps: 9 | 10 | ``` 11 | # cd ~/.oh-my-zsh/plugins 12 | // create incr folder 13 | # mkdir incr 14 | // download incr plugin 15 | # wget https://mimosa-pudica.net/src/incr-0.2.zsh 16 | // authorize incr 17 | # chmod 777 ~/.oh-my-zsh/plugins/incr/incr-0.2.zsh 18 | # vim ~/.zshrc, and insert "source ~/.oh-my-zsh/plugins/incr/incr-0.2.zsh" in the "~/.zshrc",save and quit 19 | // flush configuration 20 | # source ~/.zshrc 21 | ``` 22 | 23 | Then you can use auto-completion for jcli, maybe you will find other commands can also use it 24 | -------------------------------------------------------------------------------- /docs/book/zh/open.md: -------------------------------------------------------------------------------- 1 | # 打开浏览器 2 | 3 | 我们可以通过下面的命令快速地用浏览器打开 Jenkins: 4 | 5 | `jcli open` 6 | 7 | ## 浏览器设置 8 | 9 | 默认,`jcli` 会使用系统的缺省浏览器打开。但是,如果希望能用指定的浏览器打开的话,可以参考下面的命令: 10 | 11 | ``` 12 | jcli open --browser "Google-Chrome" 13 | JCLI_BROWSER="Google Chrome" jcli open 14 | ``` 15 | 16 | 也就是说,可以通过给定参数,或者设置环境变量的方式来指定浏览器。 17 | 18 | ## 其他地址 19 | 20 | 为了方便在浏览器中打开和某个 Jenkins 相关的服务,可以把服务地址添加到配置文件中,例如: 21 | 22 | ``` 23 | current: local 24 | jenkins_servers: 25 | - name: local 26 | url: http://localhost:8080 27 | username: admin 28 | token: '******' 29 | data: 30 | baidu: https://baidu.com 31 | jenkins: https://jenkins.io 32 | ``` 33 | 34 | 从上面的配置例子中能看到,字段 `data` 下添加了两个 `key-value`。如果要打开其中的一个地址的话,可以执行下面的命令: 35 | 36 | `jcli open .baidu` 37 | -------------------------------------------------------------------------------- /app/cmd/config_edit_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | //func TestEditConfig(t *testing.T) { 4 | // RunEditCommandTest(t, EditCommandTest{ 5 | // Procedure: func(c *expect.Console) { 6 | // c.ExpectString("Edit config item yourServer") 7 | // c.SendLine("") 8 | // go c.ExpectEOF() 9 | // time.Sleep(time.Millisecond) 10 | // c.Send("\x1b") 11 | // c.SendLine(":wq!") 12 | // }, 13 | // Test: func(stdio terminal.Stdio) (err error) { 14 | // rootOptions.ConfigFile = "test.yaml" 15 | // data, err := GenerateSampleConfig() 16 | // err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 17 | // 18 | // rootCmd.SetArgs([]string{"config", "edit"}) 19 | // configEditOption.Option.Stdio = stdio 20 | // _, err = rootCmd.ExecuteC() 21 | // return 22 | // }, 23 | // }) 24 | //} 25 | -------------------------------------------------------------------------------- /e2e/config_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "os/exec" 6 | "testing" 7 | ) 8 | 9 | func TestConfigList(t *testing.T) { 10 | cmd := exec.Command("jcli", "config", "list") 11 | _, err := cmd.CombinedOutput() 12 | assert.NotNil(t, err) 13 | } 14 | 15 | func TestConfigGenerate(t *testing.T) { 16 | cmd := exec.Command("jcli", "config", "generate", "-i=false") 17 | data, err := cmd.CombinedOutput() 18 | assert.Nil(t, err) 19 | assert.Contains(t, string(data), "jenkins_servers") 20 | } 21 | 22 | func TestShowCurrentConfig(t *testing.T) { 23 | cmd := exec.Command("jcli", "config") 24 | data, err := cmd.CombinedOutput() 25 | assert.NotNil(t, err) 26 | assert.Contains(t, string(data), "Error: no config file found or no current setting") 27 | } 28 | -------------------------------------------------------------------------------- /app/cmd/job.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 5 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // JobOption is the job cmd option 10 | type JobOption struct { 11 | cobra_ext.OutputOption 12 | } 13 | 14 | var jobOption JobOption 15 | 16 | func init() { 17 | rootCmd.AddCommand(jobCmd) 18 | jobCmd.PersistentFlags().StringVarP(&jobOption.Format, "output", "o", "json", "Format the output") 19 | } 20 | 21 | var jobCmd = &cobra.Command{ 22 | Use: "job", 23 | Short: i18n.T("Manage the job of your Jenkins"), 24 | Long: i18n.T(`Manage the job of your Jenkins 25 | Editing the pipeline job needs to install a plugin which is pipeline-restful-api 26 | https://plugins.jenkins.io/pipeline-restful-api`), 27 | } 28 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=jenkins-zh 2 | sonar.projectKey=jenkins-zh_jenkins-cli 3 | # this is the name and version displayed in the SonarCloud UI. 4 | sonar.projectName=jenkins-cli 5 | sonar.projectVersion=1.0 6 | 7 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 8 | # This property is optional if sonar.modules is set. 9 | sonar.sources=. 10 | 11 | # Encoding of the source code. Default is default system encoding 12 | #sonar.sourceEncoding=UTF-8 13 | 14 | sonar.go.exclusions=**/vendor/**,**/**/*_test.go,client/test_methods.go,app/i18n/bindata.go 15 | sonar.exclusions=**/vendor/**,**/**/*_test.go,client/test_methods.go,app/i18n/bindata.go 16 | sonar.coverage.exclusions=app/i18n/bindata.go 17 | 18 | sonar.go.coverage.reportPaths=coverage.out 19 | -------------------------------------------------------------------------------- /util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // GeneratePassword generates a password with the specific length 9 | func GeneratePassword(length int) string { 10 | if length <= 0 { 11 | return "" 12 | } 13 | 14 | rand.Seed(time.Now().UnixNano()) 15 | digits := "0123456789" 16 | specials := "~=+%^*/()[]{}/!@#$?|" 17 | all := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + 18 | "abcdefghijklmnopqrstuvwxyz" + 19 | digits + specials 20 | buf := make([]byte, length) 21 | buf[0] = digits[rand.Intn(len(digits))] 22 | buf[1] = specials[rand.Intn(len(specials))] 23 | for i := 2; i < length; i++ { 24 | buf[i] = all[rand.Intn(len(all))] 25 | } 26 | rand.Shuffle(len(buf), func(i, j int) { 27 | buf[i], buf[j] = buf[j], buf[i] 28 | }) 29 | return string(buf) 30 | } 31 | -------------------------------------------------------------------------------- /docs/book/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | * [Introduction](README.md) 4 | 5 | ## English 6 | 7 | * [Job](en/job.md) 8 | * [Plugin](en/plugin.md) 9 | * [Download](en/download.md) 10 | * [Credential](en/credential.md) 11 | * [Completion](en/completion.md) 12 | * [User](en/user.md) 13 | * [Diganose](en/doctor.md) 14 | * [Proxy setting](en/proxy.md) 15 | 16 | ## 中文 17 | 18 | * [介绍](zh/README.md) 19 | * [任务](zh/job.md) 20 | * [插件](zh/plugin.md) 21 | * [节点](zh/agent.md) 22 | * [打开浏览器](zh/open.md) 23 | * [下载](zh/download.md) 24 | * [凭据](zh/credential.md) 25 | * [自动补全](zh/completion.md) 26 | * [用户](zh/user.md) 27 | * [诊断](zh/doctor.md) 28 | * [代理设置](zh/proxy.md) 29 | * [命令钩子](zh/hooks.md) 30 | * [Shell](zh/shell.md) 31 | 32 | ## advanced 33 | 34 | * [Setup config file](advanced/setup-config-path.md) 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. windows] 28 | - Command Tool [e.g. cmd] 29 | - Version [e.g. v0.0.16] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release-v0-0-x.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release v0.0.x 3 | about: A release task tracking 4 | title: Release v0.0.x 5 | labels: chore 6 | assignees: '' 7 | 8 | --- 9 | 10 | We need to update package manage tools for [v0.0.x](https://github.com/jenkins-zh/jenkins-cli/releases/tag/v0.0.x) below: 11 | 12 | - [x] [Homebrew](https://github.com/jenkins-zh/homebrew-jcli) automatically 13 | - [x] [Scoop](https://github.com/ScoopInstaller/Main/) automatically 14 | - [ ] [jcli doc](https://github.com/jenkins-zh/jenkins-cli-doc) 15 | - [ ] Release notes PR on the WeChat is missing 16 | 17 | Below distributions are out-of-date: 18 | 19 | - [ ] [GoFish](https://github.com/fishworks/fish-food) 20 | - [ ] [Chocolatey](https://chocolatey.org/packages/jcli) 21 | - [ ] [Snapcraft](https://snapcraft.io/jcli) 22 | 23 | The previous release is #x 24 | -------------------------------------------------------------------------------- /docs/book/en/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Plugin" 3 | anchor: "plugin" 4 | weight: 70 5 | --- 6 | 7 | ## Plugin Management 8 | 9 | `jcli` allows you to search, download, install, uninstall or upload a plugin. 10 | 11 | First, please search a plugin by a keyword if you want to install it. You can get a plugin list by execute `jcli plugin search zh-cn`. You can install it with the plugin name. 12 | 13 | For example, you can install the Simplified Chinese Localization plugin by `jcli plugin install localization-zh-cn`. 14 | 15 | ### Download Plugins 16 | 17 | Some times, Jenkins just cannot connect with the offical Update Center. We can use the `download` sub-cmd to download all the plugins which're you need, then upload them. This command will take care of the dependencies of the plugin. 18 | 19 | You can try it: 20 | 21 | `jcli plugin download localization-zh-cn` 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Make sure that you've checked the boxes below before you submit PR:** 2 | 3 | ### Always 4 | 5 | - [ ] Make sure you are requesting to **pull a topic/feature/bugfix branch** (right side) and not your master branch! 6 | - [ ] Written well with PR title, we generate the [release notes](https://github.com/jenkins-zh/jenkins-cli/releases) base on that 7 | 8 | ### For the bug fixes or features only 9 | 10 | - [ ] [Quality Gate Passed](https://sonarcloud.io/dashboard?id=jenkins-zh_jenkins-cli). Change this URL to your PR. 11 | - [ ] The coverage is xxx on the new lines 12 | - [ ] I've tested it by manual in the following platform 13 | - [ ] MacOS 14 | - [ ] Linux 15 | - [ ] Windows 16 | - [ ] Unit Test covered 17 | - [ ] e2e Test covered 18 | 19 | 22 | -------------------------------------------------------------------------------- /app/health/core_test.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | // Opt only for test 9 | type Opt struct{} 10 | 11 | // Check only for test 12 | func (o *Opt) Check() error { 13 | return nil 14 | } 15 | 16 | var _ = Describe("test command health check interface", func() { 17 | var ( 18 | register CheckRegister 19 | ) 20 | 21 | BeforeEach(func() { 22 | register = CheckRegister{ 23 | Member: make(map[string]CommandHealth, 0), 24 | } 25 | }) 26 | 27 | It("basic test", func() { 28 | Expect(register.Member).NotTo(BeNil()) 29 | Expect(len(register.Member)).To(Equal(0)) 30 | }) 31 | 32 | Context("register a fake one", func() { 33 | It("should success", func() { 34 | register.Register("fake", &Opt{}) 35 | Expect(len(register.Member)).To(Equal(1)) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /app/cmd/config/root.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | goPlugin "github.com/linuxsuren/go-cli-plugin/pkg/cmd" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // NewConfigPluginCmd create a command as root of config plugin 12 | func NewConfigPluginCmd(opt *common.Option) (cmd *cobra.Command) { 13 | cmd = &cobra.Command{ 14 | Use: "plugin", 15 | Short: i18n.T("Manage plugins for jcli"), 16 | Long: i18n.T(`Manage plugins for jcli 17 | If you want to submit a plugin for jcli, please see also the following project. 18 | https://github.com/jenkins-zh/jcli-plugins`), 19 | Annotations: map[string]string{ 20 | common.Since: common.VersionSince0028, 21 | }, 22 | } 23 | 24 | goPlugin.AppendPluginCmd(cmd, "jenkins-zh", "jcli-plugins") 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /util/collect.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // MaxAndMin return the max and min number 8 | func MaxAndMin(data []float64) (max, min float64) { 9 | if len(data) > 0 { 10 | max, min = data[0], data[0] 11 | } 12 | 13 | for _, item := range data { 14 | if item < min { 15 | min = item 16 | } else if item > max { 17 | max = item 18 | } 19 | } 20 | return 21 | } 22 | 23 | // PrintCollectTrend print the trend of data 24 | func PrintCollectTrend(data []float64) (buf string) { 25 | max, min := MaxAndMin(data) 26 | 27 | unit := (max - min) / 100 28 | for _, num := range data { 29 | total := (int)(num / unit) 30 | if total == 0 { 31 | total = 1 32 | } 33 | arr := make([]int, total) 34 | for range arr { 35 | buf = fmt.Sprintf("%s*", buf) 36 | } 37 | buf = fmt.Sprintf("%s %.0f\n", buf, num) 38 | } 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /client/status_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | // PrepareGetStatus only for test 12 | func PrepareGetStatus(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) { 13 | request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/json", rootURL), nil) 14 | response := &http.Response{ 15 | StatusCode: 200, 16 | Header: http.Header{}, 17 | Request: request, 18 | Body: ioutil.NopCloser(bytes.NewBufferString(`{"nodeName":"master"}`)), 19 | } 20 | response.Header.Add("X-Jenkins", "version") 21 | roundTripper.EXPECT(). 22 | RoundTrip(NewRequestMatcher(request)).Return(response, nil) 23 | 24 | if user != "" && password != "" { 25 | request.SetBasicAuth(user, password) 26 | } 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /docs/book/zh/agent.md: -------------------------------------------------------------------------------- 1 | # 计算节点 2 | 3 | Jenkins 的最佳实践是让 master 只做调度任务,其他的构建等任务的执行都放在 agent(计算节点)上运行。 4 | 在安装不同插件后,使得 Jenkins 可以支持静态、动态类型的节点。所谓静态,指的是需要我们人工来维护,例如: 5 | 创建、上线、下线对应的节点。所谓动态,则可以根据既定的规则,自动地创建、销毁节点; 6 | 以 [Kubernetes 插件](https://github.com/jenkinsci/kubernetes-plugin/) 为例,它通过动态地创建 7 | 和销毁 [Pod](https://kubernetes.io/docs/concepts/workloads/pods/pod/) 来提供节点的运行。 8 | 9 | ## 协议 10 | 11 | 不管是动态还是静态的节点,都需要特定的协议来链接 agent 和 master。Jenkins 可以通过以下协议建立链接: 12 | * SSH 13 | * [JNLP](https://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/jnlp.html) 14 | * [WMI](https://en.wikipedia.org/wiki/Windows_Management_Instrumentation) 15 | 16 | 查看节点列表:`jcli agent list` 17 | 18 | ## 静态节点 19 | 20 | ``` 21 | jcli agent create macos 22 | jcli agent launch macos 23 | ``` 24 | 25 | 当前,只支持 JNLP 类型的节点创建。另外,对于需要通过 HTTP 代理才能链接到 Jenkins 的话,暂时不支持。 26 | 27 | ## 删除节点 28 | 29 | 给定节点的名称即可删除:`jcli agent delete macos` 30 | -------------------------------------------------------------------------------- /app/cmd/center_upgrade.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // CenterUpgradeOption option for upgrade Jenkins 13 | type CenterUpgradeOption struct { 14 | RoundTripper http.RoundTripper 15 | } 16 | 17 | var centerUpgradeOption CenterUpgradeOption 18 | 19 | func init() { 20 | centerCmd.AddCommand(centerUpgradeCmd) 21 | } 22 | 23 | var centerUpgradeCmd = &cobra.Command{ 24 | Use: "upgrade", 25 | Short: i18n.T("Upgrade your Jenkins"), 26 | Long: i18n.T("Upgrade your Jenkins"), 27 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 28 | jclient := &client.UpdateCenterManager{ 29 | JenkinsCore: client.JenkinsCore{ 30 | RoundTripper: centerUpgradeOption.RoundTripper, 31 | }, 32 | } 33 | getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) 34 | return jclient.Upgrade() 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /app/cmd/crumbIssuer.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // CrumbIssuerOptions contains the command line options 12 | type CrumbIssuerOptions struct { 13 | RoundTripper http.RoundTripper 14 | } 15 | 16 | func init() { 17 | rootCmd.AddCommand(crumbIssuerCmd) 18 | } 19 | 20 | var crumbIssuerOptions CrumbIssuerOptions 21 | 22 | var crumbIssuerCmd = &cobra.Command{ 23 | Use: "crumb", 24 | Short: "Print crumbIssuer of Jenkins", 25 | Long: `Print crumbIssuer of Jenkins`, 26 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 27 | jenkinsCore := &client.JenkinsCore{RoundTripper: crumbIssuerOptions.RoundTripper} 28 | getCurrentJenkinsAndClient(jenkinsCore) 29 | 30 | var crumb *client.JenkinsCrumb 31 | if crumb, err = jenkinsCore.GetCrumb(); err == nil && crumb != nil { 32 | cmd.Printf("%s=%s\n", crumb.CrumbRequestField, crumb.Crumb) 33 | } 34 | return 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /app/cmd/plugin_create_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/util" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | var _ = Describe("plugin create test", func() { 12 | var ( 13 | err error 14 | ) 15 | 16 | BeforeEach(func() { 17 | pluginCreateOptions.SystemCallExec = util.FakeSystemCallExecSuccess 18 | pluginCreateOptions.LookPathContext = util.FakeLookPath 19 | data, err := GenerateSampleConfig() 20 | Expect(err).To(BeNil()) 21 | rootOptions.ConfigFile = "test.yaml" 22 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 23 | Expect(err).To(BeNil()) 24 | }) 25 | 26 | JustBeforeEach(func() { 27 | rootCmd.SetArgs([]string{"plugin", "create", "--debug-output"}) 28 | _, err = rootCmd.ExecuteC() 29 | }) 30 | 31 | AfterEach(func() { 32 | os.Remove(rootOptions.ConfigFile) 33 | }) 34 | 35 | It("should success", func() { 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /app/cmd/plugin_release_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/util" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | var _ = Describe("plugin release test", func() { 12 | var ( 13 | err error 14 | ) 15 | 16 | BeforeEach(func() { 17 | pluginReleaseOptions.SystemCallExec = util.FakeSystemCallExecSuccess 18 | pluginReleaseOptions.LookPathContext = util.FakeLookPath 19 | data, err := GenerateSampleConfig() 20 | Expect(err).To(BeNil()) 21 | rootOptions.ConfigFile = "test.yaml" 22 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 23 | Expect(err).To(BeNil()) 24 | }) 25 | 26 | JustBeforeEach(func() { 27 | rootCmd.SetArgs([]string{"plugin", "release", "--debug-output"}) 28 | _, err = rootCmd.ExecuteC() 29 | }) 30 | 31 | AfterEach(func() { 32 | os.Remove(rootOptions.ConfigFile) 33 | }) 34 | 35 | It("should success", func() { 36 | Expect(err).NotTo(HaveOccurred()) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /app/cmd/computer_log.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // ComputerLogOption option for config list command 11 | type ComputerLogOption struct { 12 | common.Option 13 | } 14 | 15 | var computerLogOption ComputerLogOption 16 | 17 | func init() { 18 | computerCmd.AddCommand(computerLogCmd) 19 | } 20 | 21 | var computerLogCmd = &cobra.Command{ 22 | Use: "log ", 23 | Short: i18n.T("Output the log of the agent"), 24 | Long: i18n.T("Output the log of the agent"), 25 | ValidArgsFunction: ValidAgentNames, 26 | Args: cobra.MinimumNArgs(1), 27 | RunE: func(cmd *cobra.Command, args []string) (err error) { 28 | jClient, _ := GetComputerClient(computerLogOption.Option) 29 | 30 | var log string 31 | if log, err = jClient.GetLog(args[0]); err == nil { 32 | cmd.Print(log) 33 | } 34 | return 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /app/cmd/plugin_trend.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/app/helper" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // PluginTreadOption is the option of plugin trend command 13 | type PluginTreadOption struct { 14 | RoundTripper http.RoundTripper 15 | } 16 | 17 | var pluginTreadOption PluginTreadOption 18 | 19 | func init() { 20 | pluginCmd.AddCommand(pluginTrendCmd) 21 | } 22 | 23 | var pluginTrendCmd = &cobra.Command{ 24 | Use: "trend ", 25 | Short: "Show the trend of the plugin", 26 | Long: `Show the trend of the plugin`, 27 | Args: cobra.MinimumNArgs(1), 28 | Run: func(cmd *cobra.Command, args []string) { 29 | pluginName := args[0] 30 | 31 | jclient := &client.PluginAPI{ 32 | RoundTripper: pluginTreadOption.RoundTripper, 33 | } 34 | tread, err := jclient.ShowTrend(pluginName) 35 | if err == nil { 36 | cmd.Print(tread) 37 | } 38 | helper.CheckErr(cmd, err) 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /util/url.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/url" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | // URLJoin is a util function to join host URL and API URL 10 | func URLJoin(host, api string) (targetURL *url.URL, err error) { 11 | if targetURL, err = url.Parse(host); err == nil { 12 | pathURL, _ := url.Parse(path.Join(targetURL.Path, api)) 13 | targetURL = targetURL.ResolveReference(pathURL) 14 | } 15 | return 16 | } 17 | 18 | // URLJoinAsString is a util function to join host URL and API URL 19 | func URLJoinAsString(host, api string) (targetURLStr string, err error) { 20 | var targetURL *url.URL 21 | if targetURL, err = URLJoin(host, api); err == nil { 22 | targetURLStr = targetURL.String() 23 | } 24 | return 25 | } 26 | 27 | // ArraySplitAndDeleteEmpty split string and delete empty element 28 | func ArraySplitAndDeleteEmpty(s, sep string) []string { 29 | var r []string 30 | stringList := strings.Split(s, sep) 31 | for _, str := range stringList { 32 | if str != "" { 33 | r = append(r, str) 34 | } 35 | } 36 | return r 37 | } 38 | -------------------------------------------------------------------------------- /docs/book/en/job.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Job" 3 | weight: 80 4 | --- 5 | 6 | ## Job Management 7 | 8 | You can search a job list using a keyword, like this: `jcli job search input`. 9 | 10 | It's very simple to trigger a job. We have the batch mode and interactive mode. This command will finish immediately. 11 | 12 | `jcli job build "folderName jobName" -b` 13 | 14 | Once you triggered a job, then you can watch the log output by `jcli job log "zjproject zjproject-inputstep55" -w`. This command will always output the log of the last build. 15 | 16 | ## Proxy Support 17 | 18 | Jenkins might be stay in behind a firewall. So we cannot connect it directly. You can give `jcli` a proxy setting. It's also very simple to support a proxy setting. You just need to execute: `jcli config edit`. Then find the item which you want to add a proxy. Like the below demo: 19 | 20 | ``` 21 | jenkins_servers: 22 | - name: dev 23 | url: http://192.168.1.10 24 | username: admin 25 | token: 11132c9ae4b20edbe56ac3e09cb5a3c8c2 26 | proxy: http://192.168.10.10:47586 27 | proxyAuth: username:password 28 | ``` 29 | -------------------------------------------------------------------------------- /util/password_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("Password util test", func() { 10 | var ( 11 | ctrl *gomock.Controller 12 | length int 13 | ) 14 | 15 | BeforeEach(func() { 16 | ctrl = gomock.NewController(GinkgoT()) 17 | length = 3 18 | }) 19 | 20 | AfterEach(func() { 21 | ctrl.Finish() 22 | }) 23 | 24 | Context("basic test", func() { 25 | It("password length", func() { 26 | pass := GeneratePassword(length) 27 | Expect(len(pass)).To(Equal(length)) 28 | }) 29 | 30 | It("Different length", func() { 31 | length = 6 32 | pass := GeneratePassword(length) 33 | Expect(len(pass)).To(Equal(length)) 34 | }) 35 | 36 | It("Negative length", func() { 37 | length = -1 38 | pass := GeneratePassword(length) 39 | Expect(len(pass)).To(Equal(0)) 40 | }) 41 | 42 | It("Zero length", func() { 43 | length = 0 44 | pass := GeneratePassword(length) 45 | Expect(len(pass)).To(Equal(length)) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /app/cmd/center_start_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/util" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "io/ioutil" 8 | "os" 9 | ) 10 | 11 | var _ = Describe("center start command", func() { 12 | var ( 13 | configFile string 14 | ) 15 | BeforeEach(func() { 16 | file, err := ioutil.TempFile(".", "test.yaml") 17 | Expect(err).NotTo(HaveOccurred()) 18 | 19 | configFile = file.Name() 20 | data, err := GenerateSampleConfig() 21 | Expect(err).To(BeNil()) 22 | err = ioutil.WriteFile(configFile, data, 0664) 23 | Expect(err).To(BeNil()) 24 | 25 | rootOptions.ConfigFile = configFile 26 | }) 27 | AfterEach(func() { 28 | os.RemoveAll(configFile) 29 | }) 30 | It("enable mirror site", func() { 31 | centerStartOption.SystemCallExec = util.FakeSystemCallExecSuccess 32 | centerStartOption.LookPathContext = util.FakeLookPath 33 | rootCmd.SetArgs([]string{"center", "start", "--dry-run", "--env", "a=b", "--concurrent-indexing=12", "--https-enable"}) 34 | _, err := rootCmd.ExecuteC() 35 | Expect(err).To(BeNil()) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /app/cmd/computer_create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // ComputerCreateOption option for config list command 12 | type ComputerCreateOption struct { 13 | common.Option 14 | cobra_ext.OutputOption 15 | } 16 | 17 | var computerCreateOption ComputerCreateOption 18 | 19 | func init() { 20 | computerCmd.AddCommand(computerCreateCmd) 21 | } 22 | 23 | var computerCreateCmd = &cobra.Command{ 24 | Use: "create", 25 | Short: i18n.T("Create an Jenkins agent"), 26 | Long: i18n.T(`Create an Jenkins agent 27 | It can only create a JNLP agent.`), 28 | Example: `jcli agent create agent-name`, 29 | Args: cobra.MinimumNArgs(1), 30 | RunE: func(cmd *cobra.Command, args []string) (err error) { 31 | jClient, _ := GetComputerClient(computerCreateOption.Option) 32 | return jClient.Create(args[0]) 33 | }, 34 | Annotations: map[string]string{ 35 | common.Since: common.VersionSince0024, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /app/cmd/job_enable.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // JobEnableOption is the job delete option 11 | type JobEnableOption struct { 12 | common.BatchOption 13 | common.Option 14 | } 15 | 16 | var jobEnableOption JobEnableOption 17 | 18 | func init() { 19 | jobCmd.AddCommand(jobEnabelCmd) 20 | jobEnableOption.SetFlag(jobEnabelCmd) 21 | } 22 | 23 | var jobEnabelCmd = &cobra.Command{ 24 | Use: "enable", 25 | Short: i18n.T("Enable a job in your Jenkins"), 26 | Long: i18n.T("Enable a job in your Jenkins"), 27 | Args: cobra.MinimumNArgs(1), 28 | RunE: func(cmd *cobra.Command, args []string) (err error) { 29 | jobName := args[0] 30 | jclient := &client.JobClient{ 31 | JenkinsCore: client.JenkinsCore{ 32 | RoundTripper: jobEnableOption.RoundTripper, 33 | }, 34 | } 35 | getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) 36 | 37 | err = jclient.EnableJob(jobName) 38 | return 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /util/collect_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = Describe("collect test", func() { 9 | Context("MaxAndMin", func() { 10 | It("normal case, should success", func() { 11 | data := []float64{0.2, 0.3, 0.1, 0.4} 12 | max, min := MaxAndMin(data) 13 | Expect(max).To(Equal(0.4)) 14 | Expect(min).To(Equal(0.1)) 15 | return 16 | }) 17 | 18 | It("empty collect, should success", func() { 19 | data := []float64{} 20 | max, min := MaxAndMin(data) 21 | Expect(max).To(Equal(0.0)) 22 | Expect(min).To(Equal(0.0)) 23 | return 24 | }) 25 | 26 | It("only one item, should success", func() { 27 | data := []float64{0.3} 28 | max, min := MaxAndMin(data) 29 | Expect(max).To(Equal(0.3)) 30 | Expect(min).To(Equal(0.3)) 31 | return 32 | }) 33 | }) 34 | 35 | Context("PrintCollectTrend", func() { 36 | It("should success", func() { 37 | data := []float64{1512, 3472, 4385, 3981} 38 | buf := PrintCollectTrend(data) 39 | Expect(buf).NotTo(Equal("")) 40 | return 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Official documentation at http://goreleaser.com 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | binary: jcli 6 | goarch: 7 | - amd64 8 | - arm64 9 | goos: 10 | - windows 11 | - linux 12 | - darwin 13 | ignore: 14 | - goos: windows 15 | goarch: arm 16 | - goos: windows 17 | goarch: arm64 18 | - goos: darwin 19 | goarch: arm 20 | ldflags: 21 | - -X github.com/linuxsuren/cobra-extension/version.version={{.Version}} 22 | - -X github.com/linuxsuren/cobra-extension/version.commit={{.ShortCommit}} 23 | - -X github.com/linuxsuren/cobra-extension/version.date={{.Date}} 24 | - -w 25 | - -s 26 | dist: release 27 | archives: 28 | - name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}" 29 | format_overrides: 30 | - goos: windows 31 | format: zip 32 | files: 33 | - README.md 34 | - README-zh.md 35 | checksum: 36 | name_template: 'checksums.txt' 37 | snapshot: 38 | name_template: "{{ .Tag }}-next-{{.ShortCommit}}" 39 | changelog: 40 | sort: asc 41 | filters: 42 | exclude: 43 | - '^docs:' 44 | - '^test:' 45 | -------------------------------------------------------------------------------- /app/cmd/center_labels.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-client/pkg/core" 6 | "github.com/spf13/cobra" 7 | "net/http" 8 | ) 9 | 10 | type labelOption struct { 11 | RoundTripper http.RoundTripper 12 | } 13 | 14 | func newCenterLabelCommand() (cmd *cobra.Command) { 15 | opt := &labelOption{} 16 | cmd = &cobra.Command{ 17 | Use: "labels", 18 | Short: "Print all the labels of the Jenkins agents", 19 | RunE: opt.runE, 20 | } 21 | return 22 | } 23 | 24 | func (o *labelOption) runE(cmd *cobra.Command, args []string) (err error) { 25 | jClient := &core.Client{ 26 | JenkinsCore: core.JenkinsCore{ 27 | RoundTripper: o.RoundTripper, 28 | }, 29 | } 30 | getCurrentJenkinsAndClientV2(&(jClient.JenkinsCore)) 31 | var labelsRes *core.LabelsResponse 32 | if labelsRes, err = jClient.GetLabels(); err == nil { 33 | if labelsRes.Status == "ok" { 34 | for i := range labelsRes.Data { 35 | cmd.Println(labelsRes.Data[i].Label) 36 | } 37 | } else { 38 | err = fmt.Errorf("failed to get labels, status: %s", labelsRes.Status) 39 | } 40 | } 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /app/cmd/job_disable.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // JobDisableOption is the job delete option 11 | type JobDisableOption struct { 12 | common.BatchOption 13 | common.Option 14 | } 15 | 16 | var jobDisableOption JobDisableOption 17 | 18 | func init() { 19 | jobCmd.AddCommand(jobDisableCmd) 20 | jobDisableOption.SetFlag(jobDisableCmd) 21 | } 22 | 23 | var jobDisableCmd = &cobra.Command{ 24 | Use: "disable", 25 | Short: i18n.T("Disable a job in your Jenkins"), 26 | Long: i18n.T("Disable a job in your Jenkins"), 27 | Args: cobra.MinimumNArgs(1), 28 | RunE: func(cmd *cobra.Command, args []string) (err error) { 29 | jobName := args[0] 30 | jclient := &client.JobClient{ 31 | JenkinsCore: client.JenkinsCore{ 32 | RoundTripper: jobDisableOption.RoundTripper, 33 | }, 34 | } 35 | getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) 36 | 37 | err = jclient.DisableJob(jobName) 38 | return 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Zhao Xiaojie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/cmd/computer_delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // ComputerDeleteOption option for agent delete command 11 | type ComputerDeleteOption struct { 12 | common.Option 13 | } 14 | 15 | var computerDeleteOption ComputerDeleteOption 16 | 17 | func init() { 18 | computerCmd.AddCommand(computerDeleteCmd) 19 | } 20 | 21 | var computerDeleteCmd = &cobra.Command{ 22 | Use: "delete", 23 | Aliases: common.GetAliasesDel(), 24 | Short: i18n.T("Delete an agent from Jenkins"), 25 | Long: i18n.T("Delete an agent from Jenkins"), 26 | Args: cobra.MinimumNArgs(1), 27 | ValidArgsFunction: ValidAgentNames, 28 | Example: `jcli agent delete agent-name`, 29 | RunE: func(cmd *cobra.Command, args []string) (err error) { 30 | jClient, _ := GetComputerClient(computerDeleteOption.Option) 31 | return jClient.Delete(args[0]) 32 | }, 33 | Annotations: map[string]string{ 34 | common.Since: common.VersionSince0024, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /e2e/job_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "os/exec" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestListJobType(t *testing.T) { 15 | cmd := exec.Command("jcli", "job", "type", "--url", GetJenkinsURL()) 16 | fmt.Println(cmd.String()) 17 | data, err := cmd.CombinedOutput() 18 | fmt.Println(string(data)) 19 | assert.Nil(t, err) 20 | 21 | rand.Seed(math.MaxInt8) 22 | name := fmt.Sprintf("%d", rand.Int()) 23 | cmd = exec.Command("jcli", "job", "create", name, "--type", "hudson.model.FreeStyleProject", "--url", GetJenkinsURL(), "--logger-level", "debug") 24 | data, err = cmd.CombinedOutput() 25 | fmt.Println(string(data)) 26 | assert.Nil(t, err) 27 | 28 | cmd = exec.Command("jcli", "job", "build", name, "-b", "--url", GetJenkinsURL()) 29 | data, err = cmd.CombinedOutput() 30 | fmt.Println(string(data)) 31 | assert.Nil(t, err) 32 | time.Sleep(time.Second * 6) 33 | 34 | cmd = exec.Command("jcli", "job", "history", name, "-d", "1", "--url", GetJenkinsURL()) 35 | data, err = cmd.CombinedOutput() 36 | fmt.Println(string(data)) 37 | assert.Nil(t, err) 38 | } 39 | -------------------------------------------------------------------------------- /app/cmd/plugin_uninstall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 5 | "net/http" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/app/helper" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/client" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // PluginUninstallOption the option of uninstall a plugin 14 | type PluginUninstallOption struct { 15 | RoundTripper http.RoundTripper 16 | } 17 | 18 | var pluginUninstallOption PluginUninstallOption 19 | 20 | func init() { 21 | pluginCmd.AddCommand(pluginUninstallCmd) 22 | } 23 | 24 | var pluginUninstallCmd = &cobra.Command{ 25 | Use: "uninstall [pluginName]", 26 | Short: i18n.T("Uninstall the plugins"), 27 | Long: i18n.T("Uninstall the plugins"), 28 | Args: cobra.MinimumNArgs(1), 29 | Run: func(cmd *cobra.Command, args []string) { 30 | pluginName := args[0] 31 | jclient := &client.PluginManager{ 32 | JenkinsCore: client.JenkinsCore{ 33 | RoundTripper: pluginUninstallOption.RoundTripper, 34 | }, 35 | } 36 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 37 | 38 | err := jclient.UninstallPlugin(pluginName) 39 | helper.CheckErr(cmd, err) 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /client/common_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 10 | ) 11 | 12 | // PrepareForGetIssuer only for test 13 | func PrepareForGetIssuer(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) ( 14 | request *http.Request, response *http.Response) { 15 | request, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", rootURL, "/crumbIssuer/api/json"), nil) 16 | response = &http.Response{ 17 | StatusCode: 200, 18 | Request: request, 19 | Body: ioutil.NopCloser(bytes.NewBufferString(`{"CrumbRequestField":"CrumbRequestField","Crumb":"Crumb"}`)), 20 | } 21 | roundTripper.EXPECT(). 22 | RoundTrip(NewRequestMatcher(request)).Return(response, nil) 23 | if user != "" && password != "" { 24 | request.SetBasicAuth(user, password) 25 | } 26 | return 27 | } 28 | 29 | // PrepareForGetIssuerWith500 only for test 30 | func PrepareForGetIssuerWith500(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) { 31 | _, response := PrepareForGetIssuer(roundTripper, rootURL, user, password) 32 | response.StatusCode = 500 33 | } 34 | -------------------------------------------------------------------------------- /client/pluginManager_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 10 | ) 11 | 12 | // PrepareForOneInstalledPluginWithPluginName only for test 13 | func PrepareForOneInstalledPluginWithPluginName(roundTripper *mhttp.MockRoundTripper, rootURL, pluginName string) ( 14 | request *http.Request, response *http.Response) { 15 | request, response = PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper, rootURL, pluginName, "1.0") 16 | return 17 | } 18 | 19 | // PrepareForOneInstalledPluginWithPluginNameAndVer only for test 20 | func PrepareForOneInstalledPluginWithPluginNameAndVer(roundTripper *mhttp.MockRoundTripper, rootURL, 21 | pluginName, version string) ( 22 | request *http.Request, response *http.Response) { 23 | request, response = PrepareForEmptyInstalledPluginList(roundTripper, rootURL, 1) 24 | response.Body = ioutil.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{ 25 | "plugins": [{ 26 | "shortName": "%s", 27 | "version": "%s", 28 | "hasUpdate": true, 29 | "enable": true, 30 | "active": true 31 | }] 32 | }`, pluginName, version))) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /client/requestmatcher_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | "net/http" 7 | ) 8 | 9 | var _ = Describe("user test", func() { 10 | Context("matchHeader", func() { 11 | var ( 12 | left http.Header 13 | right http.Header 14 | ) 15 | 16 | BeforeEach(func() { 17 | left = http.Header{} 18 | right = http.Header{} 19 | }) 20 | 21 | It("two empty headers", func() { 22 | Expect(matchHeader(left, right)).To(Equal(true)) 23 | }) 24 | 25 | It("two same header with data", func() { 26 | left.Add("a", "a") 27 | right.Add("a", "a") 28 | 29 | Expect(matchHeader(left, right)).To(Equal(true)) 30 | }) 31 | 32 | It("different length of headers", func() { 33 | right.Add("a", "a") 34 | 35 | Expect(matchHeader(left, right)).To(Equal(false)) 36 | }) 37 | 38 | It("different value of headers", func() { 39 | right.Add("a", "a") 40 | left.Add("a", "b") 41 | 42 | Expect(matchHeader(left, right)).To(Equal(false)) 43 | }) 44 | 45 | It("different key of headers", func() { 46 | right.Add("a", "a") 47 | left.Add("b", "a") 48 | 49 | Expect(matchHeader(left, right)).To(Equal(false)) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /app/cmd/job_history_edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/client" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type jobHistoryEdit struct { 9 | displayName string 10 | description string 11 | id int 12 | } 13 | 14 | func createJobHistoryEditCmd() (cmd *cobra.Command) { 15 | opt := &jobHistoryEdit{} 16 | cmd = &cobra.Command{ 17 | Use: "edit", 18 | Short: "Edit job history", 19 | RunE: opt.RunE, 20 | Args: cobra.MinimumNArgs(1), 21 | } 22 | 23 | flags := cmd.Flags() 24 | flags.StringVarP(&opt.displayName, "displayName", "d", "", "Display name of target job history") 25 | flags.StringVarP(&opt.description, "description", "m", "", "Description of target job history") 26 | flags.IntVarP(&opt.id, "id", "i", -1, "ID of job history") 27 | return 28 | } 29 | 30 | func (o *jobHistoryEdit) RunE(cmd *cobra.Command, args []string) (err error) { 31 | jobName := args[0] 32 | 33 | jClient := &client.JobClient{ 34 | JenkinsCore: client.JenkinsCore{ 35 | RoundTripper: jobHistoryOption.RoundTripper, 36 | }, 37 | } 38 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 39 | 40 | err = jClient.EditBuild(jobName, o.id, o.displayName, o.description) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /app/cmd/queue_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("queue command", func() { 14 | var ( 15 | ctrl *gomock.Controller 16 | ) 17 | 18 | BeforeEach(func() { 19 | ctrl = gomock.NewController(GinkgoT()) 20 | rootCmd.SetArgs([]string{}) 21 | rootOptions.Jenkins = "" 22 | rootOptions.ConfigFile = "test.yaml" 23 | }) 24 | 25 | AfterEach(func() { 26 | rootCmd.SetArgs([]string{}) 27 | os.Remove(rootOptions.ConfigFile) 28 | rootOptions.ConfigFile = "" 29 | ctrl.Finish() 30 | }) 31 | 32 | Context("without http requests", func() { 33 | It("should success", func() { 34 | data, err := GenerateSampleConfig() 35 | Expect(err).To(BeNil()) 36 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 37 | Expect(err).To(BeNil()) 38 | 39 | rootCmd.SetArgs([]string{"queue"}) 40 | 41 | buf := new(bytes.Buffer) 42 | rootCmd.SetOutput(buf) 43 | _, err = rootCmd.ExecuteC() 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(buf.String()).To(ContainSubstring("Manage the queue of your Jenkins")) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 3 | tag-template: 'v$NEXT_PATCH_VERSION' 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | - 'regression' 17 | - 'kind/bug' 18 | - title: 📝 Documentation updates 19 | labels: 20 | - 'documentation' 21 | - 'kind/doc' 22 | - title: 👻 Maintenance 23 | labels: 24 | - chore 25 | - 'kind/chore' 26 | - dependencies 27 | - title: 🚦 Tests 28 | labels: 29 | - test 30 | - 'kind/test' 31 | - tests 32 | exclude-labels: 33 | - reverted 34 | - no-changelog 35 | - skip-changelog 36 | - invalid 37 | change-template: '* $TITLE (#$NUMBER) @$AUTHOR' 38 | replacers: 39 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' 40 | replace: '' 41 | template: | 42 | ## What’s Changed 43 | 44 | $CHANGES 45 | 46 | Thanks again to $CONTRIBUTORS! 🎉 -------------------------------------------------------------------------------- /app/cmd/doc_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/golang/mock/gomock" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("doc command test", func() { 15 | var ( 16 | ctrl *gomock.Controller 17 | ) 18 | 19 | BeforeEach(func() { 20 | ctrl = gomock.NewController(GinkgoT()) 21 | config = nil 22 | 23 | rootOptions.ConfigFile = "test.yaml" 24 | 25 | data, err := GenerateSampleConfig() 26 | Expect(err).To(BeNil()) 27 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 28 | Expect(err).To(BeNil()) 29 | }) 30 | 31 | AfterEach(func() { 32 | config = nil 33 | ctrl.Finish() 34 | }) 35 | 36 | Context("basic test", func() { 37 | It("should success", func() { 38 | buf := new(bytes.Buffer) 39 | rootCmd.SetOutput(buf) 40 | 41 | tmpdir := os.TempDir() 42 | defer os.RemoveAll(tmpdir) 43 | 44 | rootCmd.SetArgs([]string{"doc", tmpdir}) 45 | _, err := rootCmd.ExecuteC() 46 | Expect(err).NotTo(HaveOccurred()) 47 | Expect(buf.String()).To(Equal("")) 48 | 49 | _, err = os.Stat(filepath.Join(tmpdir, "jcli_doc.md")) 50 | Expect(err).NotTo(HaveOccurred()) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /e2e/setup_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "github.com/phayes/freeport" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "testing" 10 | ) 11 | 12 | var jenkinsURL string 13 | 14 | func GetJenkinsURL() string { 15 | return jenkinsURL 16 | } 17 | 18 | func TestMain(m *testing.M) { 19 | var err error 20 | 21 | version := os.Getenv("JENKINS_VERSION") 22 | os.Setenv("PATH", ".:"+os.Getenv("PATH")) 23 | 24 | javaHome := os.Getenv("JCLI_JAVA_HOME") 25 | if javaHome != "" { 26 | os.Setenv("PATH", javaHome+"/bin:"+os.Getenv("PATH")) 27 | } 28 | if err = os.Setenv("JCLI_CONFIG_LOAD", "false"); err != nil { 29 | panic(err) 30 | } 31 | if version == "" { 32 | return 33 | } 34 | 35 | var port int 36 | if port, err = freeport.GetFreePort(); err != nil { 37 | fmt.Println("get free port error", err) 38 | panic(err) 39 | } 40 | jenkinsURL = fmt.Sprintf("http://%s:%d", GetLocalIP(), port) 41 | 42 | cmd := exec.Command("jcli", "center", "start", "--random-web-dir", "--setup-wizard=false", 43 | "--port", fmt.Sprintf("%d", port), "--version", version, "--thread", "10", "--clean-home") 44 | fmt.Println(cmd.String()) 45 | RunAndWait(cmd, func(reader io.ReadCloser) { 46 | WaitJenkinsRunningUp(reader) 47 | 48 | m.Run() 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /app/cmd/plugin.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // PluginOptions contains the command line options 12 | type PluginOptions struct { 13 | common.Option 14 | 15 | Suite string 16 | } 17 | 18 | var pluginOpt PluginOptions 19 | 20 | func init() { 21 | rootCmd.AddCommand(pluginCmd) 22 | } 23 | 24 | var pluginCmd = &cobra.Command{ 25 | Use: "plugin", 26 | Short: i18n.T("Manage the plugins of Jenkins"), 27 | Long: i18n.T("Manage the plugins of Jenkins"), 28 | Example: ` jcli plugin list 29 | jcli plugin search github 30 | jcli plugin check`, 31 | } 32 | 33 | // FindPlugin find a plugin by name 34 | func (o *PluginOptions) FindPlugin(name string) (plugin *client.InstalledPlugin, err error) { 35 | jClient := &client.PluginManager{ 36 | JenkinsCore: client.JenkinsCore{ 37 | RoundTripper: o.RoundTripper, 38 | }, 39 | } 40 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 41 | if plugin, err = jClient.FindInstalledPlugin(name); err == nil && plugin == nil { 42 | err = fmt.Errorf(fmt.Sprintf("lack of plugin %s", name)) 43 | } 44 | return 45 | } 46 | -------------------------------------------------------------------------------- /app/cmd/user_create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // UserCreateOption is user create cmd option 11 | type UserCreateOption struct { 12 | RoundTripper http.RoundTripper 13 | } 14 | 15 | var userCreateOption UserCreateOption 16 | 17 | func init() { 18 | userCmd.AddCommand(userCreateCmd) 19 | } 20 | 21 | var userCreateCmd = &cobra.Command{ 22 | Use: "create [password]", 23 | Short: "Create a user for your Jenkins", 24 | Long: `Create a user for your Jenkins`, 25 | Args: cobra.MinimumNArgs(1), 26 | Run: func(cmd *cobra.Command, args []string) { 27 | username := args[0] 28 | 29 | var password string 30 | if len(args) >= 2 { 31 | password = args[1] 32 | } 33 | 34 | jclient := &client.UserClient{ 35 | JenkinsCore: client.JenkinsCore{ 36 | RoundTripper: userCreateOption.RoundTripper, 37 | Debug: rootOptions.Debug, 38 | }, 39 | } 40 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 41 | 42 | if user, err := jclient.Create(username, password); err == nil { 43 | cmd.Println("create user success. Password is:", user.Password1) 44 | } else { 45 | cmd.PrintErrln(err) 46 | } 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /app/cmd/center_list.txt: -------------------------------------------------------------------------------- 1 | +-----------------------------------------------------------------------------------------------------------------------------------------+ 2 | | JENKINS LTS CHANGELOG | 3 | +-------------------------+-------------------------+-----------------------------------------------------------+-------------------------+ 4 | | Index | Title | Description | PubDate | 5 | | 1 | Jenkins 2.289.2 | Security: Important security fixes. | Wed, 30 Jun 2021 | 6 | | | | RFE: Winstone 5.18: Update Jetty from 9.4.39.v20210325 to | | 7 | | | | 9.4.41.v20210516 for bug fixes and enhancements. | | 8 | | | | | | 9 | +-------------------------+-------------------------+-----------------------------------------------------------+-------------------------+ -------------------------------------------------------------------------------- /app/cmd/center_identity.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // CenterIdentityOption option for upgrade Jenkins 13 | type CenterIdentityOption struct { 14 | common.Option 15 | } 16 | 17 | var centerIdentityOption CenterIdentityOption 18 | 19 | func init() { 20 | centerCmd.AddCommand(centerIdentityCmd) 21 | } 22 | 23 | var centerIdentityCmd = &cobra.Command{ 24 | Use: "identity", 25 | Short: i18n.T("Print the identity of current Jenkins"), 26 | Long: i18n.T("Print the identity of current Jenkins"), 27 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 28 | jClient := &client.CoreClient{ 29 | JenkinsCore: client.JenkinsCore{ 30 | RoundTripper: centerIdentityOption.RoundTripper, 31 | }, 32 | } 33 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 34 | 35 | var identity client.JenkinsIdentity 36 | var data []byte 37 | if identity, err = jClient.GetIdentity(); err == nil { 38 | if data, err = json.MarshalIndent(identity, "", " "); err == nil { 39 | cmd.Println(string(data)) 40 | } 41 | } 42 | return 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /app/cmd/user.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/helper" 5 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 6 | "net/http" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // UserOption is the user cmd option 13 | type UserOption struct { 14 | cobra_ext.OutputOption 15 | 16 | RoundTripper http.RoundTripper 17 | } 18 | 19 | var userOption UserOption 20 | 21 | func init() { 22 | rootCmd.AddCommand(userCmd) 23 | userCmd.Flags().StringVarP(&userOption.Format, "output", "o", "json", "Format the output") 24 | } 25 | 26 | var userCmd = &cobra.Command{ 27 | Use: "user", 28 | Short: "Print the user of your Jenkins", 29 | Long: `Print the user of your Jenkins`, 30 | Run: func(cmd *cobra.Command, _ []string) { 31 | jclient := &client.UserClient{ 32 | JenkinsCore: client.JenkinsCore{ 33 | RoundTripper: userOption.RoundTripper, 34 | Debug: rootOptions.Debug, 35 | }, 36 | } 37 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 38 | 39 | status, err := jclient.Get() 40 | if err == nil { 41 | data, err := userOption.Output(status) 42 | if err == nil { 43 | cmd.Println(string(data)) 44 | } 45 | } 46 | helper.CheckErr(cmd, err) 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /client/queue_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("queue test", func() { 11 | var ( 12 | ctrl *gomock.Controller 13 | roundTripper *mhttp.MockRoundTripper 14 | queueClient QueueClient 15 | ) 16 | 17 | BeforeEach(func() { 18 | ctrl = gomock.NewController(GinkgoT()) 19 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 20 | queueClient = QueueClient{} 21 | queueClient.RoundTripper = roundTripper 22 | queueClient.URL = "http://localhost" 23 | }) 24 | 25 | AfterEach(func() { 26 | ctrl.Finish() 27 | }) 28 | 29 | Context("get queue", func() { 30 | It("should success", func() { 31 | PrepareGetQueue(roundTripper, queueClient.URL, "", "") 32 | 33 | queue, err := queueClient.Get() 34 | Expect(err).To(BeNil()) 35 | Expect(queue).NotTo(BeNil()) 36 | Expect(len(queue.Items)).To(Equal(1)) 37 | Expect(queue.Items[0].ID).To(Equal(62)) 38 | }) 39 | }) 40 | 41 | Context("cancel", func() { 42 | It("should success", func() { 43 | PrepareCancelQueue(roundTripper, queueClient.URL, "", "") 44 | 45 | err := queueClient.Cancel(1) 46 | Expect(err).To(BeNil()) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /client/status_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("status test", func() { 11 | var ( 12 | ctrl *gomock.Controller 13 | roundTripper *mhttp.MockRoundTripper 14 | statusClient JenkinsStatusClient 15 | 16 | username string 17 | password string 18 | ) 19 | 20 | BeforeEach(func() { 21 | ctrl = gomock.NewController(GinkgoT()) 22 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 23 | statusClient = JenkinsStatusClient{} 24 | statusClient.RoundTripper = roundTripper 25 | statusClient.URL = "http://localhost" 26 | 27 | username = "admin" 28 | password = "token" 29 | }) 30 | 31 | AfterEach(func() { 32 | ctrl.Finish() 33 | }) 34 | 35 | Context("Get status", func() { 36 | It("should success", func() { 37 | statusClient.UserName = username 38 | statusClient.Token = password 39 | 40 | PrepareGetStatus(roundTripper, statusClient.URL, username, password) 41 | 42 | status, err := statusClient.Get() 43 | Expect(err).To(BeNil()) 44 | Expect(status).NotTo(BeNil()) 45 | Expect(status.NodeName).To(Equal("master")) 46 | Expect(status.Version).To(Equal("version")) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /app/cmd/config_select.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // ConfigSelectOptions is the option for select a config 10 | type ConfigSelectOptions struct { 11 | common.Option 12 | } 13 | 14 | func init() { 15 | configCmd.AddCommand(configSelectCmd) 16 | configSelectOptions.Stdio = common.GetSystemStdio() 17 | } 18 | 19 | var configSelectOptions ConfigSelectOptions 20 | 21 | var configSelectCmd = &cobra.Command{ 22 | Use: "select", 23 | Short: i18n.T("Select one config as current Jenkins"), 24 | Long: i18n.T("Select one config as current Jenkins"), 25 | ValidArgsFunction: ValidJenkinsNames, 26 | RunE: func(_ *cobra.Command, args []string) (err error) { 27 | var jenkinsName string 28 | if len(args) > 0 { 29 | jenkinsName = args[0] 30 | } else { 31 | target := "" 32 | if currentJenkins := getCurrentJenkins(); currentJenkins != nil { 33 | target = currentJenkins.Name 34 | } 35 | 36 | jenkinsName, err = configSelectOptions.Select(getJenkinsNames(), 37 | "Choose a Jenkins as the current one:", target) 38 | } 39 | 40 | if err == nil { 41 | setCurrentJenkins(jenkinsName) 42 | } 43 | return 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /e2e/computer_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestListComputers(t *testing.T) { 13 | cmd := exec.Command("jcli", "computer", "list", "--url", GetJenkinsURL()) 14 | data, err := cmd.CombinedOutput() 15 | assert.Nil(t, err) 16 | fmt.Println(string(data)) 17 | 18 | cmd = exec.Command("jcli", "computer", "create", "go", "--url", GetJenkinsURL()) 19 | data, err = cmd.CombinedOutput() 20 | assert.Nil(t, err) 21 | fmt.Println(string(data)) 22 | 23 | // test agent commands with docker mode 24 | if containerIsReady() { 25 | cmd = exec.Command("jcli", "computer", "launch", "go", "--agent-type", "golang", "-m", "docker", "--url", GetJenkinsURL()) 26 | RunAndWait(cmd, func(reader io.ReadCloser) { 27 | WaitAgentRunningUp(reader) 28 | }) 29 | 30 | cmd = exec.Command("jcli", "computer", "delete", "go", "--url", GetJenkinsURL()) 31 | data, err = cmd.CombinedOutput() 32 | assert.Nil(t, err) 33 | fmt.Println(string(data)) 34 | } 35 | } 36 | 37 | func containerIsReady() bool { 38 | var err error 39 | if _, err = exec.LookPath("docker"); err == nil { 40 | // only run these tests when the docker exists 41 | _, err = exec.Command("docker", "ps").CombinedOutput() 42 | } 43 | return err == nil 44 | } 45 | -------------------------------------------------------------------------------- /app/cmd/queue_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 5 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 6 | "net/http" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // QueueListOption represents the option of queue list command 13 | type QueueListOption struct { 14 | cobra_ext.OutputOption 15 | 16 | RoundTripper http.RoundTripper 17 | } 18 | 19 | var queueListOption QueueListOption 20 | 21 | func init() { 22 | queueCmd.AddCommand(queueListCmd) 23 | queueListOption.SetFlagWithHeaders(queueListCmd, "ID,Why,URL") 24 | } 25 | 26 | var queueListCmd = &cobra.Command{ 27 | Use: "list", 28 | Short: i18n.T("Print the queue of your Jenkins"), 29 | Long: i18n.T("Print the queue of your Jenkins"), 30 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 31 | jClient := &client.QueueClient{ 32 | JenkinsCore: client.JenkinsCore{ 33 | RoundTripper: queueListOption.RoundTripper, 34 | Debug: rootOptions.Debug, 35 | }, 36 | } 37 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 38 | 39 | var jobQueue *client.JobQueue 40 | if jobQueue, err = jClient.Get(); err == nil { 41 | queueListOption.Writer = cmd.OutOrStdout() 42 | err = queueListOption.OutputV2(jobQueue.Items) 43 | } 44 | return 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /client/status.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // AgentLabel represents the label of Jenkins agent 8 | type AgentLabel struct { 9 | Name string 10 | } 11 | 12 | // View represents the view of Jenkins 13 | type View struct { 14 | Name string 15 | URL string 16 | } 17 | 18 | // JenkinsStatus holds the status of Jenkins 19 | type JenkinsStatus struct { 20 | AssignedLabels []AgentLabel 21 | Description string 22 | Jobs []Job 23 | Mode string 24 | NodeDescription string 25 | NodeName string 26 | NumExecutors int 27 | PrimaryView View 28 | QuietingDown bool 29 | SlaveAgentPort int 30 | UseCrumbs bool 31 | UseSecurity bool 32 | Views []View 33 | Version string 34 | } 35 | 36 | // JenkinsStatusClient use to connect with Jenkins status 37 | type JenkinsStatusClient struct { 38 | JenkinsCore 39 | } 40 | 41 | // Get returns status of Jenkins 42 | func (q *JenkinsStatusClient) Get() (status *JenkinsStatus, err error) { 43 | status = &JenkinsStatus{} 44 | var response *http.Response 45 | response, err = q.RequestWithResponseHeader(http.MethodGet, "/api/json", nil, nil, status) 46 | if err == nil { 47 | if ver, ok := response.Header["X-Jenkins"]; ok && len(ver) > 0 { 48 | status.Version = ver[0] 49 | } 50 | } 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /e2e/without_jenkins/completion_test.go: -------------------------------------------------------------------------------- 1 | package withoutjenkins 2 | 3 | import ( 4 | "os/exec" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestBashCompletion(t *testing.T) { 11 | cmd := exec.Command("jcli", "completion") 12 | data, err := cmd.CombinedOutput() 13 | assert.Nil(t, err) 14 | assert.Contains(t, string(data), "bash completion for jcli") 15 | 16 | // with options 17 | cmd = exec.Command("jcli", "completion", "--type", "bash") 18 | data, err = cmd.CombinedOutput() 19 | assert.Nil(t, err) 20 | assert.Contains(t, string(data), "bash completion for jcli") 21 | } 22 | 23 | func TestZshCompletion(t *testing.T) { 24 | cmd := exec.Command("jcli", "completion", "--type", "zsh") 25 | data, err := cmd.CombinedOutput() 26 | assert.Nil(t, err) 27 | assert.Contains(t, string(data), "#compdef _jcli jcli") 28 | } 29 | 30 | func TestPowerShellCompletion(t *testing.T) { 31 | cmd := exec.Command("jcli", "completion", "--type", "powerShell") 32 | data, err := cmd.CombinedOutput() 33 | assert.Nil(t, err) 34 | assert.Contains(t, string(data), "powershell completion for jcli") 35 | } 36 | 37 | func TestFishCompletion(t *testing.T) { 38 | cmd := exec.Command("jcli", "completion", "--type", "fish") 39 | data, err := cmd.CombinedOutput() 40 | assert.Nil(t, err) 41 | assert.Contains(t, string(data), "fish completion for jcli") 42 | } 43 | -------------------------------------------------------------------------------- /app/cmd/config_edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 8 | 9 | "io/ioutil" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // ConfigEditOption is the option for edit config command 15 | type ConfigEditOption struct { 16 | common.Option 17 | } 18 | 19 | var configEditOption ConfigEditOption 20 | 21 | func init() { 22 | configCmd.AddCommand(configEditCmd) 23 | configEditOption.Stdio = common.GetSystemStdio() 24 | } 25 | 26 | var configEditCmd = &cobra.Command{ 27 | Use: "edit", 28 | Short: i18n.T("Edit a Jenkins config"), 29 | Long: i18n.T(fmt.Sprintf(`Edit a Jenkins config 30 | %s`, common.GetEditorHelpText())), 31 | RunE: func(_ *cobra.Command, _ []string) (err error) { 32 | current := GetCurrentJenkinsFromOptions() 33 | configPath := configOptions.ConfigFileLocation 34 | 35 | var data []byte 36 | if data, err = ioutil.ReadFile(configPath); err == nil { 37 | content := string(data) 38 | //Help: fmt.Sprintf("Config file path: %s", configPath), 39 | configEditOption.EditFileName = ".jenkins-cli.yaml" 40 | content, err = configEditOption.Editor(content, fmt.Sprintf("Edit config item %s", current.Name)) 41 | if err == nil { 42 | err = ioutil.WriteFile(configPath, []byte(content), 0644) 43 | } 44 | } 45 | return 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /client/updateCenter_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 6 | httpdownloader "github.com/linuxsuren/http-downloader/pkg" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // PrepareForSetMirrorCertificate only for test 13 | func PrepareForSetMirrorCertificate(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string, enable bool) { 14 | api := "/update-center-mirror/use" 15 | if !enable { 16 | api = "/update-center-mirror/remove" 17 | } 18 | 19 | request, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", rootURL, api), nil) 20 | request.Header.Add(httpdownloader.ContentType, httpdownloader.ApplicationForm) 21 | PrepareCommonPost(request, "", roundTripper, user, password, rootURL) 22 | } 23 | 24 | // PrepareForChangeUpdateCenterSite only for test 25 | func PrepareForChangeUpdateCenterSite(roundTripper *mhttp.MockRoundTripper, rootURL, user, password, name, updateCenterURL string) { 26 | formData := url.Values{} 27 | formData.Add("site", updateCenterURL) 28 | payload := strings.NewReader(formData.Encode()) 29 | 30 | request, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/pluginManager/siteConfigure", rootURL), payload) 31 | request.Header.Add(httpdownloader.ContentType, httpdownloader.ApplicationForm) 32 | PrepareCommonPost(request, "", roundTripper, user, password, rootURL) 33 | } 34 | -------------------------------------------------------------------------------- /app/cmd/job_delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // JobDeleteOption is the job delete option 12 | type JobDeleteOption struct { 13 | common.BatchOption 14 | common.Option 15 | } 16 | 17 | var jobDeleteOption JobDeleteOption 18 | 19 | func init() { 20 | jobCmd.AddCommand(jobDeleteCmd) 21 | jobDeleteOption.SetFlag(jobDeleteCmd) 22 | jobDeleteOption.BatchOption.Stdio = common.GetSystemStdio() 23 | jobDeleteOption.Option.Stdio = common.GetSystemStdio() 24 | } 25 | 26 | var jobDeleteCmd = &cobra.Command{ 27 | Use: "delete", 28 | Aliases: common.GetAliasesDel(), 29 | Short: i18n.T("Delete a job in your Jenkins"), 30 | Long: i18n.T("Delete a job in your Jenkins"), 31 | Args: cobra.MinimumNArgs(1), 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | jobName := args[0] 34 | if !jobDeleteOption.Confirm(fmt.Sprintf("Are you sure to delete job %s ?", jobName)) { 35 | return 36 | } 37 | 38 | jclient := &client.JobClient{ 39 | JenkinsCore: client.JenkinsCore{ 40 | RoundTripper: jobDeleteOption.RoundTripper, 41 | }, 42 | } 43 | getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) 44 | 45 | err = jclient.Delete(jobName) 46 | return 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /app/cmd/user_edit_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | //func TestEditUser(t *testing.T) { 4 | // RunEditCommandTest(t, EditCommandTest{ 5 | // Procedure: func(c *expect.Console) { 6 | // c.ExpectString("Edit user description") 7 | // c.SendLine("") 8 | // go c.ExpectEOF() 9 | // time.Sleep(time.Millisecond) 10 | // c.Send("\x1b") 11 | // c.SendLine(":wq!") 12 | // }, 13 | // Test: func(stdio terminal.Stdio) (err error) { 14 | // configFile := path.Join(os.TempDir(), "fake.yaml") 15 | // defer os.Remove(configFile) 16 | // 17 | // data, err := GenerateSampleConfig() 18 | // err = ioutil.WriteFile(configFile, data, 0664) 19 | // 20 | // var ( 21 | // description = "fake-description\n" 22 | // ) 23 | // 24 | // ctrl := gomock.NewController(t) 25 | // roundTripper := mhttp.NewMockRoundTripper(ctrl) 26 | // 27 | // client.PrepareGetUser(roundTripper, "http://localhost:8080/jenkins", "admin", "111e3a2f0231198855dceaff96f20540a9") 28 | // 29 | // client.PrepareForEditUserDesc(roundTripper, "http://localhost:8080/jenkins", 30 | // "admin", description, "admin", "111e3a2f0231198855dceaff96f20540a9") 31 | // 32 | // rootCmd.SetArgs([]string{"user", "edit", "--desc", description, "--configFile", configFile}) 33 | // 34 | // userEditOption.RoundTripper = roundTripper 35 | // userEditOption.Option.Stdio = stdio 36 | // _, err = rootCmd.ExecuteC() 37 | // return 38 | // }, 39 | // }) 40 | //} 41 | -------------------------------------------------------------------------------- /app/cmd/user_edit.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // UserEditOption is the user edit cmd option 12 | type UserEditOption struct { 13 | common.Option 14 | 15 | Description string 16 | } 17 | 18 | var userEditOption UserEditOption 19 | 20 | func init() { 21 | userCmd.AddCommand(userEditCmd) 22 | userEditCmd.Flags().StringVarP(&userEditOption.Description, "desc", "d", "", 23 | i18n.T("Edit the description")) 24 | userEditOption.Stdio = common.GetSystemStdio() 25 | } 26 | 27 | var userEditCmd = &cobra.Command{ 28 | Use: "edit", 29 | Short: "Edit the user of your Jenkins", 30 | Long: fmt.Sprintf(`Edit the user of your Jenkins 31 | %s`, common.GetEditorHelpText()), 32 | RunE: func(_ *cobra.Command, _ []string) (err error) { 33 | jClient := &client.UserClient{ 34 | JenkinsCore: client.JenkinsCore{ 35 | RoundTripper: userEditOption.RoundTripper, 36 | }, 37 | } 38 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 39 | 40 | var user *client.User 41 | if user, err = jClient.Get(); err == nil { 42 | var content string 43 | content, err = userEditOption.Editor(user.Description, "Edit user description") 44 | if err == nil { 45 | err = jClient.EditDesc(content) 46 | } 47 | } 48 | return 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /app/cmd/plugin_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 5 | "github.com/jenkins-zh/jenkins-cli/client" 6 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 7 | "github.com/spf13/cobra" 8 | "net/http" 9 | ) 10 | 11 | // PluginListOption option for plugin list command 12 | type PluginListOption struct { 13 | cobra_ext.OutputOption 14 | 15 | RoundTripper http.RoundTripper 16 | } 17 | 18 | var pluginListOption PluginListOption 19 | 20 | func init() { 21 | pluginCmd.AddCommand(pluginListCmd) 22 | pluginListOption.SetFlagWithHeaders(pluginListCmd, "ShortName,Version,HasUpdate") 23 | } 24 | 25 | var pluginListCmd = &cobra.Command{ 26 | Use: "list", 27 | Short: i18n.T("Print all the plugins which are installed"), 28 | Long: i18n.T("Print all the plugins which are installed"), 29 | Example: ` jcli plugin list --filter ShortName=github 30 | jcli plugin list --no-headers`, 31 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 32 | jClient := &client.PluginManager{ 33 | JenkinsCore: client.JenkinsCore{ 34 | RoundTripper: pluginListOption.RoundTripper, 35 | }, 36 | } 37 | getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore)) 38 | 39 | var plugins *client.InstalledPluginList 40 | if plugins, err = jClient.GetPlugins(1); err == nil { 41 | pluginListOption.Writer = cmd.OutOrStdout() 42 | err = pluginListOption.OutputV2(plugins.Plugins) 43 | } 44 | return 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /app/cmd/shell_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/util" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("shell command", func() { 14 | var ( 15 | ctrl *gomock.Controller 16 | ) 17 | 18 | BeforeEach(func() { 19 | ctrl = gomock.NewController(GinkgoT()) 20 | rootCmd.SetArgs([]string{}) 21 | rootOptions.Jenkins = "" 22 | 23 | tempFile, err := ioutil.TempFile("", "example") 24 | Expect(err).To(BeNil()) 25 | 26 | rootOptions.ConfigFile = tempFile.Name() 27 | shellOptions.ExecContext = util.FakeExecCommandSuccess 28 | }) 29 | 30 | AfterEach(func() { 31 | rootCmd.SetArgs([]string{}) 32 | os.Remove(rootOptions.ConfigFile) 33 | rootOptions.ConfigFile = "" 34 | ctrl.Finish() 35 | }) 36 | 37 | //Context("basic test", func() { 38 | // It("should success", func() { 39 | // data, err := GenerateSampleConfig() 40 | // Expect(err).To(BeNil()) 41 | // err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 42 | // Expect(err).To(BeNil()) 43 | // 44 | // rootCmd.SetArgs([]string{"shell", "yourServer"}) 45 | // 46 | // buf := new(bytes.Buffer) 47 | // rootCmd.SetOutput(buf) 48 | // _, err = rootCmd.ExecuteC() 49 | // Expect(err).To(BeNil()) 50 | // 51 | // Expect(buf.String()).To(ContainSubstring("testing: warning: no tests to run\nPASS\n")) 52 | // }) 53 | //}) 54 | }) 55 | -------------------------------------------------------------------------------- /app/cmd/plugin_open.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "os" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/util" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // PluginOpenOption is the option of plugin open cmd 13 | type PluginOpenOption struct { 14 | ExecContext util.ExecContext 15 | 16 | Browser string 17 | } 18 | 19 | var pluginOpenOption PluginOpenOption 20 | 21 | func init() { 22 | pluginCmd.AddCommand(pluginOpenCmd) 23 | pluginOpenCmd.Flags().StringVarP(&pluginOpenOption.Browser, "browser", "b", "", 24 | i18n.T("Open Jenkins with a specific browser")) 25 | } 26 | 27 | var pluginOpenCmd = &cobra.Command{ 28 | Use: "open", 29 | Short: "Open update center server in browser", 30 | Long: `Open update center server in browser`, 31 | PreRun: func(_ *cobra.Command, _ []string) { 32 | if pluginOpenOption.Browser == "" { 33 | pluginOpenOption.Browser = os.Getenv("JCLI_BROWSER") 34 | } 35 | }, 36 | RunE: func(_ *cobra.Command, _ []string) (err error) { 37 | jenkins := GetCurrentJenkinsFromOptions() 38 | if jenkins == nil { 39 | err = fmt.Errorf("cannot found Jenkins by %s", rootOptions.Jenkins) 40 | return 41 | } 42 | 43 | if jenkins.URL != "" { 44 | browser := pluginOpenOption.Browser 45 | err = util.Open(fmt.Sprintf("%s/pluginManager", jenkins.URL), browser, pluginOpenOption.ExecContext) 46 | } else { 47 | err = fmt.Errorf("no URL fond from %s", jenkins.Name) 48 | } 49 | return 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /app/helper/error.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // Printer for print the info 11 | type Printer interface { 12 | PrintErr(i ...interface{}) 13 | Println(i ...interface{}) 14 | Printf(format string, i ...interface{}) 15 | } 16 | 17 | // CheckErr print a friendly error message 18 | func CheckErr(printer Printer, err error) { 19 | switch { 20 | case err == nil: 21 | return 22 | default: 23 | msg, ok := StandardErrorMessage(err) 24 | if !ok { 25 | msg = err.Error() 26 | if !strings.HasPrefix(msg, "error: ") { 27 | msg = fmt.Sprintln("error:", msg) 28 | } 29 | } 30 | printer.PrintErr(msg) 31 | } 32 | } 33 | 34 | // StandardErrorMessage is generic to the command in use 35 | func StandardErrorMessage(err error) (msg string, ok bool) { 36 | ok = true 37 | switch t := err.(type) { 38 | case url.InvalidHostError: 39 | msg = t.Error() 40 | case *url.Error: 41 | switch { 42 | case strings.Contains(t.Err.Error(), "connection refused"): 43 | host := t.URL 44 | if server, err := url.Parse(t.URL); err == nil { 45 | host = server.Host 46 | } 47 | msg = fmt.Sprintln("The connection to the server", host, "was refused - did you specify the right host or port?") 48 | default: 49 | msg = fmt.Sprintln("Unable to connect to the server:", t.Err) 50 | } 51 | case *os.PathError: 52 | msg = fmt.Sprintln("error:", t.Op, t.Path, ":", t.Err) 53 | default: 54 | ok = false 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /app/cmd/plugin_build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "os" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/util" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // PluginBuildOptions for the plugin build command 15 | type PluginBuildOptions struct { 16 | common.Option 17 | 18 | DebugOutput bool 19 | } 20 | 21 | var pluginBuildOptions PluginBuildOptions 22 | 23 | func init() { 24 | pluginCmd.AddCommand(pluginBuildCmd) 25 | pluginBuildCmd.Flags().BoolVar(&pluginBuildOptions.DebugOutput, "debug-output", false, 26 | i18n.T("If you want the maven output the debug info")) 27 | } 28 | 29 | var pluginBuildCmd = &cobra.Command{ 30 | Use: "build", 31 | Short: i18n.T("Build the Jenkins plugin project"), 32 | Long: i18n.T(`Build the Jenkins plugin project 33 | The default behaviour is "mvn clean package -DskipTests -Dmaven.test.skip"`), 34 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 35 | binary, err := util.LookPath("mvn", pluginBuildOptions.LookPathContext) 36 | if err == nil { 37 | env := os.Environ() 38 | 39 | mvnArgs := []string{"mvn", "clean", "package", "-DskipTests", "-Dmaven.test.skip"} 40 | if pluginBuildOptions.DebugOutput { 41 | mvnArgs = append(mvnArgs, "-X") 42 | } 43 | err = util.Exec(binary, mvnArgs, env, pluginBuildOptions.SystemCallExec) 44 | } 45 | return 46 | }, 47 | Annotations: map[string]string{ 48 | common.Since: "v0.0.27", 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /app/cmd/plugin_open_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/util" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "gopkg.in/yaml.v2" 9 | "io/ioutil" 10 | "os" 11 | ) 12 | 13 | var _ = Describe("plugin open test", func() { 14 | var ( 15 | err error 16 | ) 17 | 18 | BeforeEach(func() { 19 | pluginOpenOption.ExecContext = util.FakeExecCommandSuccess 20 | data, err := GenerateSampleConfig() 21 | Expect(err).To(BeNil()) 22 | rootOptions.ConfigFile = "test.yaml" 23 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 24 | Expect(err).To(BeNil()) 25 | }) 26 | 27 | JustBeforeEach(func() { 28 | rootCmd.SetArgs([]string{"plugin", "open"}) 29 | _, err = rootCmd.ExecuteC() 30 | }) 31 | 32 | AfterEach(func() { 33 | os.Remove(rootOptions.ConfigFile) 34 | }) 35 | 36 | It("should success", func() { 37 | Expect(err).NotTo(HaveOccurred()) 38 | }) 39 | 40 | Context("without url", func() { 41 | BeforeEach(func() { 42 | pluginOpenOption.ExecContext = util.FakeExecCommandSuccess 43 | sampleConfig := getSampleConfig() 44 | sampleConfig.JenkinsServers[0].URL = "" 45 | data, err := yaml.Marshal(&sampleConfig) 46 | Expect(err).To(BeNil()) 47 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 48 | Expect(err).To(BeNil()) 49 | }) 50 | 51 | It("should failure", func() { 52 | Expect(err).To(HaveOccurred()) 53 | Expect(fmt.Sprint(err)).To(ContainSubstring("no URL fond from")) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /app/cmd/user_delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "net/http" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/client" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // UserDeleteOption is user delete cmd option 14 | type UserDeleteOption struct { 15 | common.BatchOption 16 | 17 | RoundTripper http.RoundTripper 18 | } 19 | 20 | var userDeleteOption UserDeleteOption 21 | 22 | func init() { 23 | userCmd.AddCommand(userDeleteCmd) 24 | userDeleteCmd.Flags().BoolVarP(&userDeleteOption.Batch, "batch", "b", false, 25 | i18n.T("Batch mode, no need confirm")) 26 | userDeleteOption.BatchOption.Stdio = common.GetSystemStdio() 27 | } 28 | 29 | var userDeleteCmd = &cobra.Command{ 30 | Use: "delete ", 31 | Aliases: common.GetAliasesDel(), 32 | Short: "Delete a user for your Jenkins", 33 | Long: `Delete a user for your Jenkins`, 34 | Args: cobra.MinimumNArgs(1), 35 | RunE: func(cmd *cobra.Command, args []string) (err error) { 36 | username := args[0] 37 | 38 | if !userDeleteOption.Confirm(fmt.Sprintf("Are you sure to delete user %s ?", username)) { 39 | return 40 | } 41 | 42 | jclient := &client.UserClient{ 43 | JenkinsCore: client.JenkinsCore{ 44 | RoundTripper: userDeleteOption.RoundTripper, 45 | Debug: rootOptions.Debug, 46 | }, 47 | } 48 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 49 | return jclient.Delete(username) 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /client/casc.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "net/http" 4 | 5 | // CASCManager is the client of configuration as code 6 | type CASCManager struct { 7 | JenkinsCore 8 | } 9 | 10 | // Export exports the config of configuration-as-code 11 | func (c *CASCManager) Export() (config string, err error) { 12 | var ( 13 | data []byte 14 | statusCode int 15 | ) 16 | 17 | if statusCode, data, err = c.Request(http.MethodPost, "/configuration-as-code/export", 18 | nil, nil); err == nil && 19 | statusCode != 200 { 20 | err = c.ErrorHandle(statusCode, data) 21 | } 22 | config = string(data) 23 | return 24 | } 25 | 26 | // Schema get the schema of configuration-as-code 27 | func (c *CASCManager) Schema() (schema string, err error) { 28 | var ( 29 | data []byte 30 | statusCode int 31 | ) 32 | 33 | if statusCode, data, err = c.Request(http.MethodPost, "/configuration-as-code/schema", 34 | nil, nil); err == nil && 35 | statusCode != 200 { 36 | err = c.ErrorHandle(statusCode, data) 37 | } 38 | schema = string(data) 39 | return 40 | } 41 | 42 | // Reload reload the config of configuration-as-code 43 | func (c *CASCManager) Reload() (err error) { 44 | _, err = c.RequestWithoutData(http.MethodPost, "/configuration-as-code/reload", 45 | nil, nil, 200) 46 | return 47 | } 48 | 49 | // Apply apply the config of configuration-as-code 50 | func (c *CASCManager) Apply() (err error) { 51 | _, err = c.RequestWithoutData(http.MethodPost, "/configuration-as-code/apply", 52 | nil, nil, 200) 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /app/cmd/job_stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "github.com/spf13/cobra" 9 | "strconv" 10 | ) 11 | 12 | // JobStopOption is the job stop option 13 | type JobStopOption struct { 14 | common.BatchOption 15 | common.Option 16 | } 17 | 18 | var jobStopOption JobStopOption 19 | 20 | func init() { 21 | jobCmd.AddCommand(jobStopCmd) 22 | jobStopOption.SetFlag(jobStopCmd) 23 | jobStopOption.Option.Stdio = common.GetSystemStdio() 24 | jobStopOption.BatchOption.Stdio = common.GetSystemStdio() 25 | } 26 | 27 | var jobStopCmd = &cobra.Command{ 28 | Use: "stop [buildNumber]", 29 | Short: i18n.T("Stop a job build in your Jenkins"), 30 | Long: i18n.T("Stop a job build in your Jenkins"), 31 | Args: cobra.MinimumNArgs(1), 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | buildNum := -1 34 | if len(args) > 1 { 35 | if buildNum, err = strconv.Atoi(args[1]); err != nil { 36 | return 37 | } 38 | } 39 | 40 | jobName := args[0] 41 | if !jobStopOption.Confirm(fmt.Sprintf("Are you sure to stop job %s ?", jobName)) { 42 | return 43 | } 44 | 45 | jclient := &client.JobClient{ 46 | JenkinsCore: client.JenkinsCore{ 47 | RoundTripper: jobStopOption.RoundTripper, 48 | }, 49 | } 50 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 51 | 52 | return jclient.StopJob(jobName, buildNum) 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /client/artifacts_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 10 | ) 11 | 12 | // PrepareGetArtifacts only for test 13 | func PrepareGetArtifacts(roundTripper *mhttp.MockRoundTripper, rootURL, user, passwd, 14 | jobName string, buildID int) (response *http.Response) { 15 | path := ParseJobPath(jobName) 16 | var api string 17 | if buildID <= 0 { 18 | api = fmt.Sprintf("%s/lastBuild/wfapi/artifacts", path) 19 | } else { 20 | api = fmt.Sprintf("%s/%d/wfapi/artifacts", path, buildID) 21 | } 22 | request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", rootURL, api), nil) 23 | response = &http.Response{ 24 | StatusCode: 200, 25 | Request: request, 26 | Body: ioutil.NopCloser(bytes.NewBufferString(`[{"id":"n1","name":"a.log","path":"a.log","url":"/job/pipeline/1/artifact/a.log","size":0}]`)), 27 | } 28 | roundTripper.EXPECT(). 29 | RoundTrip(NewRequestMatcher(request)).Return(response, nil) 30 | 31 | if user != "" && passwd != "" { 32 | request.SetBasicAuth(user, passwd) 33 | } 34 | return 35 | } 36 | 37 | // PrepareGetEmptyArtifacts only for test 38 | func PrepareGetEmptyArtifacts(roundTripper *mhttp.MockRoundTripper, rootURL, user, passwd, 39 | jobName string, buildID int) (response *http.Response) { 40 | response = PrepareGetArtifacts(roundTripper, rootURL, user, passwd, jobName, buildID) 41 | response.Body = ioutil.NopCloser(bytes.NewBufferString(`[]`)) 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /e2e/plugin_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestListPlugins(t *testing.T) { 13 | cmd := exec.Command("jcli", "plugin", "list", "--url", GetJenkinsURL()) 14 | data, err := cmd.CombinedOutput() 15 | assert.Nil(t, err) 16 | 17 | fmt.Println(string(data)) 18 | } 19 | 20 | func TestSearchPlugins(t *testing.T) { 21 | cmd := exec.Command("jcli", "plugin", "search", "localization-zh-cn", "--url", GetJenkinsURL()) 22 | data, err := cmd.CombinedOutput() 23 | assert.Nil(t, err) 24 | 25 | fmt.Println(string(data)) 26 | } 27 | 28 | func TestCheckUpdateCenter(t *testing.T) { 29 | cmd := exec.Command("jcli", "plugin", "check", "--url", GetJenkinsURL()) 30 | data, err := cmd.CombinedOutput() 31 | assert.Nil(t, err) 32 | 33 | fmt.Println(string(data)) 34 | } 35 | 36 | func TestInstallPlugin(t *testing.T) { 37 | TestCheckUpdateCenter(t) 38 | 39 | cmd := exec.Command("jcli", "plugin", "install", "localization-zh-cn", "--url", GetJenkinsURL()) 40 | data, err := cmd.CombinedOutput() 41 | assert.Nil(t, err) 42 | 43 | fmt.Println(string(data)) 44 | } 45 | 46 | func TestDownloadPlugin(t *testing.T) { 47 | tempDir := os.TempDir() 48 | defer os.Remove(tempDir) 49 | 50 | cmd := exec.Command("jcli", "plugin", "download", "localization-zh-cn", 51 | "--download-dir", tempDir, "--url", GetJenkinsURL()) 52 | data, err := cmd.CombinedOutput() 53 | assert.Nil(t, err) 54 | 55 | fmt.Println(string(data)) 56 | } 57 | -------------------------------------------------------------------------------- /app/cmd/keyring/core.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/config" 6 | "github.com/zalando/go-keyring" 7 | ) 8 | 9 | const ( 10 | // PlaceHolder is the replacer of original credential 11 | PlaceHolder = "******" 12 | // KeyTokenPrefix is the prefix of keyring service 13 | KeyTokenPrefix = "jcli-config-token" 14 | ) 15 | 16 | // SaveTokenToKeyring store the token to keyring 17 | func SaveTokenToKeyring(config *config.Config) { 18 | if config == nil { 19 | return 20 | } 21 | for i, item := range config.JenkinsServers { 22 | token := item.Token 23 | if token == PlaceHolder { 24 | continue 25 | } 26 | 27 | if err := keyring.Set(fmt.Sprintf("%s-%s", KeyTokenPrefix, item.Name), item.UserName, token); err == nil { 28 | (&item).Token = PlaceHolder 29 | config.JenkinsServers[i] = item 30 | } 31 | } 32 | } 33 | 34 | // LoadTokenFromKeyring load token from keyring 35 | func LoadTokenFromKeyring(config *config.Config) { 36 | for i, item := range config.JenkinsServers { 37 | if item.Token != PlaceHolder { 38 | continue 39 | } 40 | if token, err := keyring.Get(fmt.Sprintf("%s-%s", KeyTokenPrefix, item.Name), item.UserName); err == nil { 41 | (&item).Token = token 42 | config.JenkinsServers[i] = item 43 | } 44 | } 45 | } 46 | 47 | // DelToken removes the token from keyring 48 | func DelToken(jenkins config.JenkinsServer) (err error) { 49 | err = keyring.Delete(fmt.Sprintf("%s-%s", KeyTokenPrefix, jenkins.Name), jenkins.UserName) 50 | return 51 | } 52 | -------------------------------------------------------------------------------- /app/cmd/plugin_create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "os" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/util" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // PluginCreateOptions for the plugin create command 15 | type PluginCreateOptions struct { 16 | common.Option 17 | 18 | DebugOutput bool 19 | } 20 | 21 | var pluginCreateOptions PluginCreateOptions 22 | 23 | func init() { 24 | pluginCmd.AddCommand(pluginCreateCmd) 25 | pluginCreateCmd.Flags().BoolVar(&pluginCreateOptions.DebugOutput, "debug-output", false, 26 | i18n.T("If you want the maven output the debug info")) 27 | } 28 | 29 | var pluginCreateCmd = &cobra.Command{ 30 | Use: "create", 31 | Short: i18n.T("Create a plugin project from the archetypes"), 32 | Long: i18n.T(`Create a plugin project from the archetypes 33 | Plugin tutorial is here https://jenkins.io/doc/developer/tutorial/`), 34 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 35 | binary, err := util.LookPath("mvn", pluginCreateOptions.LookPathContext) 36 | if err == nil { 37 | env := os.Environ() 38 | 39 | mvnArgs := []string{"mvn", "archetype:generate", "-U", `-Dfilter=io.jenkins.archetypes:`} 40 | if pluginCreateOptions.DebugOutput { 41 | mvnArgs = append(mvnArgs, "-X") 42 | } 43 | err = util.Exec(binary, mvnArgs, env, pluginCreateOptions.SystemCallExec) 44 | } 45 | return 46 | }, 47 | Annotations: map[string]string{ 48 | common.Since: "v0.0.23", 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /app/cmd/config_update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | type configUpdateOption struct { 9 | name string 10 | token string 11 | } 12 | 13 | func createConfigUpdateCmd() (cmd *cobra.Command) { 14 | opt := &configUpdateOption{} 15 | 16 | cmd = &cobra.Command{ 17 | Use: "update", 18 | Aliases: []string{"up"}, 19 | Short: "Update a Jenkins config", 20 | Example: "jcli config update --token", 21 | PreRunE: opt.preRunE, 22 | ValidArgsFunction: ValidJenkinsNames, 23 | RunE: opt.runE, 24 | } 25 | 26 | flags := cmd.Flags() 27 | flags.StringVarP(&opt.token, "token", "", "", 28 | "The token of Jenkins config item") 29 | return 30 | } 31 | 32 | func (o *configUpdateOption) preRunE(_ *cobra.Command, args []string) (err error) { 33 | if o.token == "" { 34 | err = fmt.Errorf("no token provided") 35 | } 36 | 37 | if len(args) > 0 { 38 | o.name = args[0] 39 | } 40 | 41 | if o.name == "" { 42 | if jenkins := getCurrentJenkins(); jenkins != nil { 43 | o.name = jenkins.Name 44 | } 45 | } 46 | return 47 | } 48 | 49 | func (o *configUpdateOption) runE(_ *cobra.Command, _ []string) (err error) { 50 | found := false 51 | for i, cfg := range config.JenkinsServers { 52 | if cfg.Name == o.name { 53 | found = true 54 | config.JenkinsServers[i].Token = o.token 55 | err = saveConfig() 56 | break 57 | } 58 | } 59 | 60 | if !found { 61 | err = fmt.Errorf("jenkins '%s' does not exist", o.name) 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /app/cmd/queue_cancel.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 8 | 9 | "go.uber.org/zap" 10 | 11 | "github.com/jenkins-zh/jenkins-cli/client" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // QueueCancelOption represents the option of queue cancel command 16 | type QueueCancelOption struct { 17 | RoundTripper http.RoundTripper 18 | } 19 | 20 | var queueCancelOption QueueCancelOption 21 | 22 | func init() { 23 | queueCmd.AddCommand(queueCancelCmd) 24 | } 25 | 26 | var queueCancelCmd = &cobra.Command{ 27 | Use: "cancel", 28 | Example: "jcli queue cancel 234", 29 | Short: i18n.T("Cancel the queue items of your Jenkins"), 30 | Long: i18n.T("Cancel the queue items of your Jenkins"), 31 | Args: cobra.MinimumNArgs(1), 32 | RunE: func(cmd *cobra.Command, args []string) (err error) { 33 | for _, arg := range args { 34 | if err = queueCancelOption.cancel(arg); err != nil { 35 | break 36 | } 37 | } 38 | return 39 | }, 40 | } 41 | 42 | func (c *QueueCancelOption) cancel(id string) (err error) { 43 | var queueID int 44 | if queueID, err = strconv.Atoi(id); err != nil { 45 | return 46 | } 47 | 48 | jclient := &client.QueueClient{ 49 | JenkinsCore: client.JenkinsCore{ 50 | RoundTripper: queueCancelOption.RoundTripper, 51 | Debug: rootOptions.Debug, 52 | }, 53 | } 54 | getCurrentJenkinsAndClient(&(jclient.JenkinsCore)) 55 | 56 | logger.Debug("cancel queue by id,", zap.Int("id", queueID)) 57 | 58 | err = jclient.Cancel(queueID) 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /app/cmd/plugin_trend_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/jenkins-zh/jenkins-cli/client" 13 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("plugin trend command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | pluginName string 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 26 | pluginTreadOption.RoundTripper = roundTripper 27 | rootCmd.SetArgs([]string{}) 28 | rootOptions.Jenkins = "" 29 | rootOptions.ConfigFile = "test.yaml" 30 | pluginName = "fake" 31 | }) 32 | 33 | AfterEach(func() { 34 | rootCmd.SetArgs([]string{}) 35 | os.Remove(rootOptions.ConfigFile) 36 | rootOptions.ConfigFile = "" 37 | ctrl.Finish() 38 | }) 39 | 40 | Context("basic cases", func() { 41 | It("should success", func() { 42 | data, err := GenerateSampleConfig() 43 | Expect(err).To(BeNil()) 44 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 45 | Expect(err).To(BeNil()) 46 | 47 | client.PrepareShowTrend(roundTripper, pluginName) 48 | 49 | rootCmd.SetArgs([]string{"plugin", "trend", pluginName}) 50 | 51 | buf := new(bytes.Buffer) 52 | rootCmd.SetOutput(buf) 53 | _, err = rootCmd.ExecuteC() 54 | Expect(err).To(BeNil()) 55 | 56 | Expect(buf.String()).NotTo(Equal("")) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /app/cmd/computer_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 7 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // ComputerListOption option for config list command 15 | type ComputerListOption struct { 16 | common.Option 17 | cobra_ext.OutputOption 18 | } 19 | 20 | var computerListOption ComputerListOption 21 | 22 | func init() { 23 | computerCmd.AddCommand(computerListCmd) 24 | computerListOption.SetFlagWithHeaders(computerListCmd, "DisplayName,NumExecutors,Description,Offline") 25 | } 26 | 27 | var computerListCmd = &cobra.Command{ 28 | Use: "list", 29 | Short: i18n.T("List all Jenkins agents"), 30 | Long: i18n.T("List all Jenkins agents"), 31 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 32 | jClient, config := GetComputerClient(computerListOption.Option) 33 | if config == nil { 34 | err = fmt.Errorf("cannot found the configuration") 35 | return 36 | } 37 | 38 | var computers computer.List 39 | if computers, err = jClient.List(); err == nil { 40 | computerListOption.Writer = cmd.OutOrStdout() 41 | computerListOption.CellRenderMap = map[string]cobra_ext.RenderCell{ 42 | "Offline": func(offline string) string { 43 | switch offline { 44 | case "true": 45 | return "yes" 46 | } 47 | return "no" 48 | }, 49 | } 50 | err = computerListOption.OutputV2(computers.Computer) 51 | } 52 | return 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /app/cmd/job_enable_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("job enable command", func() { 15 | var ( 16 | ctrl *gomock.Controller 17 | roundTripper *mhttp.MockRoundTripper 18 | err error 19 | ) 20 | 21 | BeforeEach(func() { 22 | ctrl = gomock.NewController(GinkgoT()) 23 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 24 | jobEnableOption.RoundTripper = roundTripper 25 | rootCmd.SetArgs([]string{}) 26 | rootOptions.Jenkins = "" 27 | rootOptions.ConfigFile = "test.yaml" 28 | 29 | data, err := GenerateSampleConfig() 30 | Expect(err).To(BeNil()) 31 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 32 | Expect(err).To(BeNil()) 33 | }) 34 | 35 | AfterEach(func() { 36 | rootCmd.SetArgs([]string{}) 37 | err = os.Remove(rootOptions.ConfigFile) 38 | rootOptions.ConfigFile = "" 39 | ctrl.Finish() 40 | }) 41 | 42 | Context("basic cases", func() { 43 | It("should not error", func() { 44 | Expect(err).NotTo(HaveOccurred()) 45 | }) 46 | 47 | It("should success", func() { 48 | jobName := "fakeJob" 49 | client.PrepareForEnableJob(roundTripper, "http://localhost:8080/jenkins", jobName, "admin", "111e3a2f0231198855dceaff96f20540a9") 50 | 51 | rootCmd.SetArgs([]string{"job", "enable", jobName}) 52 | 53 | _, err = rootCmd.ExecuteC() 54 | Expect(err).To(BeNil()) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /app/cmd/job_disable_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("job disable command", func() { 15 | var ( 16 | ctrl *gomock.Controller 17 | roundTripper *mhttp.MockRoundTripper 18 | err error 19 | ) 20 | 21 | BeforeEach(func() { 22 | ctrl = gomock.NewController(GinkgoT()) 23 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 24 | jobDisableOption.RoundTripper = roundTripper 25 | rootCmd.SetArgs([]string{}) 26 | rootOptions.Jenkins = "" 27 | rootOptions.ConfigFile = "test.yaml" 28 | 29 | data, err := GenerateSampleConfig() 30 | Expect(err).To(BeNil()) 31 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 32 | Expect(err).To(BeNil()) 33 | }) 34 | 35 | AfterEach(func() { 36 | rootCmd.SetArgs([]string{}) 37 | err = os.Remove(rootOptions.ConfigFile) 38 | rootOptions.ConfigFile = "" 39 | ctrl.Finish() 40 | }) 41 | 42 | Context("basic cases", func() { 43 | It("should not error", func() { 44 | Expect(err).NotTo(HaveOccurred()) 45 | }) 46 | 47 | It("should success", func() { 48 | jobName := "fakeJob" 49 | client.PrepareForDisableJob(roundTripper, "http://localhost:8080/jenkins", jobName, "admin", "111e3a2f0231198855dceaff96f20540a9") 50 | 51 | rootCmd.SetArgs([]string{"job", "disable", jobName}) 52 | 53 | _, err = rootCmd.ExecuteC() 54 | Expect(err).To(BeNil()) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /app/cmd/plugin_checkout_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-cli/client" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/golang/mock/gomock" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("plugin checkout command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | ) 21 | 22 | BeforeEach(func() { 23 | ctrl = gomock.NewController(GinkgoT()) 24 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 25 | pluginCheckoutOption.RoundTripper = roundTripper 26 | rootCmd.SetArgs([]string{}) 27 | rootOptions.Jenkins = "" 28 | rootOptions.ConfigFile = "test.yaml" 29 | }) 30 | 31 | AfterEach(func() { 32 | rootCmd.SetArgs([]string{}) 33 | os.Remove(rootOptions.ConfigFile) 34 | rootOptions.ConfigFile = "" 35 | ctrl.Finish() 36 | }) 37 | 38 | Context("basic cases", func() { 39 | It("should success", func() { 40 | var err error 41 | var data []byte 42 | data, err = GenerateSampleConfig() 43 | Expect(err).To(BeNil()) 44 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 45 | Expect(err).To(BeNil()) 46 | 47 | client.PrepareCheckUpdate(roundTripper, "http://localhost:8080/jenkins", 48 | "admin", "111e3a2f0231198855dceaff96f20540a9") 49 | 50 | rootCmd.SetArgs([]string{"plugin", "check"}) 51 | 52 | buf := new(bytes.Buffer) 53 | rootCmd.SetOut(buf) 54 | _, err = rootCmd.ExecuteC() 55 | Expect(err).To(BeNil()) 56 | Expect(buf.String()).To(Equal("")) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /app/cmd/job_artifact.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 7 | "strconv" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/client" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // JobArtifactOption is the options of job artifact command 14 | type JobArtifactOption struct { 15 | cobra_ext.OutputOption 16 | common.Option 17 | } 18 | 19 | var jobArtifactOption JobArtifactOption 20 | 21 | func init() { 22 | jobCmd.AddCommand(jobArtifactCmd) 23 | jobArtifactOption.SetFlagWithHeaders(jobArtifactCmd, "Name,Path,Size") 24 | } 25 | 26 | var jobArtifactCmd = &cobra.Command{ 27 | Use: "artifact [buildID]", 28 | Short: i18n.T("Print the artifact list of target job"), 29 | Long: i18n.T("Print the artifact list of target job"), 30 | Args: cobra.MinimumNArgs(1), 31 | RunE: func(cmd *cobra.Command, args []string) (err error) { 32 | argLen := len(args) 33 | jobName := args[0] 34 | buildID := -1 35 | 36 | if argLen >= 2 { 37 | if buildID, err = strconv.Atoi(args[1]); err != nil { 38 | return 39 | } 40 | } 41 | 42 | jclient := &client.ArtifactClient{ 43 | JenkinsCore: client.JenkinsCore{ 44 | RoundTripper: jobArtifactOption.RoundTripper, 45 | }, 46 | } 47 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 48 | 49 | var artifacts []client.Artifact 50 | if artifacts, err = jclient.List(jobName, buildID); err == nil { 51 | jobArtifactOption.Writer = cmd.OutOrStdout() 52 | err = jobArtifactOption.OutputV2(artifacts) 53 | } 54 | return 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /app/cmd/plugin_check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | "io/ioutil" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/jenkins-zh/jenkins-cli/client" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // PluginCheckoutOption is the option for plugin checkout command 15 | type PluginCheckoutOption struct { 16 | RoundTripper http.RoundTripper 17 | // Timeout is the timeout setting for check Jenkins update-center 18 | Timeout int64 19 | } 20 | 21 | var pluginCheckoutOption PluginCheckoutOption 22 | 23 | func init() { 24 | pluginCmd.AddCommand(pluginCheckCmd) 25 | 26 | flags := pluginCheckCmd.Flags() 27 | flags.Int64VarP(&pluginCheckoutOption.Timeout, "timeout", "", 30, 28 | "Timeout in second setting for checking Jenkins update-center") 29 | } 30 | 31 | var pluginCheckCmd = &cobra.Command{ 32 | Use: "check", 33 | Short: i18n.T("Check update center server"), 34 | Long: i18n.T(`Check update center server`), 35 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 36 | jClient := &client.PluginManager{ 37 | JenkinsCore: client.JenkinsCore{ 38 | RoundTripper: pluginCheckoutOption.RoundTripper, 39 | Timeout: time.Duration(pluginCheckoutOption.Timeout) * time.Second, 40 | }, 41 | } 42 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 43 | 44 | err = jClient.CheckUpdate(func(response *http.Response) { 45 | code := response.StatusCode 46 | if code != 200 { 47 | contentData, _ := ioutil.ReadAll(response.Body) 48 | cmd.PrintErrln(fmt.Sprintf("response code is %d, content: %s", code, string(contentData))) 49 | } 50 | }) 51 | return 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.187.0/containers/docker-existing-dockerfile 3 | { 4 | "name": "Existing Dockerfile", 5 | 6 | // Sets the run context to one level up instead of the .devcontainer folder. 7 | "context": "..", 8 | 9 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 10 | "dockerFile": "../.gitpod.Dockerfile", 11 | 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": {}, 14 | 15 | // Add the IDs of extensions you want installed when the container is created. 16 | "extensions": [ 17 | "zenor.makefile-creator", 18 | "golang.go" 19 | ] 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Uncomment the next line to run commands after the container is created - for example installing curl. 25 | // "postCreateCommand": "apt-get update && apt-get install -y curl", 26 | 27 | // Uncomment when using a ptrace-based debugger like C++, Go, and Rust 28 | // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 29 | 30 | // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker. 31 | // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ], 32 | 33 | // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. 34 | // "remoteUser": "vscode" 35 | } 36 | -------------------------------------------------------------------------------- /docs/book/zh/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 插件 3 | weight: 70 4 | --- 5 | 6 | # 插件 7 | 8 | `jcli` 可以让你搜索、下载、安装、卸载或者上传插件。 9 | 10 | ## 列表 11 | 12 | 下面的命令可以列出所有已经安装的插件: 13 | 14 | `jcli plugin list` 15 | 16 | 如果需要根据字段进行过滤的话,可以利用下面的命令: 17 | 18 | `jcli plugin list --filter ShortName=github` 19 | 20 | ## 检索 21 | 22 | 你可以通过关键字来搜索要安装的插件,命令如下: 23 | 24 | `jcli plugin search zh-cn` 25 | 26 | ## 安装 27 | 28 | 给定要安装的插件的名称,并用如下的命令来安装: 29 | 30 | `jcli plugin install localization-zh-cn` 31 | 32 | 执行完成上面的安装命令后,可以通过下面的命令看到安装过程: 33 | 34 | `jcli center watch` 35 | 36 | ## 下载 37 | 38 | 当你的 Jenkins 无法访问外网,或者其他无法直接安装插件的情况下,可以先把需要安装的插件下载到本地,然后再上传。 39 | 40 | `jcli plugin download localization-zh-cn` 41 | 42 | 默认情况下,会下载你需要的插件以及依赖。如果不需要下载依赖的话,可以使用参数: `--skip-dependency` 43 | 44 | ## 上传 45 | 46 | 你可以选择上传本地或者远程的插件文件,甚至可以实现编译本地的插件源码后上传。在没有给定任何参数的情况下, 47 | 上传命令首先会尝试执行 Maven 的构建命令,然后再上传文件。 48 | 49 | `jcli plugin upload` 50 | 51 | 如果你已经有编译好的插件文件,可以使用下面的命令: 52 | 53 | `jcli plugin upload sample.hpi` 54 | 55 | ## 升级 56 | 57 | 如果没有任何参数的话,下面的命令会列出来所有可以升级的插件,利用方向键以及空格可以选择所需要升级的插件,最后回车确认: 58 | 59 | `jcli plugin upgrade` 60 | 61 | 另外,也可以通过给定插件名称的方式,直接升级指定的插件: 62 | 63 | `jcli plugin upgrade blueocean-personalization` 64 | 65 | ## 卸载 66 | 67 | `jcli plugin uninstall` 68 | 69 | ## 检查更新 70 | 71 | 检查更新,也就是从 Jenkins 的更新中心(Update Center)中获取最新的版本信息,执行下面的命令: 72 | 73 | `jcli plugin check` 74 | 75 | 该命令执行的时间长短,和 Jenkins 所在机器的网络状态有关系,默认的超时时间为:30秒。另外,也可以通过设置参数的方式指定: 76 | 77 | `jcli plugin checkout --timeout 60` 78 | 79 | ## 创建 80 | 81 | 对于插件的开发者而言,插件的创建、构建、发布也是高频操作,`jcli` 对这些都有支持: 82 | 83 | `jcli plugin create` 84 | 85 | ## 构建 86 | 87 | `jcli plugin build` 88 | 89 | ## 发布 90 | 91 | `jcli plugin release` 92 | -------------------------------------------------------------------------------- /mock/mhttp/roundtripper.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: net/http (interfaces: RoundTripper) 3 | 4 | // Package mhttp is a generated GoMock package. 5 | package mhttp 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | http "net/http" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockRoundTripper is a mock of RoundTripper interface 14 | type MockRoundTripper struct { 15 | ctrl *gomock.Controller 16 | recorder *MockRoundTripperMockRecorder 17 | } 18 | 19 | // MockRoundTripperMockRecorder is the mock recorder for MockRoundTripper 20 | type MockRoundTripperMockRecorder struct { 21 | mock *MockRoundTripper 22 | } 23 | 24 | // NewMockRoundTripper creates a new mock instance 25 | func NewMockRoundTripper(ctrl *gomock.Controller) *MockRoundTripper { 26 | mock := &MockRoundTripper{ctrl: ctrl} 27 | mock.recorder = &MockRoundTripperMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockRoundTripper) EXPECT() *MockRoundTripperMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // RoundTrip mocks base method 37 | func (m *MockRoundTripper) RoundTrip(arg0 *http.Request) (*http.Response, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "RoundTrip", arg0) 40 | ret0, _ := ret[0].(*http.Response) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // RoundTrip indicates an expected call of RoundTrip 46 | func (mr *MockRoundTripperMockRecorder) RoundTrip(arg0 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RoundTrip", reflect.TypeOf((*MockRoundTripper)(nil).RoundTrip), arg0) 49 | } 50 | -------------------------------------------------------------------------------- /client/artifacts_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("artifacts test", func() { 11 | var ( 12 | ctrl *gomock.Controller 13 | roundTripper *mhttp.MockRoundTripper 14 | artifactClient ArtifactClient 15 | 16 | username string 17 | password string 18 | ) 19 | 20 | BeforeEach(func() { 21 | ctrl = gomock.NewController(GinkgoT()) 22 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 23 | artifactClient = ArtifactClient{} 24 | artifactClient.RoundTripper = roundTripper 25 | artifactClient.URL = "http://localhost" 26 | 27 | username = "admin" 28 | password = "token" 29 | }) 30 | 31 | AfterEach(func() { 32 | ctrl.Finish() 33 | }) 34 | 35 | Context("List", func() { 36 | It("should success", func() { 37 | artifactClient.UserName = username 38 | artifactClient.Token = password 39 | 40 | jobName := "fakename" 41 | PrepareGetArtifacts(roundTripper, artifactClient.URL, username, password, jobName, 1) 42 | 43 | artifacts, err := artifactClient.List(jobName, 1) 44 | Expect(err).To(BeNil()) 45 | Expect(len(artifacts)).To(Equal(1)) 46 | }) 47 | 48 | It("should success, with empty artifacts", func() { 49 | artifactClient.UserName = username 50 | artifactClient.Token = password 51 | 52 | jobName := "fakename" 53 | PrepareGetEmptyArtifacts(roundTripper, artifactClient.URL, username, password, jobName, 1) 54 | 55 | artifacts, err := artifactClient.List(jobName, 1) 56 | Expect(err).To(BeNil()) 57 | Expect(len(artifacts)).To(Equal(0)) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /app/cmd/center_mirror.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // CenterMirrorOption option for upgrade Jenkins 13 | type CenterMirrorOption struct { 14 | RoundTripper http.RoundTripper 15 | 16 | Enable bool 17 | MirrorURL string 18 | } 19 | 20 | var centerMirrorOption CenterMirrorOption 21 | 22 | func init() { 23 | centerCmd.AddCommand(centerMirrorCmd) 24 | centerMirrorCmd.Flags().BoolVarP(¢erMirrorOption.Enable, "enable", "", true, 25 | i18n.T("If you want to enable update center server")) 26 | centerMirrorCmd.Flags().StringVarP(¢erMirrorOption.MirrorURL, "mirror-url", "", "https://updates.jenkins-zh.cn/update-center.json", 27 | i18n.T("The address of update center site mirror")) 28 | } 29 | 30 | var centerMirrorCmd = &cobra.Command{ 31 | Use: "mirror", 32 | Short: i18n.T("Set the update center to a mirror address"), 33 | Long: i18n.T("Set the update center to a mirror address"), 34 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 35 | jclient := &client.UpdateCenterManager{ 36 | JenkinsCore: client.JenkinsCore{ 37 | RoundTripper: centerUpgradeOption.RoundTripper, 38 | }, 39 | } 40 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 41 | 42 | var siteURL string 43 | if centerMirrorOption.Enable { 44 | siteURL = centerMirrorOption.MirrorURL 45 | } else { 46 | siteURL = "https://updates.jenkins.io/update-center.json" 47 | } 48 | 49 | if err = jclient.ChangeUpdateCenterSite("default", siteURL); err == nil { 50 | err = jclient.SetMirrorCertificate(centerMirrorOption.Enable) 51 | } 52 | return 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /app/cmd/crumbissuer_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 15 | ) 16 | 17 | var _ = Describe("crumb command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | jenkinsRoot string 22 | username string 23 | token string 24 | ) 25 | 26 | BeforeEach(func() { 27 | ctrl = gomock.NewController(GinkgoT()) 28 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 29 | crumbIssuerOptions.RoundTripper = roundTripper 30 | rootCmd.SetArgs([]string{}) 31 | rootOptions.Jenkins = "" 32 | rootOptions.ConfigFile = "test.yaml" 33 | 34 | jenkinsRoot = "http://localhost:8080/jenkins" 35 | username = "admin" 36 | token = "111e3a2f0231198855dceaff96f20540a9" 37 | }) 38 | 39 | AfterEach(func() { 40 | rootCmd.SetArgs([]string{}) 41 | os.Remove(rootOptions.ConfigFile) 42 | rootOptions.ConfigFile = "" 43 | ctrl.Finish() 44 | }) 45 | 46 | Context("basic cases", func() { 47 | It("should success", func() { 48 | data, err := GenerateSampleConfig() 49 | Expect(err).To(BeNil()) 50 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 51 | Expect(err).To(BeNil()) 52 | 53 | client.PrepareForGetIssuer(roundTripper, jenkinsRoot, username, token) 54 | 55 | buf := new(bytes.Buffer) 56 | rootCmd.SetOutput(buf) 57 | rootCmd.SetArgs([]string{"crumb"}) 58 | _, err = rootCmd.ExecuteC() 59 | Expect(err).To(BeNil()) 60 | Expect(buf.String()).To(Equal("CrumbRequestField=Crumb\n")) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /app/cmd/shutdown_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | 12 | "github.com/golang/mock/gomock" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("shutdown command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | configFile string 22 | err error 23 | ) 24 | 25 | BeforeEach(func() { 26 | rootOptions := GetRootOptions() 27 | ctrl = gomock.NewController(GinkgoT()) 28 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 29 | rootOptions.CommonOption = &common.Option{ 30 | RoundTripper: roundTripper, 31 | } 32 | rootOptions.Jenkins = "" 33 | configFile = path.Join(os.TempDir(), "fake.yaml") 34 | rootOptions.ConfigFile = configFile 35 | 36 | var data []byte 37 | data, err = GenerateSampleConfig() 38 | Expect(err).To(BeNil()) 39 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 40 | Expect(err).To(BeNil()) 41 | }) 42 | 43 | AfterEach(func() { 44 | os.Remove(configFile) 45 | GetRootOptions().ConfigFile = "" 46 | ctrl.Finish() 47 | }) 48 | 49 | Context("with batch mode", func() { 50 | It("should success", func() { 51 | client.PrepareForShutdown(roundTripper, "http://localhost:8080/jenkins", "admin", "111e3a2f0231198855dceaff96f20540a9", true) 52 | 53 | GetRootCommand().SetArgs([]string{"shutdown", "-b"}) 54 | 55 | buf := new(bytes.Buffer) 56 | GetRootCommand().SetOutput(buf) 57 | _, err = GetRootCommand().ExecuteC() 58 | Expect(err).To(BeNil()) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /app/cmd/job_history.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 5 | "net/http" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // JobHistoryOption is the job history option 13 | type JobHistoryOption struct { 14 | cobra_ext.OutputOption 15 | 16 | Delete int 17 | RoundTripper http.RoundTripper 18 | } 19 | 20 | var jobHistoryOption JobHistoryOption 21 | 22 | func init() { 23 | jobCmd.AddCommand(jobHistoryCmd) 24 | jobHistoryOption.SetFlagWithHeaders(jobHistoryCmd, "ID,DisplayName,Description,Building,Result") 25 | jobHistoryCmd.Flags().IntVarP(&jobHistoryOption.Delete, "delete", "d", -1, "Delete a history item") 26 | 27 | jobHistoryCmd.AddCommand(createJobHistoryEditCmd()) 28 | } 29 | 30 | var jobHistoryCmd = &cobra.Command{ 31 | Use: "history", 32 | Short: i18n.T("Print the history of job in your Jenkins"), 33 | Long: i18n.T(`Print the history of job in your Jenkins`), 34 | Args: cobra.MinimumNArgs(1), 35 | RunE: func(cmd *cobra.Command, args []string) (err error) { 36 | jobName := args[0] 37 | 38 | jClient := &client.JobClient{ 39 | JenkinsCore: client.JenkinsCore{ 40 | RoundTripper: jobHistoryOption.RoundTripper, 41 | }, 42 | } 43 | getCurrentJenkinsAndClientOrDie(&(jClient.JenkinsCore)) 44 | 45 | if jobHistoryOption.Delete != -1 { 46 | err = jClient.DeleteHistory(jobName, jobHistoryOption.Delete) 47 | return 48 | } 49 | 50 | var builds []*client.JobBuild 51 | builds, err = jClient.GetHistory(jobName) 52 | if err == nil { 53 | jobHistoryOption.Writer = cmd.OutOrStdout() 54 | err = jobHistoryOption.OutputV2(builds) 55 | } 56 | return 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /app/cmd/restart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // RestartOption holds the options for restart cmd 12 | type RestartOption struct { 13 | common.BatchOption 14 | common.Option 15 | 16 | Safe bool 17 | } 18 | 19 | var restartOption RestartOption 20 | 21 | func init() { 22 | rootCmd.AddCommand(restartCmd) 23 | restartOption.SetFlag(restartCmd) 24 | restartCmd.Flags().BoolVarP(&restartOption.Safe, "safe", "s", true, 25 | i18n.T("Puts Jenkins into the quiet mode, wait for existing builds to be completed, and then restart Jenkins")) 26 | restartOption.BatchOption.Stdio = common.GetSystemStdio() 27 | restartOption.Option.Stdio = common.GetSystemStdio() 28 | } 29 | 30 | var restartCmd = &cobra.Command{ 31 | Use: "restart", 32 | Short: i18n.T("Restart your Jenkins"), 33 | Long: i18n.T("Restart your Jenkins"), 34 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 35 | jenkins := GetCurrentJenkinsFromOptions() 36 | if !restartOption.Confirm(fmt.Sprintf("Are you sure to restart Jenkins %s?", jenkins.URL)) { 37 | return 38 | } 39 | 40 | jClient := &client.CoreClient{ 41 | JenkinsCore: client.JenkinsCore{ 42 | RoundTripper: restartOption.RoundTripper, 43 | Debug: rootOptions.Debug, 44 | }, 45 | } 46 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 47 | 48 | if restartOption.Safe { 49 | err = jClient.Restart() 50 | } else { 51 | err = jClient.RestartDirectly() 52 | } 53 | 54 | if err == nil { 55 | cmd.Println("Please wait while Jenkins is restarting") 56 | } 57 | return 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /app/cmd/center_identity_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/golang/mock/gomock" 6 | "github.com/jenkins-zh/jenkins-cli/client" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "io/ioutil" 10 | "os" 11 | 12 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 13 | ) 14 | 15 | var _ = Describe("center identity command", func() { 16 | var ( 17 | ctrl *gomock.Controller 18 | roundTripper *mhttp.MockRoundTripper 19 | targetFilePath string 20 | 21 | err error 22 | ) 23 | 24 | BeforeEach(func() { 25 | ctrl = gomock.NewController(GinkgoT()) 26 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 27 | centerIdentityOption.RoundTripper = roundTripper 28 | targetFilePath = "jenkins.war" 29 | 30 | rootOptions.Jenkins = "" 31 | rootOptions.ConfigFile = "test.yaml" 32 | }) 33 | 34 | AfterEach(func() { 35 | rootCmd.SetArgs([]string{}) 36 | err = os.Remove(targetFilePath) 37 | ctrl.Finish() 38 | }) 39 | 40 | Context("basic cases", func() { 41 | It("should not error", func() { 42 | var data []byte 43 | data, err = GenerateSampleConfig() 44 | Expect(err).To(BeNil()) 45 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 46 | Expect(err).To(BeNil()) 47 | 48 | client.PrepareForGetIdentity(roundTripper, "http://localhost:8080/jenkins", 49 | "admin", "111e3a2f0231198855dceaff96f20540a9") 50 | 51 | buf := new(bytes.Buffer) 52 | rootCmd.SetOut(buf) 53 | rootCmd.SetArgs([]string{"center", "identity"}) 54 | _, err = rootCmd.ExecuteC() 55 | Expect(err).NotTo(HaveOccurred()) 56 | Expect(buf.String()).To(Equal(`{ 57 | "Fingerprint": "fingerprint", 58 | "PublicKey": "publicKey", 59 | "SystemMessage": "systemMessage" 60 | } 61 | `)) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /client/queue.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // QueueClient is the client of queue 9 | type QueueClient struct { 10 | JenkinsCore 11 | } 12 | 13 | // Get returns the job queue 14 | func (q *QueueClient) Get() (status *JobQueue, err error) { 15 | err = q.RequestWithData(http.MethodGet, "/queue/api/json", nil, nil, 200, &status) 16 | return 17 | } 18 | 19 | // Cancel will cancel a job from the queue 20 | func (q *QueueClient) Cancel(id int) (err error) { 21 | api := fmt.Sprintf("/queue/cancelItem?id=%d", id) 22 | var statusCode int 23 | if statusCode, err = q.RequestWithoutData(http.MethodPost, api, nil, nil, 302); err != nil && 24 | (statusCode == 200 || 25 | statusCode == 404) { // 404 should be an error, but no idea why it can be triggered successful 26 | err = nil 27 | } 28 | return 29 | } 30 | 31 | // JobQueue represent the job queue 32 | type JobQueue struct { 33 | Items []QueueItem 34 | } 35 | 36 | // QueueItem is the item of job queue 37 | type QueueItem struct { 38 | Blocked bool 39 | Buildable bool 40 | ID int 41 | Params string 42 | Pending bool 43 | Stuck bool 44 | URL string 45 | Why string 46 | BuildableStartMilliseconds int64 47 | InQueueSince int64 48 | Actions []CauseAction 49 | } 50 | 51 | // CauseAction is the collection of causes 52 | type CauseAction struct { 53 | Causes []Cause 54 | } 55 | 56 | // Cause represent the reason why job is triggered 57 | type Cause struct { 58 | UpstreamURL string 59 | UpstreamProject string 60 | UpstreamBuild int 61 | ShortDescription string 62 | } 63 | -------------------------------------------------------------------------------- /app/cmd/config_remove.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/keyring" 6 | "go.uber.org/zap" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | configCmd.AddCommand(configRemoveCmd) 14 | } 15 | 16 | var configRemoveCmd = &cobra.Command{ 17 | Use: "remove", 18 | Short: i18n.T("Remove a Jenkins config"), 19 | Long: i18n.T("Remove a Jenkins config"), 20 | Args: cobra.MinimumNArgs(1), 21 | ValidArgsFunction: ValidJenkinsNames, 22 | RunE: func(_ *cobra.Command, args []string) error { 23 | target := args[0] 24 | return removeJenkins(target) 25 | }, 26 | } 27 | 28 | func removeJenkins(name string) (err error) { 29 | current := getCurrentJenkins() 30 | if name == current.Name { 31 | err = fmt.Errorf("you cannot remove current Jenkins, if you want to remove it, can select other items before") 32 | return 33 | } 34 | 35 | index := -1 36 | config := getConfig() 37 | for i, jenkins := range config.JenkinsServers { 38 | if name == jenkins.Name { 39 | index = i 40 | break 41 | } 42 | } 43 | 44 | if index == -1 { 45 | err = fmt.Errorf("cannot found by name %s", name) 46 | } else { 47 | targetJenkins := config.JenkinsServers[index] 48 | if err = keyring.DelToken(targetJenkins); err == nil { 49 | logger.Info("delete keyring item successfully", zap.String("name", targetJenkins.Name)) 50 | } else { 51 | logger.Error("cannot delete keyring item", zap.String("name", targetJenkins.Name), 52 | zap.Error(err)) 53 | } 54 | 55 | config.JenkinsServers = append(config.JenkinsServers[:index], config.JenkinsServers[index+1:]...) 56 | err = saveConfig() 57 | } 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /app/cmd/computer_create_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp" 15 | ) 16 | 17 | var _ = Describe("create list command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | buf io.Writer 22 | ) 23 | 24 | BeforeEach(func() { 25 | ctrl = gomock.NewController(GinkgoT()) 26 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 27 | rootCmd.SetArgs([]string{}) 28 | buf = new(bytes.Buffer) 29 | rootCmd.SetOutput(buf) 30 | rootOptions.Jenkins = "" 31 | rootOptions.ConfigFile = "test.yaml" 32 | 33 | computerCreateOption.RoundTripper = roundTripper 34 | }) 35 | 36 | AfterEach(func() { 37 | rootCmd.SetArgs([]string{}) 38 | os.Remove(rootOptions.ConfigFile) 39 | rootOptions.ConfigFile = "" 40 | ctrl.Finish() 41 | }) 42 | 43 | Context("basic cases", func() { 44 | var ( 45 | err error 46 | ) 47 | 48 | BeforeEach(func() { 49 | var data []byte 50 | data, err = GenerateSampleConfig() 51 | Expect(err).To(BeNil()) 52 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 53 | Expect(err).To(BeNil()) 54 | }) 55 | 56 | It("should success", func() { 57 | name := "fake-name" 58 | 59 | computer.PrepareForComputerCreateRequest(roundTripper, "http://localhost:8080/jenkins", 60 | "admin", "111e3a2f0231198855dceaff96f20540a9", name) 61 | 62 | rootCmd.SetArgs([]string{"computer", "create", name}) 63 | _, err = rootCmd.ExecuteC() 64 | Expect(err).NotTo(HaveOccurred()) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /app/cmd/credential_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 6 | "net/http" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | 10 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // CredentialListOption option for credential list command 16 | type CredentialListOption struct { 17 | cobra_ext.OutputOption 18 | 19 | Store string 20 | 21 | RoundTripper http.RoundTripper 22 | } 23 | 24 | var credentialListOption CredentialListOption 25 | 26 | func init() { 27 | credentialCmd.AddCommand(credentialListCmd) 28 | credentialListCmd.Flags().StringVarP(&credentialListOption.Store, "store", "", "system", 29 | i18n.T("The store name of Jenkins credentials")) 30 | credentialListOption.SetFlagWithHeaders(credentialListCmd, "DisplayName,ID,TypeName,Description") 31 | } 32 | 33 | var credentialListCmd = &cobra.Command{ 34 | Use: "list", 35 | Short: i18n.T("List all credentials of Jenkins"), 36 | Long: i18n.T("List all credentials of Jenkins"), 37 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 38 | jClient := &client.CredentialsManager{ 39 | JenkinsCore: client.JenkinsCore{ 40 | RoundTripper: credentialListOption.RoundTripper, 41 | }, 42 | } 43 | getCurrentJenkinsAndClient(&(jClient.JenkinsCore)) 44 | 45 | var credentialList client.CredentialList 46 | if credentialList, err = jClient.GetList(credentialListOption.Store); err == nil { 47 | credentialListOption.Writer = cmd.OutOrStdout() 48 | err = credentialListOption.OutputV2(credentialList.Credentials) 49 | } 50 | return 51 | }, 52 | Annotations: map[string]string{ 53 | common.Since: common.VersionSince0024, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /app/cmd/computer_delete_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp" 15 | ) 16 | 17 | var _ = Describe("create delete command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | buf io.Writer 22 | ) 23 | 24 | BeforeEach(func() { 25 | ctrl = gomock.NewController(GinkgoT()) 26 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 27 | rootCmd.SetArgs([]string{}) 28 | buf = new(bytes.Buffer) 29 | rootCmd.SetOutput(buf) 30 | rootOptions.Jenkins = "" 31 | rootOptions.ConfigFile = "test.yaml" 32 | 33 | computerDeleteOption.RoundTripper = roundTripper 34 | }) 35 | 36 | AfterEach(func() { 37 | rootCmd.SetArgs([]string{}) 38 | os.Remove(rootOptions.ConfigFile) 39 | rootOptions.ConfigFile = "" 40 | ctrl.Finish() 41 | }) 42 | 43 | Context("basic cases", func() { 44 | var ( 45 | err error 46 | ) 47 | 48 | BeforeEach(func() { 49 | var data []byte 50 | data, err = GenerateSampleConfig() 51 | Expect(err).To(BeNil()) 52 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 53 | Expect(err).To(BeNil()) 54 | }) 55 | 56 | It("should success", func() { 57 | name := "fake-name" 58 | 59 | computer.PrepareForComputerDeleteRequest(roundTripper, "http://localhost:8080/jenkins", 60 | "admin", "111e3a2f0231198855dceaff96f20540a9", name) 61 | 62 | rootCmd.SetArgs([]string{"computer", "delete", name}) 63 | _, err = rootCmd.ExecuteC() 64 | Expect(err).NotTo(HaveOccurred()) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /app/cmd/plugin_download_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-cli/client" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/golang/mock/gomock" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("plugin download command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | ) 21 | 22 | BeforeEach(func() { 23 | ctrl = gomock.NewController(GinkgoT()) 24 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 25 | pluginDownloadOption.RoundTripper = roundTripper 26 | rootCmd.SetArgs([]string{}) 27 | rootOptions.Jenkins = "" 28 | rootOptions.ConfigFile = "test.yaml" 29 | }) 30 | 31 | AfterEach(func() { 32 | rootCmd.SetArgs([]string{}) 33 | os.Remove(rootOptions.ConfigFile) 34 | rootOptions.ConfigFile = "" 35 | ctrl.Finish() 36 | }) 37 | 38 | Context("basic cases", func() { 39 | It("should success", func() { 40 | var err error 41 | var data []byte 42 | data, err = GenerateSampleConfig() 43 | Expect(err).To(BeNil()) 44 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 45 | Expect(err).To(BeNil()) 46 | 47 | client.PrepareOnePluginWithOptionalDep(roundTripper, "fake") 48 | client.PrepareDownloadPlugin(roundTripper) 49 | 50 | rootCmd.SetArgs([]string{"plugin", "download", "fake", "--show-progress=false", "--use-mirror=false"}) 51 | 52 | buf := new(bytes.Buffer) 53 | rootCmd.SetOut(buf) 54 | _, err = rootCmd.ExecuteC() 55 | Expect(err).To(BeNil()) 56 | Expect(buf.String()).To(Equal("")) 57 | 58 | _, err = os.Stat("fake.hpi") 59 | defer os.Remove("fake.hpi") 60 | Expect(err).To(BeNil()) 61 | }) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /app/cmd/computer_log_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/golang/mock/gomock" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("computer log command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | buf *bytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 26 | rootCmd.SetArgs([]string{}) 27 | buf = new(bytes.Buffer) 28 | rootCmd.SetOutput(buf) 29 | rootOptions.Jenkins = "" 30 | rootOptions.ConfigFile = "test.yaml" 31 | 32 | computerLogOption.RoundTripper = roundTripper 33 | }) 34 | 35 | AfterEach(func() { 36 | rootCmd.SetArgs([]string{}) 37 | os.Remove(rootOptions.ConfigFile) 38 | rootOptions.ConfigFile = "" 39 | ctrl.Finish() 40 | }) 41 | 42 | Context("basic cases", func() { 43 | var ( 44 | err error 45 | ) 46 | 47 | BeforeEach(func() { 48 | var data []byte 49 | data, err = GenerateSampleConfig() 50 | Expect(err).To(BeNil()) 51 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 52 | Expect(err).To(BeNil()) 53 | }) 54 | 55 | It("should success", func() { 56 | name := "fake" 57 | 58 | computer.PrepareForComputerLogRequest(roundTripper, "http://localhost:8080/jenkins", 59 | "admin", "111e3a2f0231198855dceaff96f20540a9", name) 60 | 61 | rootCmd.SetArgs([]string{"computer", "log", name}) 62 | _, err = rootCmd.ExecuteC() 63 | Expect(err).NotTo(HaveOccurred()) 64 | Expect(buf.String()).To(Equal("fake-log")) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /e2e/withdependencies/setup_test.go: -------------------------------------------------------------------------------- 1 | package withdependencies 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/e2e" 6 | "github.com/phayes/freeport" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | ) 12 | 13 | var jenkinsURL string 14 | 15 | func GetJenkinsURL() string { 16 | return jenkinsURL 17 | } 18 | 19 | func TestMain(m *testing.M) { 20 | var err error 21 | 22 | version := os.Getenv("JENKINS_VERSION") 23 | os.Setenv("PATH", ".:"+os.Getenv("PATH")) 24 | 25 | javaHome := os.Getenv("JCLI_JAVA_HOME") 26 | if javaHome != "" { 27 | os.Setenv("PATH", javaHome+"/bin:"+os.Getenv("PATH")) 28 | } 29 | if err = os.Setenv("JCLI_CONFIG_LOAD", "false"); err != nil { 30 | panic(err) 31 | } 32 | if version == "" { 33 | return 34 | } 35 | 36 | var port int 37 | if port, err = freeport.GetFreePort(); err != nil { 38 | fmt.Println("get free port error", err) 39 | panic(err) 40 | } 41 | jenkinsURL = fmt.Sprintf("http://%s:%d", e2e.GetLocalIP(), port) 42 | 43 | cmd := exec.Command("jcli", "center", "start", "--random-web-dir", "--setup-wizard=false", "--port", fmt.Sprintf("%d", port), "--version", version) 44 | fmt.Println(cmd.String()) 45 | e2e.RunAndWait(cmd, func(reader io.ReadCloser) { 46 | e2e.WaitJenkinsRunningUp(reader) 47 | 48 | e2e.ExecuteCmd("plugin", "check", "--url", GetJenkinsURL()) 49 | e2e.InstallPlugin("localization-zh-cn", GetJenkinsURL(), true) 50 | 51 | e2e.RestartAndWait(GetJenkinsURL(), reader) 52 | 53 | e2e.ExecuteCmd("center", "mirror", "--url", GetJenkinsURL()) 54 | e2e.ExecuteCmd("plugin", "check", "--url", GetJenkinsURL()) 55 | e2e.InstallPlugin("configuration-as-code", GetJenkinsURL(), true) 56 | e2e.InstallPlugin("pipeline-restful-api", GetJenkinsURL(), true) 57 | 58 | e2e.RestartAndWait(GetJenkinsURL(), reader) 59 | 60 | m.Run() 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /app/cmd/user_token.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // UserTokenOption represents a user token cmd option 13 | type UserTokenOption struct { 14 | Generate bool 15 | Name string 16 | TargetUser string 17 | 18 | RoundTripper http.RoundTripper 19 | } 20 | 21 | var userTokenOption UserTokenOption 22 | 23 | func init() { 24 | userCmd.AddCommand(userTokenCmd) 25 | userTokenCmd.Flags().BoolVarP(&userTokenOption.Generate, "generate", "g", false, 26 | i18n.T("Generate the token")) 27 | userTokenCmd.Flags().StringVarP(&userTokenOption.Name, "name", "n", "", 28 | i18n.T("Name of the token")) 29 | userTokenCmd.Flags().StringVarP(&userTokenOption.TargetUser, "target-user", "", "", 30 | i18n.T("The target user of the new token")) 31 | } 32 | 33 | var userTokenCmd = &cobra.Command{ 34 | Use: "token", 35 | Short: i18n.T("Token the user of your Jenkins"), 36 | Long: i18n.T("Token the user of your Jenkins"), 37 | Example: `jcli user token -g`, 38 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 39 | if !userTokenOption.Generate { 40 | cmd.Help() 41 | return 42 | } 43 | 44 | jclient := &client.UserClient{ 45 | JenkinsCore: client.JenkinsCore{ 46 | RoundTripper: userTokenOption.RoundTripper, 47 | Debug: rootOptions.Debug, 48 | }, 49 | } 50 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 51 | 52 | var token *client.Token 53 | token, err = jclient.CreateToken(userTokenOption.TargetUser, userTokenOption.Name) 54 | if err == nil { 55 | var data []byte 56 | data, err = userOption.Output(token) 57 | if err == nil { 58 | cmd.Println(string(data)) 59 | } 60 | } 61 | return 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /app/config/type.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | // ANNOTATION_CONFIG_LOAD annotation for config loading set 5 | ANNOTATION_CONFIG_LOAD string = "config.load" 6 | ) 7 | 8 | // JenkinsServer holds the configuration of your Jenkins 9 | type JenkinsServer struct { 10 | Name string `yaml:"name"` 11 | URL string `yaml:"url"` 12 | UserName string `yaml:"username"` 13 | Token string `yaml:"token"` 14 | Proxy string `yaml:"proxy,omitempty"` 15 | ProxyAuth string `yaml:"proxyAuth,omitempty"` 16 | InsecureSkipVerify bool `yaml:"insecureSkipVerify,omitempty"` 17 | Description string `yaml:"description,omitempty"` 18 | Data map[string]string `yaml:"data,omitempty"` 19 | } 20 | 21 | // CommandHook is a hook 22 | type CommandHook struct { 23 | Path string `yaml:"path"` 24 | Command string `yaml:"cmd"` 25 | } 26 | 27 | // PluginSuite define a suite of plugins 28 | type PluginSuite struct { 29 | Name string `yaml:"name"` 30 | Plugins []string `yaml:"plugins"` 31 | Description string `yaml:"description"` 32 | } 33 | 34 | // JenkinsMirror represents the mirror of Jenkins 35 | type JenkinsMirror struct { 36 | Name string 37 | URL string 38 | } 39 | 40 | // Config is a global config struct 41 | type Config struct { 42 | Current string `yaml:"current"` 43 | Language string `yaml:"language,omitempty"` 44 | JenkinsServers []JenkinsServer `yaml:"jenkins_servers"` 45 | PreHooks []CommandHook `yaml:"preHooks,omitempty"` 46 | PostHooks []CommandHook `yaml:"postHooks,omitempty"` 47 | PluginSuites []PluginSuite `yaml:"pluginSuites,omitempty"` 48 | Mirrors []JenkinsMirror `yaml:"mirrors"` 49 | } 50 | -------------------------------------------------------------------------------- /app/cmd/runner_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 13 | ) 14 | 15 | var _ = Describe("Runner test command", func() { 16 | var ( 17 | ctrl *gomock.Controller 18 | roundTripper *mhttp.MockRoundTripper 19 | ) 20 | 21 | BeforeEach(func() { 22 | ctrl = gomock.NewController(GinkgoT()) 23 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 24 | runnerOption.RoundTripper = roundTripper 25 | rootCmd.SetArgs([]string{}) 26 | rootOptions.Jenkins = "" 27 | rootOptions.ConfigFile = "test.yaml" 28 | }) 29 | 30 | AfterEach(func() { 31 | rootCmd.SetArgs([]string{}) 32 | os.Remove(rootOptions.ConfigFile) 33 | rootOptions.ConfigFile = "" 34 | ctrl.Finish() 35 | }) 36 | 37 | Context("basic cases", func() { 38 | It("should pass", func() { 39 | data, err := GenerateSampleConfig() 40 | Expect(err).To(BeNil()) 41 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 42 | Expect(err).To(BeNil()) 43 | rootCmd.SetArgs([]string{"runner", "--plugin-path=home/sladyn/plugin.txt", "--jenkinsfile-path=home/sladyn/Jenkinsfile"}) 44 | buf := new(bytes.Buffer) 45 | rootCmd.SetOutput(buf) 46 | _, err = rootCmd.ExecuteC() 47 | Expect(err).To(BeNil()) 48 | }) 49 | 50 | It("Empty file path", func() { 51 | data, err := GenerateSampleConfig() 52 | Expect(err).To(BeNil()) 53 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 54 | Expect(err).To(BeNil()) 55 | rootCmd.SetArgs([]string{"runner", "--path=home/sladyn/sladyn.go"}) 56 | buf := new(bytes.Buffer) 57 | rootCmd.SetOutput(buf) 58 | _, err = rootCmd.ExecuteC() 59 | Expect(err).To(HaveOccurred()) 60 | }) 61 | 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /app/cmd/plugin_upload_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/jenkins-zh/jenkins-cli/client" 13 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("plugin upload command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | ) 21 | 22 | BeforeEach(func() { 23 | ctrl = gomock.NewController(GinkgoT()) 24 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 25 | pluginUploadOption.RoundTripper = roundTripper 26 | rootCmd.SetArgs([]string{}) 27 | rootOptions.Jenkins = "" 28 | rootOptions.ConfigFile = "test.yaml" 29 | }) 30 | 31 | AfterEach(func() { 32 | rootCmd.SetArgs([]string{}) 33 | os.Remove(rootOptions.ConfigFile) 34 | rootOptions.ConfigFile = "" 35 | ctrl.Finish() 36 | }) 37 | 38 | Context("basic cases", func() { 39 | It("should success", func() { 40 | data, err := GenerateSampleConfig() 41 | Expect(err).To(BeNil()) 42 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 43 | Expect(err).To(BeNil()) 44 | 45 | tmpfile, err := ioutil.TempFile("", "example") 46 | Expect(err).To(BeNil()) 47 | 48 | request, _, requestCrumb, _ := client.PrepareForUploadPlugin(roundTripper, "http://localhost:8080/jenkins") 49 | request.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") 50 | requestCrumb.SetBasicAuth("admin", "111e3a2f0231198855dceaff96f20540a9") 51 | 52 | rootCmd.SetArgs([]string{"plugin", "upload", tmpfile.Name(), "--show-progress=false"}) 53 | 54 | buf := new(bytes.Buffer) 55 | rootCmd.SetOutput(buf) 56 | _, err = rootCmd.ExecuteC() 57 | Expect(err).To(BeNil()) 58 | 59 | Expect(buf.String()).To(Equal("")) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /app/cmd/config_add_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("config add command", func() { 16 | var ( 17 | ctrl *gomock.Controller 18 | buf io.Writer 19 | err error 20 | configPath string 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | rootCmd.SetArgs([]string{}) 26 | buf = new(bytes.Buffer) 27 | rootCmd.SetOutput(buf) 28 | rootOptions.Jenkins = "" 29 | 30 | configPath = path.Join(os.TempDir(), "fake.yaml") 31 | 32 | var data []byte 33 | data, err = GenerateSampleConfig() 34 | Expect(err).To(BeNil()) 35 | err = ioutil.WriteFile(configPath, data, 0664) 36 | Expect(err).To(BeNil()) 37 | }) 38 | 39 | AfterEach(func() { 40 | rootCmd.SetArgs([]string{}) 41 | os.Remove(configPath) 42 | rootOptions.ConfigFile = "" 43 | ctrl.Finish() 44 | }) 45 | 46 | Context("basic cases", func() { 47 | It("lack of name", func() { 48 | rootCmd.SetArgs([]string{"config", "add", "--configFile", configPath}) 49 | _, err = rootCmd.ExecuteC() 50 | Expect(err).To(HaveOccurred()) 51 | Expect(err.Error()).To(ContainSubstring("name cannot be empty")) 52 | }) 53 | 54 | It("add an exist one", func() { 55 | rootCmd.SetArgs([]string{"config", "add", "--name", "yourServer", "--configFile", configPath}) 56 | _, err = rootCmd.ExecuteC() 57 | Expect(err).To(HaveOccurred()) 58 | Expect(err.Error()).To(ContainSubstring("jenkins yourServer is existed")) 59 | }) 60 | 61 | It("should success", func() { 62 | rootCmd.SetArgs([]string{"config", "add", "--name", "fake", "--configFile", configPath}) 63 | _, err = rootCmd.ExecuteC() 64 | Expect(err).NotTo(HaveOccurred()) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /app/cmd/config_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // ConfigListOption option for config list command 12 | type ConfigListOption struct { 13 | cobra_ext.OutputOption 14 | 15 | Config string 16 | } 17 | 18 | var configListOption ConfigListOption 19 | 20 | func init() { 21 | configCmd.AddCommand(configListCmd) 22 | configListCmd.Flags().StringVarP(&configListOption.Config, "config", "", "JenkinsServers", 23 | i18n.T("The type of config items, contains PreHooks, PostHooks, Mirrors, PluginSuites")) 24 | configListOption.SetFlagWithHeaders(configListCmd, "Name,URL,Description") 25 | } 26 | 27 | var configListCmd = &cobra.Command{ 28 | Use: "list", 29 | Short: i18n.T("List all Jenkins config items"), 30 | Long: i18n.T("List all Jenkins config items"), 31 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 32 | configListOption.Writer = cmd.OutOrStdout() 33 | 34 | config := getConfig() 35 | if config == nil { 36 | return fmt.Errorf("no config file found") 37 | } 38 | 39 | switch configListOption.Config { 40 | case "JenkinsServers": 41 | err = configListOption.OutputV2(config.JenkinsServers) 42 | case "PreHooks": 43 | configListOption.Columns = "Path,Command" 44 | err = configListOption.OutputV2(config.PreHooks) 45 | case "PostHooks": 46 | configListOption.Columns = "Path,Command" 47 | err = configListOption.OutputV2(config.PostHooks) 48 | case "Mirrors": 49 | configListOption.Columns = "Name,URL" 50 | err = configListOption.OutputV2(config.Mirrors) 51 | case "PluginSuites": 52 | configListOption.Columns = "Name,Description" 53 | err = configListOption.OutputV2(config.PluginSuites) 54 | default: 55 | err = fmt.Errorf("unknow config %s", configListOption.Config) 56 | } 57 | return 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /app/cmd/job_log_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("job log command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | rootURL string 21 | username string 22 | token string 23 | ) 24 | 25 | BeforeEach(func() { 26 | ctrl = gomock.NewController(GinkgoT()) 27 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 28 | jobLogOption.RoundTripper = roundTripper 29 | rootCmd.SetArgs([]string{}) 30 | rootOptions.Jenkins = "" 31 | rootOptions.ConfigFile = "test.yaml" 32 | 33 | rootURL = "http://localhost:8080/jenkins" 34 | username = "admin" 35 | token = "111e3a2f0231198855dceaff96f20540a9" 36 | }) 37 | 38 | AfterEach(func() { 39 | rootCmd.SetArgs([]string{}) 40 | os.Remove(rootOptions.ConfigFile) 41 | rootOptions.ConfigFile = "" 42 | ctrl.Finish() 43 | }) 44 | 45 | Context("basic cases, need RoundTripper", func() { 46 | It("output the last build log", func() { 47 | data, err := GenerateSampleConfig() 48 | Expect(err).To(BeNil()) 49 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 50 | Expect(err).To(BeNil()) 51 | 52 | jobName := "fake" 53 | client.PrepareForGetBuild(roundTripper, rootURL, jobName, -1, username, token) 54 | client.PrepareForJobLog(roundTripper, rootURL, jobName, -1, username, token) 55 | rootCmd.SetArgs([]string{"job", "log", jobName}) 56 | 57 | buf := new(bytes.Buffer) 58 | rootCmd.SetOutput(buf) 59 | _, err = rootCmd.ExecuteC() 60 | Expect(err).To(BeNil()) 61 | 62 | Expect(buf.String()).To(Equal("Current build number: 0\nCurrent build url: \nfake log")) 63 | }) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /app/cmd/plugin_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "os" 6 | 7 | "github.com/jenkins-zh/jenkins-cli/util" 8 | 9 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // PluginRunOptions for the plugin run command 15 | type PluginRunOptions struct { 16 | common.Option 17 | 18 | CleanHome bool 19 | DebugOutput bool 20 | } 21 | 22 | var pluginRunOptions PluginRunOptions 23 | 24 | func init() { 25 | pluginCmd.AddCommand(pluginRunCmd) 26 | 27 | flags := pluginRunCmd.Flags() 28 | flags.BoolVar(&pluginRunOptions.DebugOutput, "debug-output", false, 29 | i18n.T("If you want the maven output the debug info")) 30 | flags.BoolVarP(&pluginRunOptions.CleanHome, "clean-home", "", false, 31 | i18n.T("If you want to clean the JENKINS_HOME before start it")) 32 | } 33 | 34 | var pluginRunCmd = &cobra.Command{ 35 | Use: "run", 36 | Short: i18n.T("Run the Jenkins plugin project"), 37 | Long: i18n.T(`Run the Jenkins plugin project 38 | The default behaviour is "mvn hpi:run"`), 39 | PreRunE: func(cmd *cobra.Command, args []string) (err error) { 40 | if pluginRunOptions.CleanHome { 41 | err = os.RemoveAll("work") 42 | } 43 | return 44 | }, 45 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 46 | binary, err := util.LookPath("mvn", pluginRunOptions.LookPathContext) 47 | if err == nil { 48 | env := os.Environ() 49 | 50 | mvnArgs := []string{"mvn"} 51 | if pluginRunOptions.DebugOutput { 52 | mvnArgs = append(mvnArgs, "-X") 53 | } 54 | if pluginRunOptions.CleanHome { 55 | mvnArgs = append(mvnArgs, "clean") 56 | } 57 | mvnArgs = append(mvnArgs, []string{"hpi:run", "-Dhpi.prefix=/", "-Djetty.port=8080"}...) 58 | err = util.Exec(binary, mvnArgs, env, pluginRunOptions.SystemCallExec) 59 | } 60 | return 61 | }, 62 | Annotations: map[string]string{ 63 | common.Since: "v0.0.31", 64 | }, 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/backup.yaml: -------------------------------------------------------------------------------- 1 | name: Backup Git repository 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | BackupBinary: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Set up Go 1.16 14 | uses: actions/setup-go@v5.2.0 15 | with: 16 | go-version: 1.23 17 | id: go 18 | - uses: actions/checkout@v4 19 | - name: Upgrade upx 20 | run: | 21 | # try to fix https://github.com/jenkins-zh/jenkins-cli/issues/493 22 | wget https://github.com/upx/upx/releases/download/v3.96/upx-3.96-amd64_linux.tar.xz 23 | tar xvf upx-3.96-amd64_linux.tar.xz 24 | upx-3.96-amd64_linux/upx -V 25 | sudo mv upx-3.96-amd64_linux/upx $(which upx) 26 | upx -V 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v6.1.0 29 | with: 30 | version: latest 31 | args: release --rm-dist --snapshot 32 | BackupGit: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v4 36 | - name: backup 37 | uses: jenkins-zh/git-backup-actions@v0.0.8 38 | env: 39 | GIT_DEPLOY_KEY: ${{ secrets.GIT_DEPLOY_KEY }} 40 | TARGET_GIT: "git@gitee.com:jenkins-zh/jenkins-cli.git" 41 | UnitTest: 42 | name: Test 43 | runs-on: ubuntu-20.04 44 | steps: 45 | - name: Set up Go 1.16 46 | uses: actions/setup-go@v5.2.0 47 | with: 48 | go-version: 1.23 49 | id: go 50 | - name: Check out code into the Go module directory 51 | uses: actions/checkout@v4 52 | - name: Test 53 | run: | 54 | make test 55 | - name: Upload coverage to Codecov 56 | uses: codecov/codecov-action@v5.1.2 57 | with: 58 | token: ${{ secrets.CODECOV_TOKEN }} 59 | files: coverage.out 60 | flags: unittests 61 | name: codecov-umbrella 62 | fail_ci_if_error: true 63 | -------------------------------------------------------------------------------- /app/cmd/credential_list_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 15 | ) 16 | 17 | var _ = Describe("credential list command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | buf *bytes.Buffer 22 | store string 23 | ) 24 | 25 | BeforeEach(func() { 26 | ctrl = gomock.NewController(GinkgoT()) 27 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 28 | rootCmd.SetArgs([]string{}) 29 | buf = new(bytes.Buffer) 30 | rootCmd.SetOutput(buf) 31 | rootOptions.Jenkins = "" 32 | rootOptions.ConfigFile = "test.yaml" 33 | 34 | credentialListOption.RoundTripper = roundTripper 35 | 36 | store = "system" 37 | }) 38 | 39 | AfterEach(func() { 40 | rootCmd.SetArgs([]string{}) 41 | os.Remove(rootOptions.ConfigFile) 42 | rootOptions.ConfigFile = "" 43 | ctrl.Finish() 44 | }) 45 | 46 | Context("basic cases", func() { 47 | var ( 48 | err error 49 | ) 50 | 51 | BeforeEach(func() { 52 | var data []byte 53 | data, err = GenerateSampleConfig() 54 | Expect(err).To(BeNil()) 55 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 56 | Expect(err).To(BeNil()) 57 | }) 58 | 59 | It("should success", func() { 60 | client.PrepareForGetCredentialList(roundTripper, "http://localhost:8080/jenkins", 61 | "admin", "111e3a2f0231198855dceaff96f20540a9", store) 62 | 63 | rootCmd.SetArgs([]string{"credential", "list"}) 64 | _, err = rootCmd.ExecuteC() 65 | Expect(err).To(BeNil()) 66 | Expect(buf.String()).To(Equal(`DisplayName ID TypeName Description 67 | displayName 19c27487-acca-4a39-9889-9ddd500388f3 Username with password 68 | `)) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /app/cmd/computer_list_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/golang/mock/gomock" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | 13 | "github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp" 14 | ) 15 | 16 | var _ = Describe("computer list command", func() { 17 | var ( 18 | ctrl *gomock.Controller 19 | roundTripper *mhttp.MockRoundTripper 20 | buf *bytes.Buffer 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 26 | rootCmd.SetArgs([]string{}) 27 | buf = new(bytes.Buffer) 28 | rootCmd.SetOutput(buf) 29 | rootOptions.Jenkins = "" 30 | rootOptions.ConfigFile = "test.yaml" 31 | 32 | computerListOption.RoundTripper = roundTripper 33 | }) 34 | 35 | AfterEach(func() { 36 | rootCmd.SetArgs([]string{}) 37 | os.Remove(rootOptions.ConfigFile) 38 | rootOptions.ConfigFile = "" 39 | ctrl.Finish() 40 | }) 41 | 42 | Context("basic cases", func() { 43 | var ( 44 | err error 45 | ) 46 | 47 | BeforeEach(func() { 48 | var data []byte 49 | data, err = GenerateSampleConfig() 50 | Expect(err).To(BeNil()) 51 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 52 | Expect(err).To(BeNil()) 53 | }) 54 | 55 | It("should success", func() { 56 | computer.PrepareForComputerListRequest(roundTripper, "http://localhost:8080/jenkins", "admin", "111e3a2f0231198855dceaff96f20540a9") 57 | 58 | rootCmd.SetArgs([]string{"computer", "list"}) 59 | _, err = rootCmd.ExecuteC() 60 | Expect(err).To(BeNil()) 61 | }) 62 | 63 | It("with a fake jenkins as option", func() { 64 | rootCmd.SetArgs([]string{"computer", "list", "--jenkins", "fake"}) 65 | _, err := rootCmd.ExecuteC() 66 | Expect(err).To(HaveOccurred()) 67 | Expect(buf.String()).To(ContainSubstring("cannot found the configuration")) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /client/casc_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | "github.com/golang/mock/gomock" 5 | "github.com/jenkins-zh/jenkins-cli/client" 6 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("", func() { 12 | var ( 13 | ctrl *gomock.Controller 14 | roundTripper *mhttp.MockRoundTripper 15 | cascManager client.CASCManager 16 | ) 17 | 18 | BeforeEach(func() { 19 | ctrl = gomock.NewController(GinkgoT()) 20 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 21 | cascManager = client.CASCManager{} 22 | cascManager.RoundTripper = roundTripper 23 | cascManager.URL = "http://localhost" 24 | }) 25 | 26 | AfterEach(func() { 27 | ctrl.Finish() 28 | }) 29 | 30 | It("normal cases", func() { 31 | client.PrepareForSASCReload(roundTripper, cascManager.URL, "", "") 32 | client.PrepareForSASCApply(roundTripper, cascManager.URL, "", "") 33 | client.PrepareForSASCExport(roundTripper, cascManager.URL, "", "") 34 | client.PrepareForSASCSchema(roundTripper, cascManager.URL, "", "") 35 | 36 | reloadErr := cascManager.Reload() 37 | applyErr := cascManager.Apply() 38 | config, exportErr := cascManager.Export() 39 | schema, schemaErr := cascManager.Schema() 40 | 41 | Expect(reloadErr).NotTo(HaveOccurred()) 42 | Expect(applyErr).NotTo(HaveOccurred()) 43 | Expect(exportErr).NotTo(HaveOccurred()) 44 | Expect(schemaErr).NotTo(HaveOccurred()) 45 | 46 | Expect(config).To(Equal("sample")) 47 | Expect(schema).To(Equal("sample")) 48 | }) 49 | 50 | Context("with error code", func() { 51 | BeforeEach(func() { 52 | client.PrepareForSASCExportWithCode(roundTripper, cascManager.URL, "", "", 500) 53 | client.PrepareForSASCSchemaWithCode(roundTripper, cascManager.URL, "", "", 500) 54 | }) 55 | 56 | It("get error", func() { 57 | _, exportErr := cascManager.Export() 58 | _, schemaErr := cascManager.Schema() 59 | 60 | Expect(exportErr).To(HaveOccurred()) 61 | Expect(schemaErr).To(HaveOccurred()) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /app/cmd/computer.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | appCfg "github.com/jenkins-zh/jenkins-cli/app/config" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | "github.com/jenkins-zh/jenkins-client/pkg/computer" 8 | "github.com/jenkins-zh/jenkins-client/pkg/core" 9 | "github.com/spf13/cobra" 10 | "strings" 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(computerCmd) 15 | } 16 | 17 | var computerCmd = &cobra.Command{ 18 | Use: "computer", 19 | Aliases: []string{"cpu", "agent"}, 20 | Short: i18n.T("Manage the computers of your Jenkins"), 21 | Long: i18n.T(`Manage the computers of your Jenkins`), 22 | } 23 | 24 | // GetComputerClient returns the client of computer 25 | func GetComputerClient(option common.Option) (*computer.Client, *appCfg.JenkinsServer) { 26 | jClient := &computer.Client{ 27 | JenkinsCore: core.JenkinsCore{ 28 | RoundTripper: option.RoundTripper, 29 | }, 30 | } 31 | return jClient, getCurrentJenkinsAndClientV2(&(jClient.JenkinsCore)) 32 | } 33 | 34 | // ValidAgentNames autocomplete with agent names 35 | func ValidAgentNames(cmd *cobra.Command, args []string, prefix string) (agentNames []string, directive cobra.ShellCompDirective) { 36 | directive = cobra.ShellCompDirectiveNoFileComp 37 | agentNames = make([]string, 0) 38 | 39 | jClient, _ := GetComputerClient(computerListOption.Option) 40 | if jClient != nil { 41 | if computers, err := jClient.List(); err == nil { 42 | for i := range computers.Computer { 43 | agent := computers.Computer[i] 44 | 45 | // handle it according different cmd 46 | if (cmd.Use == "start" || cmd.Use == "launch") && !agent.Offline { 47 | continue 48 | } 49 | 50 | duplicated := false 51 | for j := range args { 52 | if agent.DisplayName == args[j] { 53 | duplicated = true 54 | break 55 | } 56 | } 57 | 58 | if !duplicated && strings.HasPrefix(agent.DisplayName, prefix) { 59 | agentNames = append(agentNames, agent.DisplayName) 60 | } 61 | } 62 | } 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /app/cmd/config_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | appCfg "github.com/jenkins-zh/jenkins-cli/app/config" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // ConfigAddOptions is the config ad option 12 | type ConfigAddOptions struct { 13 | appCfg.JenkinsServer 14 | } 15 | 16 | var configAddOptions ConfigAddOptions 17 | 18 | func init() { 19 | configCmd.AddCommand(configAddCmd) 20 | configAddCmd.Flags().StringVarP(&configAddOptions.Name, "name", "n", "", 21 | i18n.T("Name of the Jenkins")) 22 | configAddCmd.Flags().StringVarP(&configAddOptions.URL, "url", "", "", 23 | i18n.T("URL of the Jenkins")) 24 | configAddCmd.Flags().StringVarP(&configAddOptions.UserName, "username", "u", "", 25 | i18n.T("UserName of the Jenkins")) 26 | configAddCmd.Flags().StringVarP(&configAddOptions.Token, "token", "t", "", 27 | i18n.T("Token of the Jenkins")) 28 | configAddCmd.Flags().StringVarP(&configAddOptions.Proxy, "proxy", "p", "", 29 | i18n.T("Proxy of the Jenkins")) 30 | configAddCmd.Flags().StringVarP(&configAddOptions.ProxyAuth, "proxyAuth", "a", "", 31 | i18n.T("ProxyAuth of the Jenkins")) 32 | configAddCmd.Flags().StringVarP(&configAddOptions.Description, "description", "d", "", 33 | i18n.T("Description of the Jenkins")) 34 | } 35 | 36 | var configAddCmd = &cobra.Command{ 37 | Use: "add", 38 | Short: i18n.T("Add a Jenkins config item"), 39 | Long: i18n.T("Add a Jenkins config item"), 40 | RunE: func(_ *cobra.Command, _ []string) error { 41 | return addJenkins(configAddOptions.JenkinsServer) 42 | }, 43 | Example: "jcli config add -n demo", 44 | } 45 | 46 | func addJenkins(jenkinsServer appCfg.JenkinsServer) (err error) { 47 | jenkinsName := jenkinsServer.Name 48 | if jenkinsName == "" { 49 | err = fmt.Errorf("name cannot be empty") 50 | return 51 | } 52 | 53 | if findJenkinsByName(jenkinsName) != nil { 54 | err = fmt.Errorf("jenkins %s is existed", jenkinsName) 55 | return 56 | } 57 | 58 | config.JenkinsServers = append(config.JenkinsServers, jenkinsServer) 59 | err = saveConfig() 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /client/artifacts.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Artifact represents the artifacts from Jenkins build 10 | type Artifact struct { 11 | ID string 12 | Name string 13 | Path string 14 | URL string 15 | Size int64 16 | } 17 | 18 | // JobWithArtifacts is the artifacts from a job 19 | type JobWithArtifacts struct { 20 | Artifacts []JobArtifact `json:"artifacts"` 21 | } 22 | 23 | // GetArtifacts gets the artifacts from the JobWithArtifacts object 24 | func (j JobWithArtifacts) GetArtifacts() (artifacts []Artifact) { 25 | for _, a := range j.Artifacts { 26 | artifacts = append(artifacts, Artifact{ 27 | ID: a.FileName, 28 | Name: a.FileName, 29 | Path: a.RelativePath, 30 | }) 31 | } 32 | return 33 | } 34 | 35 | // JobArtifact represents the artifact object 36 | type JobArtifact struct { 37 | RelativePath string `json:"relativePath"` 38 | FileName string `json:"fileName"` 39 | } 40 | 41 | // ArtifactClient is client for getting the artifacts 42 | type ArtifactClient struct { 43 | JenkinsCore 44 | } 45 | 46 | // List get the list of artifacts from a build 47 | func (q *ArtifactClient) List(jobName string, buildID int) (artifacts []Artifact, err error) { 48 | path := ParseJobPath(jobName) 49 | var api string 50 | var oldAPI string 51 | if buildID < 1 { 52 | api = fmt.Sprintf("%s/lastBuild/wfapi/artifacts", path) 53 | oldAPI = fmt.Sprintf("%s/lastBuild/api/json", path) 54 | } else { 55 | api = fmt.Sprintf("%s/%d/wfapi/artifacts", path, buildID) 56 | oldAPI = fmt.Sprintf("%s/%d/api/json", path, buildID) 57 | } 58 | err = q.RequestWithData(http.MethodGet, api, nil, nil, 200, &artifacts) 59 | if err != nil { 60 | job := JobWithArtifacts{} 61 | if err = q.RequestWithData(http.MethodGet, oldAPI, nil, nil, 200, &job); err == nil { 62 | artifacts = job.GetArtifacts() 63 | 64 | for i := 0; i < len(artifacts); i++ { 65 | if artifacts[i].URL == "" { 66 | artifacts[i].URL = strings.ReplaceAll(oldAPI, "api/json", "artifact/") + artifacts[i].Path 67 | } 68 | } 69 | } 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /client/release.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "github.com/google/go-github/v29/github" 6 | ) 7 | 8 | // GitHubReleaseClient is the client of jcli github 9 | type GitHubReleaseClient struct { 10 | Client *github.Client 11 | } 12 | 13 | // ReleaseAsset is the asset from GitHub release 14 | type ReleaseAsset struct { 15 | TagName string 16 | Body string 17 | } 18 | 19 | // Init init the GitHub client 20 | func (g *GitHubReleaseClient) Init() { 21 | g.Client = github.NewClient(nil) 22 | } 23 | 24 | // GetLatestJCLIAsset returns the latest jcli asset 25 | func (g *GitHubReleaseClient) GetLatestJCLIAsset() (*ReleaseAsset, error) { 26 | return g.GetLatestReleaseAsset("jenkins-zh", "jenkins-cli") 27 | } 28 | 29 | // GetLatestReleaseAsset returns the latest release asset 30 | func (g *GitHubReleaseClient) GetLatestReleaseAsset(owner, repo string) (ra *ReleaseAsset, err error) { 31 | ctx := context.Background() 32 | 33 | var release *github.RepositoryRelease 34 | if release, _, err = g.Client.Repositories.GetLatestRelease(ctx, owner, repo); err == nil { 35 | ra = &ReleaseAsset{ 36 | TagName: release.GetTagName(), 37 | Body: release.GetBody(), 38 | } 39 | } 40 | return 41 | } 42 | 43 | // GetJCLIAsset returns the asset from a tag name 44 | func (g *GitHubReleaseClient) GetJCLIAsset(tagName string) (*ReleaseAsset, error) { 45 | return g.GetReleaseAssetByTagName("jenkins-zh", "jenkins-cli", tagName) 46 | } 47 | 48 | // GetReleaseAssetByTagName returns the release asset by tag name 49 | func (g *GitHubReleaseClient) GetReleaseAssetByTagName(owner, repo, tagName string) (ra *ReleaseAsset, err error) { 50 | ctx := context.Background() 51 | 52 | opt := &github.ListOptions{ 53 | PerPage: 99999, 54 | } 55 | 56 | var releaseList []*github.RepositoryRelease 57 | if releaseList, _, err = g.Client.Repositories.ListReleases(ctx, owner, repo, opt); err == nil { 58 | for _, item := range releaseList { 59 | if item.GetTagName() == tagName { 60 | ra = &ReleaseAsset{ 61 | TagName: item.GetTagName(), 62 | Body: item.GetBody(), 63 | } 64 | break 65 | } 66 | } 67 | } 68 | return 69 | } 70 | -------------------------------------------------------------------------------- /app/cmd/queue_cancel_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/jenkins-zh/jenkins-cli/client" 10 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("queue cancel command", func() { 16 | var ( 17 | ctrl *gomock.Controller 18 | roundTripper *mhttp.MockRoundTripper 19 | ) 20 | 21 | BeforeEach(func() { 22 | ctrl = gomock.NewController(GinkgoT()) 23 | rootCmd.SetArgs([]string{}) 24 | rootOptions.Jenkins = "" 25 | rootOptions.ConfigFile = "test.yaml" 26 | }) 27 | 28 | AfterEach(func() { 29 | rootCmd.SetArgs([]string{}) 30 | os.Remove(rootOptions.ConfigFile) 31 | rootOptions.ConfigFile = "" 32 | ctrl.Finish() 33 | }) 34 | 35 | Context("with http requests", func() { 36 | BeforeEach(func() { 37 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 38 | queueCancelOption.RoundTripper = roundTripper 39 | }) 40 | 41 | It("should success", func() { 42 | data, err := GenerateSampleConfig() 43 | Expect(err).To(BeNil()) 44 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 45 | Expect(err).To(BeNil()) 46 | 47 | client.PrepareCancelQueue(roundTripper, "http://localhost:8080/jenkins", "admin", "111e3a2f0231198855dceaff96f20540a9") 48 | 49 | rootCmd.SetArgs([]string{"queue", "cancel", "1"}) 50 | 51 | buf := new(bytes.Buffer) 52 | rootCmd.SetOutput(buf) 53 | _, err = rootCmd.ExecuteC() 54 | Expect(err).To(BeNil()) 55 | 56 | Expect(buf.String()).To(Equal("")) 57 | }) 58 | 59 | It("should have error with invalid number", func() { 60 | data, err := GenerateSampleConfig() 61 | Expect(err).To(BeNil()) 62 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 63 | Expect(err).To(BeNil()) 64 | 65 | rootCmd.SetArgs([]string{"queue", "cancel", "a"}) 66 | 67 | buf := new(bytes.Buffer) 68 | rootCmd.SetOutput(buf) 69 | _, err = rootCmd.ExecuteC() 70 | Expect(err).To(HaveOccurred()) 71 | Expect(buf.String()).To(ContainSubstring("strconv.Atoi: parsing \"a\": invalid syntax\n")) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /client/release_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | jClient "github.com/jenkins-zh/jenkins-cli/client" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestInit(t *testing.T) { 10 | ghClient := jClient.GitHubReleaseClient{} 11 | 12 | assert.Nil(t, ghClient.Client) 13 | ghClient.Init() 14 | assert.NotNil(t, ghClient.Client) 15 | } 16 | 17 | func TestGetLatestReleaseAsset(t *testing.T) { 18 | client, teardown := jClient.PrepareForGetLatestReleaseAsset() //setup() 19 | defer teardown() 20 | 21 | ghClient := jClient.GitHubReleaseClient{ 22 | Client: client, 23 | } 24 | asset, err := ghClient.GetLatestReleaseAsset("o", "r") 25 | 26 | assert.Nil(t, err) 27 | assert.NotNil(t, asset) 28 | assert.Equal(t, "tagName", asset.TagName) 29 | assert.Equal(t, "body", asset.Body) 30 | } 31 | 32 | func TestGetLatestJCLIAsset(t *testing.T) { 33 | client, teardown := jClient.PrepareForGetLatestJCLIAsset() //setup() 34 | defer teardown() 35 | 36 | ghClient := jClient.GitHubReleaseClient{ 37 | Client: client, 38 | } 39 | asset, err := ghClient.GetLatestJCLIAsset() 40 | 41 | assert.Nil(t, err) 42 | assert.NotNil(t, asset) 43 | assert.Equal(t, "tagName", asset.TagName) 44 | assert.Equal(t, "body", asset.Body) 45 | } 46 | 47 | func TestGetJCLIAsset(t *testing.T) { 48 | client, teardown := jClient.PrepareForGetJCLIAsset("tagName") //setup() 49 | defer teardown() 50 | 51 | ghClient := jClient.GitHubReleaseClient{ 52 | Client: client, 53 | } 54 | asset, err := ghClient.GetJCLIAsset("tagName") 55 | 56 | assert.Nil(t, err) 57 | assert.NotNil(t, asset) 58 | assert.Equal(t, "tagName", asset.TagName) 59 | assert.Equal(t, "body", asset.Body) 60 | } 61 | 62 | func TestGetReleaseAssetByTagName(t *testing.T) { 63 | client, teardown := jClient.PrepareForGetReleaseAssetByTagName() //setup() 64 | defer teardown() 65 | 66 | ghClient := jClient.GitHubReleaseClient{ 67 | Client: client, 68 | } 69 | asset, err := ghClient.GetReleaseAssetByTagName("jenkins-zh", "jenkins-cli", "tagName") 70 | 71 | assert.Nil(t, err) 72 | assert.NotNil(t, asset) 73 | assert.Equal(t, "tagName", asset.TagName) 74 | assert.Equal(t, "body", asset.Body) 75 | } 76 | -------------------------------------------------------------------------------- /app/cmd/job_history_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | 10 | "github.com/golang/mock/gomock" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 15 | ) 16 | 17 | var _ = Describe("job history command", func() { 18 | var ( 19 | ctrl *gomock.Controller 20 | roundTripper *mhttp.MockRoundTripper 21 | err error 22 | jenkinsRoot string 23 | username string 24 | token string 25 | ) 26 | 27 | BeforeEach(func() { 28 | ctrl = gomock.NewController(GinkgoT()) 29 | roundTripper = mhttp.NewMockRoundTripper(ctrl) 30 | jobHistoryOption.RoundTripper = roundTripper 31 | rootCmd.SetArgs([]string{}) 32 | rootOptions.Jenkins = "" 33 | rootOptions.ConfigFile = "test.yaml" 34 | 35 | jenkinsRoot = "http://localhost:8080/jenkins" 36 | username = "admin" 37 | token = "111e3a2f0231198855dceaff96f20540a9" 38 | }) 39 | 40 | AfterEach(func() { 41 | rootCmd.SetArgs([]string{}) 42 | err = os.Remove(rootOptions.ConfigFile) 43 | rootOptions.ConfigFile = "" 44 | ctrl.Finish() 45 | }) 46 | 47 | Context("basic cases", func() { 48 | It("should not error", func() { 49 | Expect(err).NotTo(HaveOccurred()) 50 | }) 51 | 52 | It("should success", func() { 53 | data, err := GenerateSampleConfig() 54 | Expect(err).To(BeNil()) 55 | err = ioutil.WriteFile(rootOptions.ConfigFile, data, 0664) 56 | Expect(err).To(BeNil()) 57 | 58 | jobName := "fakeJob" 59 | 60 | client.PrepareForGetJob(roundTripper, jenkinsRoot, jobName, username, token) 61 | client.PrepareForGetBuild(roundTripper, jenkinsRoot, jobName, 1, username, token) 62 | client.PrepareForGetBuild(roundTripper, jenkinsRoot, jobName, 2, username, token) 63 | 64 | rootCmd.SetArgs([]string{"job", "history", jobName}) 65 | 66 | buf := new(bytes.Buffer) 67 | rootCmd.SetOutput(buf) 68 | _, err = rootCmd.ExecuteC() 69 | Expect(err).To(BeNil()) 70 | 71 | Expect(buf.String()).To(Equal(`DisplayName Building Result 72 | fake false 73 | fake false 74 | `)) 75 | }) 76 | }) 77 | }) 78 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: jenkins-cli 3 | description: Jenkins CLI allows you manage your Jenkins as an easy way 4 | homepage: https://jenkins-zh.cn 5 | private: false 6 | has_issues: true 7 | has_wiki: false 8 | has_downloads: false 9 | default_branch: master 10 | allow_squash_merge: true 11 | allow_merge_commit: true 12 | allow_rebase_merge: true 13 | labels: 14 | - name: newbie 15 | color: abe7f4 16 | description: 新手上路 17 | - name: bug 18 | color: d73a4a 19 | description: Something isn't working 20 | - name: feature 21 | color: ffc6a3 22 | - name: enhancement 23 | color: a2eeef 24 | description: New feature or request 25 | - name: help wanted 26 | color: 008672 27 | description: Extra attention is needed 28 | - name: bugfix 29 | color: 0412d6 30 | - name: regression 31 | color: c5def5 32 | - name: documentation 33 | color: 5ce05e 34 | - name: Hacktoberfest 35 | description: More details from https://hacktoberfest.digitalocean.com/ 36 | color: 5ce05e 37 | - name: test 38 | color: c2c2fc 39 | - name: chore 40 | color: c2c2fc 41 | - name: dependencies 42 | color: 0366d6 43 | description: Pull requests that update a dependency file 44 | - name: no-changelog 45 | color: c2c2fc 46 | - name: priority-high 47 | color: D93F0B 48 | - name: priority-medium 49 | color: FBCA04 50 | - name: priority-low 51 | color: 006B75 52 | - name: kind/bug 53 | color: c2c2fc 54 | - name: kind/feature 55 | color: c2c2fc 56 | - name: kind/doc 57 | color: c2c2fc 58 | - name: kind/dep 59 | color: c2c2fc 60 | - name: kind/chore 61 | color: c2c2fc 62 | branches: 63 | - name: master 64 | protection: 65 | required_pull_request_reviews: 66 | required_approving_review_count: 1 67 | dismiss_stale_reviews: true 68 | require_code_owner_reviews: true 69 | dismissal_restrictions: 70 | users: [] 71 | teams: [] 72 | required_status_checks: 73 | strict: true 74 | contexts: [] 75 | enforce_admins: false 76 | restrictions: 77 | users: [] 78 | teams: [] 79 | -------------------------------------------------------------------------------- /app/cmd/config_data.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 6 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // ConfigDataOptions is the config data option 12 | type ConfigDataOptions struct { 13 | Key string 14 | Value string 15 | } 16 | 17 | var configDataOptions ConfigDataOptions 18 | 19 | func init() { 20 | configCmd.AddCommand(configDataCmd) 21 | configDataCmd.Flags().StringVarP(&configDataOptions.Key, "key", "k", "", 22 | i18n.T("The key of config data")) 23 | configDataCmd.Flags().StringVarP(&configDataOptions.Value, "value", "v", "", 24 | i18n.T("The value of config data")) 25 | 26 | configDataCmd.MarkFlagRequired("key") 27 | configDataCmd.MarkFlagRequired("value") 28 | } 29 | 30 | var configDataCmd = &cobra.Command{ 31 | Use: "data", 32 | Short: i18n.T("Add a key/value to a config item"), 33 | Long: i18n.T("Add a key/value to a config item"), 34 | ValidArgsFunction: ValidJenkinsNames, 35 | RunE: func(_ *cobra.Command, args []string) (err error) { 36 | var jenkinsName string 37 | if len(args) <= 0 { 38 | target := "" 39 | if currentJenkins := getCurrentJenkins(); currentJenkins != nil { 40 | target = currentJenkins.Name 41 | } 42 | jenkinsName, err = configSelectOptions.Select(getJenkinsNames(), 43 | "Choose a Jenkins to add key/value:", target) 44 | } else { 45 | jenkinsName = args[0] 46 | } 47 | 48 | found := false 49 | for i, cfg := range config.JenkinsServers { 50 | if cfg.Name == jenkinsName { 51 | if config.JenkinsServers[i].Data == nil { 52 | config.JenkinsServers[i].Data = make(map[string]string, 1) 53 | } 54 | config.JenkinsServers[i].Data[configDataOptions.Key] = configDataOptions.Value 55 | err = saveConfig() 56 | found = true 57 | break 58 | } 59 | } 60 | 61 | if !found { 62 | err = fmt.Errorf("jenkins '%s' does not exist", jenkinsName) 63 | } 64 | return 65 | }, 66 | Annotations: map[string]string{ 67 | common.Since: "v0.0.31", 68 | }, 69 | Example: "jcli config data local -k jcli -v https://github.com/jenkins-zh/jenkins-cli", 70 | } 71 | -------------------------------------------------------------------------------- /client/core.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/util" 5 | "go.uber.org/zap" 6 | "net/http" 7 | ) 8 | 9 | var logger *zap.Logger 10 | 11 | // SetLogger set a global logger 12 | func SetLogger(zapLogger *zap.Logger) { 13 | logger = zapLogger 14 | } 15 | 16 | func init() { 17 | if logger == nil { 18 | var err error 19 | if logger, err = util.InitLogger("warn"); err != nil { 20 | panic(err) 21 | } 22 | } 23 | } 24 | 25 | // CoreClient hold the client of Jenkins core 26 | type CoreClient struct { 27 | JenkinsCore 28 | } 29 | 30 | // Restart will send the restart request 31 | func (q *CoreClient) Restart() (err error) { 32 | _, err = q.RequestWithoutData(http.MethodPost, "/safeRestart", nil, nil, 503) 33 | return 34 | } 35 | 36 | // RestartDirectly restart Jenkins directly 37 | func (q *CoreClient) RestartDirectly() (err error) { 38 | _, err = q.RequestWithoutData(http.MethodPost, "/restart", nil, nil, 503) 39 | return 40 | } 41 | 42 | // Shutdown puts Jenkins into the quiet mode, wait for existing builds to be completed, and then shut down Jenkins 43 | func (q *CoreClient) Shutdown(safe bool) (err error) { 44 | if safe { 45 | _, err = q.RequestWithoutData(http.MethodPost, "/safeExit", nil, nil, 200) 46 | } else { 47 | _, err = q.RequestWithoutData(http.MethodPost, "/exit", nil, nil, 200) 48 | } 49 | return 50 | } 51 | 52 | // PrepareShutdown Put Jenkins in a Quiet mode, in preparation for a restart. In that mode Jenkins don’t start any build 53 | func (q *CoreClient) PrepareShutdown(cancel bool) (err error) { 54 | if cancel { 55 | _, err = q.RequestWithoutData(http.MethodPost, "/cancelQuietDown", nil, nil, 200) 56 | } else { 57 | _, err = q.RequestWithoutData(http.MethodPost, "/quietDown", nil, nil, 200) 58 | } 59 | return 60 | } 61 | 62 | // JenkinsIdentity belongs to a Jenkins 63 | type JenkinsIdentity struct { 64 | Fingerprint string 65 | PublicKey string 66 | SystemMessage string 67 | } 68 | 69 | // GetIdentity returns the identity of a Jenkins 70 | func (q *CoreClient) GetIdentity() (identity JenkinsIdentity, err error) { 71 | err = q.RequestWithData(http.MethodGet, "/instance", nil, nil, 200, &identity) 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /app/i18n/i18n_test.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/gomega" 7 | "os" 8 | ) 9 | 10 | var _ = Describe("test LoadTranslations", func() { 11 | var ( 12 | root string 13 | getLangFunc func() string 14 | err error 15 | ) 16 | 17 | JustBeforeEach(func() { 18 | err = LoadTranslations(root, getLangFunc) 19 | }) 20 | 21 | AfterEach(func() { 22 | root = "" 23 | getLangFunc = nil 24 | }) 25 | 26 | It("default param", func() { 27 | Expect(err).NotTo(HaveOccurred()) 28 | }) 29 | 30 | Context("unknown language", func() { 31 | BeforeEach(func() { 32 | getLangFunc = func() string { 33 | return "fake" 34 | } 35 | }) 36 | 37 | It("should not have error", func() { 38 | Expect(err).NotTo(HaveOccurred()) 39 | }) 40 | }) 41 | 42 | Context("given invalid environment of language", func() { 43 | var ( 44 | osEnvErr error 45 | ) 46 | 47 | BeforeEach(func() { 48 | osEnvErr = os.Setenv("LC_ALL", "zh_CN") 49 | }) 50 | 51 | It("should not have error", func() { 52 | Expect(osEnvErr).NotTo(HaveOccurred()) 53 | Expect(err).NotTo(HaveOccurred()) 54 | }) 55 | }) 56 | 57 | Context("given valid environment of language", func() { 58 | var ( 59 | osEnvErr error 60 | ) 61 | 62 | BeforeEach(func() { 63 | root = "jcli" 64 | osEnvErr = os.Setenv("LC_ALL", "zh_CN.utf-8") 65 | }) 66 | 67 | It("should not have error", func() { 68 | Expect(osEnvErr).NotTo(HaveOccurred()) 69 | Expect(err).NotTo(HaveOccurred()) 70 | }) 71 | }) 72 | }) 73 | 74 | var _ = Describe("test i18n function T", func() { 75 | var ( 76 | text string 77 | args []int 78 | result string 79 | ) 80 | 81 | JustBeforeEach(func() { 82 | result = T(text, args...) 83 | }) 84 | 85 | It("simple case, without args", func() { 86 | Expect(result).To(Equal(text)) 87 | }) 88 | 89 | Context("with args", func() { 90 | BeforeEach(func() { 91 | text = "fake %d" 92 | args = []int{1} 93 | }) 94 | 95 | It("should success", func() { 96 | Expect(result).To(Equal(fmt.Sprintf(text, 1))) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /app/cmd/job_type.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/jenkins-zh/jenkins-cli/app/cmd/common" 5 | "github.com/jenkins-zh/jenkins-cli/app/i18n" 6 | cobra_ext "github.com/linuxsuren/cobra-extension/pkg" 7 | 8 | "github.com/jenkins-zh/jenkins-cli/client" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // JobTypeOption is the job type cmd option 13 | type JobTypeOption struct { 14 | cobra_ext.OutputOption 15 | common.Option 16 | } 17 | 18 | var jobTypeOption JobTypeOption 19 | 20 | func init() { 21 | jobCmd.AddCommand(jobTypeCmd) 22 | jobTypeOption.SetFlagWithHeaders(jobTypeCmd, "DisplayName,Class") 23 | } 24 | 25 | var jobTypeCmd = &cobra.Command{ 26 | Use: "type", 27 | Short: i18n.T("Print the types of job which in your Jenkins"), 28 | Long: i18n.T("Print the types of job which in your Jenkins"), 29 | RunE: func(cmd *cobra.Command, _ []string) (err error) { 30 | jclient := &client.JobClient{ 31 | JenkinsCore: client.JenkinsCore{ 32 | RoundTripper: jobTypeOption.RoundTripper, 33 | }, 34 | } 35 | getCurrentJenkinsAndClientOrDie(&(jclient.JenkinsCore)) 36 | 37 | var jobCategories []client.JobCategory 38 | jobCategories, err = jclient.GetJobTypeCategories() 39 | if err == nil { 40 | var jobCategoryItems []client.JobCategoryItem 41 | for _, jobCategory := range jobCategories { 42 | for _, item := range jobCategory.Items { 43 | jobCategoryItems = append(jobCategoryItems, item) 44 | } 45 | } 46 | jobTypeOption.Writer = cmd.OutOrStdout() 47 | err = jobTypeOption.OutputV2(jobCategoryItems) 48 | } 49 | return 50 | }, 51 | } 52 | 53 | // GetCategories returns the categories of current Jenkins 54 | func GetCategories(jclient *client.JobClient) ( 55 | typeMap map[string]string, types []string, err error) { 56 | typeMap = make(map[string]string) 57 | var categories []client.JobCategory 58 | if categories, err = jclient.GetJobTypeCategories(); err == nil { 59 | for _, category := range categories { 60 | for _, item := range category.Items { 61 | typeMap[item.DisplayName] = item.Class 62 | } 63 | } 64 | 65 | types = make([]string, len(typeMap)) 66 | i := 0 67 | for tp := range typeMap { 68 | types[i] = tp 69 | i++ 70 | } 71 | } 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /client/casc_test_common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jenkins-zh/jenkins-cli/mock/mhttp" 6 | "net/http" 7 | ) 8 | 9 | // PrepareForSASCReload only for test 10 | func PrepareForSASCReload(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) { 11 | request, _ := http.NewRequest(http.MethodPost, 12 | fmt.Sprintf("%s/configuration-as-code/reload", rootURL), nil) 13 | PrepareCommonPost(request, "", roundTripper, user, password, rootURL) 14 | } 15 | 16 | // PrepareForSASCApply only for test 17 | func PrepareForSASCApply(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) { 18 | request, _ := http.NewRequest(http.MethodPost, 19 | fmt.Sprintf("%s/configuration-as-code/apply", rootURL), nil) 20 | PrepareCommonPost(request, "", roundTripper, user, password, rootURL) 21 | } 22 | 23 | // PrepareForSASCExport only for test 24 | func PrepareForSASCExport(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) ( 25 | response *http.Response) { 26 | request, _ := http.NewRequest(http.MethodPost, 27 | fmt.Sprintf("%s/configuration-as-code/export", rootURL), nil) 28 | response = PrepareCommonPost(request, "sample", roundTripper, user, password, rootURL) 29 | return 30 | } 31 | 32 | // PrepareForSASCExportWithCode only for test 33 | func PrepareForSASCExportWithCode(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string, code int) { 34 | response := PrepareForSASCExport(roundTripper, rootURL, user, password) 35 | response.StatusCode = code 36 | } 37 | 38 | // PrepareForSASCSchema only for test 39 | func PrepareForSASCSchema(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string) ( 40 | response *http.Response) { 41 | request, _ := http.NewRequest(http.MethodPost, 42 | fmt.Sprintf("%s/configuration-as-code/schema", rootURL), nil) 43 | response = PrepareCommonPost(request, "sample", roundTripper, user, password, rootURL) 44 | return 45 | } 46 | 47 | // PrepareForSASCSchemaWithCode only for test 48 | func PrepareForSASCSchemaWithCode(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string, code int) { 49 | response := PrepareForSASCSchema(roundTripper, rootURL, user, password) 50 | response.StatusCode = code 51 | } 52 | -------------------------------------------------------------------------------- /app/cmd/condition/plugin_dep.go: -------------------------------------------------------------------------------- 1 | package condition 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/go-version" 6 | appCfg "github.com/jenkins-zh/jenkins-cli/app/config" 7 | "github.com/jenkins-zh/jenkins-cli/client" 8 | "net/http" 9 | ) 10 | 11 | // PluginDepCheck is the checker of plugin deps 12 | type PluginDepCheck struct { 13 | client *client.PluginManager 14 | pluginName, targetVersion string 15 | } 16 | 17 | // NewChecker returns a plugin dep checker 18 | func NewChecker(jenkins *appCfg.JenkinsServer, roundTripper http.RoundTripper, pluginName, targetVersion string) ( 19 | checker *PluginDepCheck) { 20 | checker = &PluginDepCheck{ 21 | pluginName: pluginName, 22 | targetVersion: targetVersion, 23 | } 24 | 25 | jClient := &client.PluginManager{ 26 | JenkinsCore: client.JenkinsCore{ 27 | RoundTripper: roundTripper, 28 | }, 29 | } 30 | jClient.URL = jenkins.URL 31 | jClient.UserName = jenkins.UserName 32 | jClient.Token = jenkins.Token 33 | jClient.Proxy = jenkins.Proxy 34 | jClient.ProxyAuth = jenkins.ProxyAuth 35 | jClient.InsecureSkipVerify = jenkins.InsecureSkipVerify 36 | checker.client = jClient 37 | return 38 | } 39 | 40 | // FindPlugin find a plugin by name 41 | func (p *PluginDepCheck) FindPlugin(name string) (plugin *client.InstalledPlugin, err error) { 42 | if plugin, err = p.client.FindInstalledPlugin(name); err == nil && plugin == nil { 43 | err = fmt.Errorf(fmt.Sprintf("lack of plugin %s", name)) 44 | } 45 | return 46 | } 47 | 48 | // Check check if the target plugin with a specific version does exists 49 | func (p *PluginDepCheck) Check() (err error) { 50 | var plugin *client.InstalledPlugin 51 | if plugin, err = p.FindPlugin(p.pluginName); err == nil { 52 | var ( 53 | current *version.Version 54 | target *version.Version 55 | versionMatch bool 56 | ) 57 | 58 | if current, err = version.NewVersion(plugin.Version); err == nil { 59 | if target, err = version.NewVersion(p.targetVersion); err == nil { 60 | versionMatch = current.GreaterThanOrEqual(target) 61 | } 62 | } 63 | 64 | if err == nil && !versionMatch { 65 | err = fmt.Errorf("%s version is %s, should be %s", p.pluginName, plugin.Version, p.targetVersion) 66 | } 67 | } 68 | return 69 | } 70 | --------------------------------------------------------------------------------