diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 34fc36c3a6630e3222677c0780997e47bdb1ee54..0a08c8ece31ff9c7ac4416af47a04a6aac4ab05a 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -8,7 +8,7 @@ on: jobs: build: name: Build - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - name: Set up Go 1.13 diff --git a/.gitignore b/.gitignore index 28b3b97f241be5272a3d724d58d0ec2c2391c865..e20856a7ad69e33d5ffeb0deebe8225b69e56d27 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ bin/ release/ +jcli *.xml diff --git a/Makefile b/Makefile index f094e37d3c5aa3cd99a9c89c9d6c6979bfa26879..17d2aaf874662d4b6540b96fa67701d804a61403 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ init: gen-mock darwin: gen-data GO111MODULE=on CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o bin/darwin/$(NAME) $(MAIN_SRC_FILE) chmod +x bin/darwin/$(NAME) + rm -rf jcli && ln -s bin/darwin/$(NAME) jcli linux: gen-data CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o bin/linux/$(NAME) $(MAIN_SRC_FILE) diff --git a/app/cmd/cwp.go b/app/cmd/cwp.go new file mode 100644 index 0000000000000000000000000000000000000000..9c53ad1c56c435b89db016b23a712188c1969cde --- /dev/null +++ b/app/cmd/cwp.go @@ -0,0 +1,192 @@ +package cmd + +import ( + "encoding/xml" + "fmt" + "github.com/jenkins-zh/jenkins-cli/app/i18n" + "github.com/jenkins-zh/jenkins-cli/util" + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "io/ioutil" + "os" + "path" +) + +func init() { + rootCmd.AddCommand(cwpCmd) + + cwpCmd.Flags().BoolVarP(&cwpOptions.BatchMode, "batch-mode", "", false, + i18n.T("Enables the batch mode for the build")) + cwpCmd.Flags().StringVarP(&cwpOptions.BomPath, "bom-path", "", "", + i18n.T("Path to the BOM file. If defined, it will override settings in Config YAML")) + cwpCmd.Flags().StringVarP(&cwpOptions.Environment, "environment", "", "", + i18n.T("Environment to be used")) + cwpCmd.Flags().BoolVarP(&cwpOptions.InstallArtifacts, "install-artifacts", "", false, + i18n.T("If set, the final artifacts will be automatically installed to the local repository (current version - only WAR)")) + cwpCmd.Flags().StringVarP(&cwpOptions.ConfigPath, "config-path", "", "", + i18n.T("Path to the configuration YAML. See the tool's README for format")) + cwpCmd.Flags().BoolVarP(&cwpOptions.Demo, "demo", "", false, + i18n.T("Enables demo mode with predefined config file")) + cwpCmd.Flags().StringVarP(&cwpOptions.MvnSettingsFile, "mvn-settings-file", "", "", + i18n.T("Path to a custom Maven settings file to be used within the build")) + cwpCmd.Flags().StringVarP(&cwpOptions.TmpDir, "tmp-dir", "", "", + i18n.T("Temporary directory for generated files and the output WAR.")) + cwpCmd.Flags().StringVarP(&cwpOptions.Version, "version", "", "1.0-SNAPSHOT", + i18n.T("Version of WAR to be set.")) + + cwpCmd.Flags().BoolVarP(&cwpOptions.ShowProgress, "show-progress", "", true, + i18n.T("Show the progress of downloading files")) + cwpCmd.Flags().StringVarP(&cwpOptions.MetadataURL, "metadata-url", "", + "https://repo.jenkins-ci.org/list/releases/io/jenkins/tools/custom-war-packager/custom-war-packager-cli/maven-metadata.xml", + i18n.T("The metadata URL")) + + localCache := path.Join(os.TempDir(), "/", ".jenkins-cli") + if userHome, err := homedir.Dir(); err == nil { + localCache = path.Join(userHome, "/", ".jenkins-cli") + } + cwpCmd.Flags().StringVarP(&cwpOptions.LocalCache, "local-cache", "", localCache, + i18n.T("The local cache directory")) +} + +// CWPOptions is the option of custom-war-packager +// see also https://github.com/jenkinsci/custom-war-packager +type CWPOptions struct { + CommonOption + + ConfigPath string + Version string + TmpDir string + Environment string + BomPath string + MvnSettingsFile string + + BatchMode bool + Demo bool + InstallArtifacts bool + + ShowProgress bool + MetadataURL string + LocalCache string +} + +var cwpOptions CWPOptions + +var cwpCmd = &cobra.Command{ + Use: "cwp", + Short: i18n.T("Custom Jenkins WAR packager for Jenkins"), + Long: i18n.T(`Custom Jenkins WAR packager for Jenkins +This's a wrapper of https://github.com/jenkinsci/custom-war-packager`), + RunE: cwpOptions.Run, + Annotations: map[string]string{ + since: "v0.0.27", + }, +} + +// Run is the main logic of cwp cmd +func (o *CWPOptions) Run(cmd *cobra.Command, args []string) (err error) { + localCWP := o.getLocalCWP() + _, err = os.Stat(localCWP) + if os.IsNotExist(err) { + if err = o.Download(); err != nil { + return + } + } else if err != nil { + return + } + + var binary string + binary, err = util.LookPath("java", o.LookPathContext) + if err == nil { + env := os.Environ() + + cwpArgs := []string{"java"} + cwpArgs = append(cwpArgs, "-jar", localCWP) + + if o.Demo { + cwpArgs = append(cwpArgs, "-demo") + } + + if o.BatchMode { + cwpArgs = append(cwpArgs, "--batch-mode") + } + + if o.InstallArtifacts { + cwpArgs = append(cwpArgs, "--installArtifacts") + } + + if o.ConfigPath != "" { + cwpArgs = append(cwpArgs, "-configPath", o.ConfigPath) + } + + if o.TmpDir != "" { + cwpArgs = append(cwpArgs, "-tmpDir", o.TmpDir) + } + err = util.Exec(binary, cwpArgs, env, o.SystemCallExec) + } + return +} + +// Download get the latest cwp from server into local +func (o *CWPOptions) Download() (err error) { + var latest string + if latest, err = o.GetLatest(); err == nil { + cwpURL := o.GetCWPURL(latest) + + err = o.downloadFile(cwpURL, o.getLocalCWP()) + } + return +} + +// GetCWPURL returns the download URL of a specific version cwp +func (o *CWPOptions) GetCWPURL(version string) string { + return fmt.Sprintf("https://repo.jenkins-ci.org/list/releases/io/jenkins/tools/custom-war-packager/custom-war-packager-cli/%s/custom-war-packager-cli-%s-jar-with-dependencies.jar", + version, version) +} + +func (o *CWPOptions) getLocalCWP() string { + return path.Join(o.LocalCache, "cwp-cli.jar") +} + +// GetLatest returns the latest of cwp +func (o *CWPOptions) GetLatest() (version string, err error) { + metadataURL := o.MetadataURL + output := "metadata.xml" + + if err = o.downloadFile(metadataURL, output); err == nil { + var data []byte + + mavenMeta := MavenMetadata{} + if data, err = ioutil.ReadFile(output); err == nil { + err = xml.Unmarshal(data, &mavenMeta) + } + + if err == nil { + version = mavenMeta.Versioning.Latest + } + } + return +} + +func (o *CWPOptions) downloadFile(url, output string) (err error) { + downloader := util.HTTPDownloader{ + RoundTripper: o.RoundTripper, + TargetFilePath: output, + URL: url, + ShowProgress: o.ShowProgress, + } + err = downloader.DownloadFile() + return +} + +// MavenMetadata is the maven metadata xml root +type MavenMetadata struct { + XMLName xml.Name `xml:"metadata"` + Versioning MavenVersioning `xml:"versioning"` +} + +// MavenVersioning is the versioning of maven +type MavenVersioning struct { + XMLName xml.Name `xml:"versioning"` + Latest string `xml:"latest"` + Release string `xml:"release"` +} diff --git a/app/cmd/cwp_test.go b/app/cmd/cwp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cca2795fcc2d2a4b2a9e30240a82cece66f478c2 --- /dev/null +++ b/app/cmd/cwp_test.go @@ -0,0 +1,133 @@ +package cmd + +import ( + "bytes" + "github.com/golang/mock/gomock" + "github.com/jenkins-zh/jenkins-cli/mock/mhttp" + "github.com/jenkins-zh/jenkins-cli/util" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "os" + "path" + "testing" +) + +var _ = Describe("cwp command test", func() { + var ( + ctrl *gomock.Controller + localCache string + ) + + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + localCache = os.TempDir() + roundTripper := mhttp.NewMockRoundTripper(ctrl) + cwpOptions = CWPOptions{ + CommonOption: CommonOption{RoundTripper: roundTripper}, + MetadataURL: "http://localhost/maven-metadata.xml", + LocalCache: localCache, + } + prepareMavenMetadataRequest(roundTripper) + + fakeContent := "hello" + prepareDownloadFileRequest(cwpOptions.GetCWPURL("2.0-alpha-2"), fakeContent, roundTripper) + + cwpOptions.SystemCallExec = util.FakeSystemCallExecSuccess + cwpOptions.LookPathContext = util.FakeLookPath + }) + + AfterEach(func() { + os.RemoveAll(localCache) + ctrl.Finish() + }) + + Context("basic test", func() { + It("should success", func() { + rootCmd.SetArgs([]string{"cwp"}) + _, err := rootCmd.ExecuteC() + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +func TestDownload(t *testing.T) { + ctrl := gomock.NewController(t) + + tmpDir := os.TempDir() + defer os.RemoveAll(tmpDir) + + roundTripper := mhttp.NewMockRoundTripper(ctrl) + cwpOpts := CWPOptions{ + CommonOption: CommonOption{RoundTripper: roundTripper}, + MetadataURL: "http://localhost/maven-metadata.xml", + LocalCache: tmpDir, + } + prepareMavenMetadataRequest(roundTripper) + + fakeContent := "hello" + prepareDownloadFileRequest(cwpOpts.GetCWPURL("2.0-alpha-2"), fakeContent, roundTripper) + + err := cwpOpts.Download() + assert.Nil(t, err) + + var data []byte + data, err = ioutil.ReadFile(path.Join(tmpDir, "cwp-cli.jar")) + assert.Nil(t, err) + assert.Equal(t, fakeContent, string(data)) +} + +func TestGetLatest(t *testing.T) { + ctrl := gomock.NewController(t) + + roundTripper := mhttp.NewMockRoundTripper(ctrl) + cwpOpts := CWPOptions{ + CommonOption: CommonOption{RoundTripper: roundTripper}, + MetadataURL: "http://localhost/maven-metadata.xml", + } + prepareMavenMetadataRequest(roundTripper) + + ver, err := cwpOpts.GetLatest() + assert.Nil(t, err) + assert.Equal(t, "2.0-alpha-2", ver) +} + +func prepareDownloadFileRequest(url, content string, roundTripper *mhttp.MockRoundTripper) { + request, _ := http.NewRequest("GET", url, nil) + response := &http.Response{ + StatusCode: 200, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(content)), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) +} + +func prepareMavenMetadataRequest(roundTripper *mhttp.MockRoundTripper) { + request, _ := http.NewRequest("GET", "http://localhost/maven-metadata.xml", nil) + response := &http.Response{ + StatusCode: 200, + Request: request, + Body: ioutil.NopCloser(bytes.NewBufferString(getMavenMetadataSample())), + } + roundTripper.EXPECT(). + RoundTrip(request).Return(response, nil) +} + +func getMavenMetadataSample() string { + return ` + + io.jenkins.tools.custom-war-packager + custom-war-packager-cli + + 2.0-alpha-2 + 2.0-alpha-2 + + 2.0-alpha-2 + + 20190815083928 + +` +} diff --git a/app/cmd/doc.go b/app/cmd/doc.go index 674e4a4f2be4b5a942ee625787340f18879038c3..27722b39fee7c2bcfeaacf9c57f5281a977ebe8c 100644 --- a/app/cmd/doc.go +++ b/app/cmd/doc.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "os" "path" "path/filepath" "strings" @@ -28,10 +29,11 @@ version: %s ) var docCmd = &cobra.Command{ - Use: "doc ", - Short: i18n.T("Generate document for all jcl commands"), - Long: i18n.T("Generate document for all jcl commands"), - Args: cobra.MinimumNArgs(1), + Use: "doc", + Example: "doc tmp", + Short: i18n.T("Generate document for all jcl commands"), + Long: i18n.T("Generate document for all jcl commands"), + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { now := time.Now().Format(time.RFC3339) prepender := func(filename string) string { @@ -48,9 +50,10 @@ var docCmd = &cobra.Command{ } outputDir := args[0] - - rootCmd.DisableAutoGenTag = true - err = doc.GenMarkdownTreeCustom(rootCmd, outputDir, prepender, linkHandler) + if err = os.MkdirAll(outputDir, os.FileMode(0755)); err == nil { + rootCmd.DisableAutoGenTag = true + err = doc.GenMarkdownTreeCustom(rootCmd, outputDir, prepender, linkHandler) + } return }, } diff --git a/app/cmd/doc_test.go b/app/cmd/doc_test.go index 6b115644d875f017e6e5290af64da3376c5bfa49..154dd9f4bb44a89b9cfec3493a0d81ec79f571f3 100644 --- a/app/cmd/doc_test.go +++ b/app/cmd/doc_test.go @@ -2,12 +2,13 @@ package cmd import ( "bytes" - "github.com/golang/mock/gomock" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "io/ioutil" "os" "path/filepath" + + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = Describe("doc command test", func() { @@ -37,17 +38,16 @@ var _ = Describe("doc command test", func() { buf := new(bytes.Buffer) rootCmd.SetOutput(buf) - tmpdir, err := ioutil.TempDir("", "test-gen-cmd-tree") - Expect(err).To(BeNil()) + tmpdir := os.TempDir() defer os.RemoveAll(tmpdir) rootCmd.SetArgs([]string{"doc", tmpdir}) - _, err = rootCmd.ExecuteC() - Expect(err).To(BeNil()) + _, err := rootCmd.ExecuteC() + Expect(err).NotTo(HaveOccurred()) Expect(buf.String()).To(Equal("")) _, err = os.Stat(filepath.Join(tmpdir, "jcli_doc.md")) - Expect(err).To(BeNil()) + Expect(err).NotTo(HaveOccurred()) }) }) }) diff --git a/app/cmd/job_build_test.go b/app/cmd/job_build_test.go index 249efa957c54e781a2d1faaee0a3012213accd78..0d1e0968ce91afb6e306180c5b1292dfc22bd9e2 100644 --- a/app/cmd/job_build_test.go +++ b/app/cmd/job_build_test.go @@ -3,13 +3,14 @@ package cmd import ( "bytes" "fmt" - "github.com/Netflix/go-expect" "io/ioutil" "net/http" "os" "testing" "time" + expect "github.com/Netflix/go-expect" + "github.com/AlecAivazis/survey/v2/terminal" "github.com/golang/mock/gomock" "github.com/jenkins-zh/jenkins-cli/client" diff --git a/app/cmd/job_disable_test.go b/app/cmd/job_disable_test.go index e834bb7dc371276123438c2ebc9785bd322154a3..acb8ae7c60fd70dd0b2eb9b6f52bcaa875063734 100644 --- a/app/cmd/job_disable_test.go +++ b/app/cmd/job_disable_test.go @@ -4,11 +4,11 @@ import ( "io/ioutil" "os" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "github.com/golang/mock/gomock" "github.com/jenkins-zh/jenkins-cli/client" "github.com/jenkins-zh/jenkins-cli/mock/mhttp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = Describe("job disable command", func() { diff --git a/app/cmd/job_enable.go b/app/cmd/job_enable.go index 4b2568c511863ab4941c30082d8453c315fe9206..1369e4ab7c3fa81892de1fec4e3620e5b50ac385 100644 --- a/app/cmd/job_enable.go +++ b/app/cmd/job_enable.go @@ -20,10 +20,10 @@ func init() { } var jobEnabelCmd = &cobra.Command{ - Use: "enable", - Short: i18n.T("Enable a job in your Jenkins"), - Long: i18n.T("Enable a job in your Jenkins"), - Args: cobra.MinimumNArgs(1), + Use: "enable", + Short: i18n.T("Enable a job in your Jenkins"), + Long: i18n.T("Enable a job in your Jenkins"), + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { jobName := args[0] jclient := &client.JobClient{ diff --git a/app/cmd/job_enable_test.go b/app/cmd/job_enable_test.go index 6335e5643599cc444aaa24494f8f3e97495d3f29..31eb30b8c741b62e480b538ecfffeb91a7528a07 100644 --- a/app/cmd/job_enable_test.go +++ b/app/cmd/job_enable_test.go @@ -4,11 +4,11 @@ import ( "io/ioutil" "os" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "github.com/golang/mock/gomock" "github.com/jenkins-zh/jenkins-cli/client" "github.com/jenkins-zh/jenkins-cli/mock/mhttp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) var _ = Describe("job enable command", func() { diff --git a/app/cmd/root.go b/app/cmd/root.go index 6a4ad6c1fd80c2532052386383f9d1bf0df7c7ab..65589b4ca6dde6c08923c7ae22c2091941bfe9a5 100644 --- a/app/cmd/root.go +++ b/app/cmd/root.go @@ -92,6 +92,7 @@ func needReadConfig(cmd *cobra.Command) bool { ignoreConfigLoad := []string{ "config.generate", "center.start", + "cwp", "version", } configPath := getCmdPath(cmd) diff --git a/util/cmd.go b/util/cmd.go index 1decf4b5bccfd780040d0e369a2e6153aa58f4ea..6ff38dca6db8f54efbf9d165dde87538b250955f 100644 --- a/util/cmd.go +++ b/util/cmd.go @@ -1,7 +1,6 @@ package util import ( - "os" "os/exec" "runtime" "syscall" @@ -57,7 +56,8 @@ type LookPathContext = func(file string) (string, error) func FakeExecCommandSuccess(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=TestShellProcessSuccess", "--", command} cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) + cmd := exec.Command("go", cs...) + //cmd := exec.Command(os.Args[0], cs...) cmd.Env = []string{"GO_TEST_PROCESS=1"} return cmd } diff --git a/util/http.go b/util/http.go index 7e385de550b41c7d5841a63b76dd3e781c453982..c128e6fd326d0c99d19f5ccca9343ab96275de1b 100644 --- a/util/http.go +++ b/util/http.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "os" + "path" "strconv" "github.com/gosuri/uiprogress" @@ -109,6 +110,11 @@ func (h *HTTPDownloader) DownloadFile() error { } } } + + if err := os.MkdirAll(path.Dir(filepath), os.FileMode(0755)); err != nil { + return err + } + // Create the file out, err := os.Create(filepath) if err != nil {