From e844f05b4755c3eef3ae733cdce592a7d30405ae Mon Sep 17 00:00:00 2001 From: Sylvain Nieuwlandt Date: Mon, 22 Apr 2024 11:42:29 +0000 Subject: [PATCH] feat: Create a job artifact command --- Makefile | 2 +- commands/ci/artifact/artifact.go | 90 +---------------------------- commands/ci/ci.go | 9 ++- commands/ci/ci_test.go | 72 +++++++++++++++++++---- commands/job/artifact/artifact.go | 38 +++++++++++++ commands/job/artifact/logic.go | 95 +++++++++++++++++++++++++++++++ commands/job/job.go | 20 +++++++ commands/job/job_test.go | 72 +++++++++++++++++++++++ commands/root.go | 2 + docs/source/ci/index.md | 4 ++ docs/source/job/artifact.md | 45 +++++++++++++++ docs/source/job/help.md | 25 ++++++++ docs/source/job/index.md | 30 ++++++++++ 13 files changed, 404 insertions(+), 100 deletions(-) create mode 100644 commands/job/artifact/artifact.go create mode 100644 commands/job/artifact/logic.go create mode 100644 commands/job/job.go create mode 100644 commands/job/job_test.go create mode 100644 docs/source/job/artifact.md create mode 100644 docs/source/job/help.md create mode 100644 docs/source/job/index.md diff --git a/Makefile b/Makefile index 61d36802..f4bc9305 100644 --- a/Makefile +++ b/Makefile @@ -147,7 +147,7 @@ endif bin/golangci-lint-${GOLANGCI_VERSION}: @mkdir -p bin - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION} + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b ./bin v${GOLANGCI_VERSION} @mv bin/golangci-lint $@ .PHONY: coverage diff --git a/commands/ci/artifact/artifact.go b/commands/ci/artifact/artifact.go index 527eaf8b..ffe6ed5f 100644 --- a/commands/ci/artifact/artifact.go +++ b/commands/ci/artifact/artifact.go @@ -1,34 +1,12 @@ package ci import ( - "archive/zip" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - "github.com/xanzy/go-gitlab" - "gitlab.com/gitlab-org/cli/api" "gitlab.com/gitlab-org/cli/commands/cmdutils" - "gitlab.com/gitlab-org/cli/internal/config" - "gitlab.com/gitlab-org/cli/pkg/utils" + jobArtifact "gitlab.com/gitlab-org/cli/commands/job/artifact" ) -func ensurePathIsCreated(filename string) error { - dir, _ := filepath.Split(filename) - - if _, err := os.Stat(filename); os.IsNotExist(err) { - err = os.MkdirAll(dir, 0o700) // Create your file - if err != nil { - return fmt.Errorf("could not create new path: %v", err) - } - } - return nil -} - func NewCmdRun(f *cmdutils.Factory) *cobra.Command { jobArtifactCmd := &cobra.Command{ Use: "artifact [flags]", @@ -54,71 +32,9 @@ func NewCmdRun(f *cmdutils.Factory) *cobra.Command { return err } - artifact, err := api.DownloadArtifactJob(apiClient, repo.FullName(), args[0], &gitlab.DownloadArtifactsFileOptions{Job: &args[1]}) - if err != nil { - return err - } - - zipReader, err := zip.NewReader(artifact, artifact.Size()) - if err != nil { - return err - } - - if !config.CheckPathExists(path) { - if err := os.Mkdir(path, 0o755); err != nil { - return err - } - } - - if !strings.HasSuffix(path, "/") { - path = path + "/" - } - - for _, v := range zipReader.File { - sanitizedAssetName := utils.SanitizePathName(v.Name) - - destDir, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("resolving absolute download directory path: %v", err) - } - destPath := filepath.Join(destDir, sanitizedAssetName) - if !strings.HasPrefix(destPath, destDir) { - return fmt.Errorf("invalid file path name") - } - - if v.FileInfo().IsDir() { - if err := os.Mkdir(destPath, v.Mode()); err != nil { - return err - } - } else { - srcFile, err := zipReader.Open(v.Name) - if err != nil { - return err - } - defer srcFile.Close() - - err = ensurePathIsCreated(destPath) - if err != nil { - return err - } - - symlinkCheck, _ := os.Lstat(destPath) - - if symlinkCheck != nil && symlinkCheck.Mode()&os.ModeSymlink != 0 { - return fmt.Errorf("file in artifact would overwrite a symbolic link- cannot extract") - } - - dstFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.Mode()) - if err != nil { - return err - } - if _, err := io.Copy(dstFile, srcFile); err != nil { - return err - } - } - } - return nil + return jobArtifact.DownloadArtifacts(apiClient, repo, path, args[0], args[1]) }, + Deprecated: "please use 'glab job artifact' instead", } jobArtifactCmd.Flags().StringP("path", "p", "./", "Path to download the artifact files") diff --git a/commands/ci/ci.go b/commands/ci/ci.go index 603484c5..3a2f6c40 100644 --- a/commands/ci/ci.go +++ b/commands/ci/ci.go @@ -1,6 +1,9 @@ package ci import ( + "fmt" + "os" + jobArtifactCmd "gitlab.com/gitlab-org/cli/commands/ci/artifact" ciConfigCmd "gitlab.com/gitlab-org/cli/commands/ci/config" pipeDeleteCmd "gitlab.com/gitlab-org/cli/commands/ci/delete" @@ -26,10 +29,13 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { Short: `Work with GitLab CI/CD pipelines and jobs`, Long: ``, Aliases: []string{"pipe", "pipeline"}, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(os.Stderr, "Aliases 'pipe' and 'pipeline' are deprecated. Please use 'ci' instead.\n\n") + _ = cmd.Help() + }, } cmdutils.EnableRepoOverride(ciCmd, f) - ciCmd.AddCommand(legacyCICmd.NewCmdCI(f)) ciCmd.AddCommand(ciTraceCmd.NewCmdTrace(f)) ciCmd.AddCommand(ciViewCmd.NewCmdView(f)) @@ -44,5 +50,6 @@ func NewCmdCI(f *cmdutils.Factory) *cobra.Command { ciCmd.AddCommand(jobArtifactCmd.NewCmdRun(f)) ciCmd.AddCommand(pipeGetCmd.NewCmdGet(f)) ciCmd.AddCommand(ciConfigCmd.NewCmdConfig(f)) + return ciCmd } diff --git a/commands/ci/ci_test.go b/commands/ci/ci_test.go index eaa61e71..01d2392b 100644 --- a/commands/ci/ci_test.go +++ b/commands/ci/ci_test.go @@ -1,22 +1,72 @@ package ci import ( + "bytes" + "io" "os" "testing" "github.com/stretchr/testify/assert" "gitlab.com/gitlab-org/cli/commands/cmdutils" - "gitlab.com/gitlab-org/cli/test" ) -func TestPipelineCmd(t *testing.T) { - old := os.Stdout // keep backup of the real stdout - r, w, _ := os.Pipe() - os.Stdout = w - - assert.Nil(t, NewCmdCI(&cmdutils.Factory{}).Execute()) - - out := test.ReturnBuffer(old, r, w) - - assert.Contains(t, out, "Use \"ci [command] --help\" for more information about a command.\n") +var tests = []struct { + name string + args string + expectedOut string + expectedErr string +}{ + { + name: "when no args should display the help message", + args: "", + expectedOut: "Use \"ci [command] --help\" for more information about a command.\n", + expectedErr: "Aliases 'pipe' and 'pipeline' are deprecated. Please use 'ci' instead.", + }, +} + +func TestPipelineCmd(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + wantedErr := "" + if len(test.expectedErr) > 0 { + wantedErr = test.expectedErr + } + + // Catching Stdout & Stderr + oldOut := os.Stdout + rOut, wOut, _ := os.Pipe() + os.Stdout = wOut + outC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, rOut) + outC <- buf.String() + }() + + oldErr := os.Stderr + rErr, wErr, _ := os.Pipe() + os.Stderr = wErr + errC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, rErr) + errC <- buf.String() + }() + + err := NewCmdCI(&cmdutils.Factory{}).Execute() + + // Rollbacking Stdout & Stderr + wOut.Close() + os.Stdout = oldOut + stdout := <-outC + wErr.Close() + os.Stderr = oldErr + stderr := <-errC + + if assert.NoErrorf(t, err, "error running `ci %s` : %v", test.args, err) { + assert.Contains(t, stderr, wantedErr) + assert.Contains(t, stdout, test.expectedOut) + } + }) + } } diff --git a/commands/job/artifact/artifact.go b/commands/job/artifact/artifact.go new file mode 100644 index 00000000..caef3edf --- /dev/null +++ b/commands/job/artifact/artifact.go @@ -0,0 +1,38 @@ +package artifact + +import ( + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func NewCmdArtifact(f *cmdutils.Factory) *cobra.Command { + jobArtifactCmd := &cobra.Command{ + Use: "artifact [flags]", + Short: `Download all artifacts from the last pipeline`, + Aliases: []string{"push"}, + Example: heredoc.Doc(` + glab job artifact main build + glab job artifact main deploy --path="artifacts/" + `), + Long: ``, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + repo, err := f.BaseRepo() + if err != nil { + return err + } + apiClient, err := f.HttpClient() + if err != nil { + return err + } + path, err := cmd.Flags().GetString("path") + if err != nil { + return err + } + return DownloadArtifacts(apiClient, repo, path, args[0], args[1]) + }, + } + jobArtifactCmd.Flags().StringP("path", "p", "./", "Path to download the artifact files") + return jobArtifactCmd +} diff --git a/commands/job/artifact/logic.go b/commands/job/artifact/logic.go new file mode 100644 index 00000000..d4cb0c3f --- /dev/null +++ b/commands/job/artifact/logic.go @@ -0,0 +1,95 @@ +package artifact + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/xanzy/go-gitlab" + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/internal/config" + "gitlab.com/gitlab-org/cli/internal/glrepo" + "gitlab.com/gitlab-org/cli/pkg/utils" +) + +func ensurePathIsCreated(filename string) error { + dir, _ := filepath.Split(filename) + + if _, err := os.Stat(filename); os.IsNotExist(err) { + err = os.MkdirAll(dir, 0o700) // Create your file + if err != nil { + return fmt.Errorf("could not create new path: %v", err) + } + } + return nil +} + +func DownloadArtifacts(apiClient *gitlab.Client, repo glrepo.Interface, path string, refName string, jobName string) error { + artifact, err := api.DownloadArtifactJob(apiClient, repo.FullName(), refName, &gitlab.DownloadArtifactsFileOptions{Job: &jobName}) + if err != nil { + return err + } + + zipReader, err := zip.NewReader(artifact, artifact.Size()) + if err != nil { + return err + } + + if !config.CheckPathExists(path) { + if err := os.Mkdir(path, 0o755); err != nil { + return err + } + } + + if !strings.HasSuffix(path, "/") { + path = path + "/" + } + + for _, v := range zipReader.File { + sanitizedAssetName := utils.SanitizePathName(v.Name) + + destDir, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("resolving absolute download directory path: %v", err) + } + destPath := filepath.Join(destDir, sanitizedAssetName) + if !strings.HasPrefix(destPath, destDir) { + return fmt.Errorf("invalid file path name") + } + + if v.FileInfo().IsDir() { + if err := os.Mkdir(destPath, v.Mode()); err != nil { + return err + } + } else { + srcFile, err := zipReader.Open(v.Name) + if err != nil { + return err + } + defer srcFile.Close() + + err = ensurePathIsCreated(destPath) + if err != nil { + return err + } + + symlinkCheck, _ := os.Lstat(destPath) + + if symlinkCheck != nil && symlinkCheck.Mode()&os.ModeSymlink != 0 { + return fmt.Errorf("file in artifact would overwrite a symbolic link- cannot extract") + } + + dstFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, v.Mode()) + if err != nil { + return err + } + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + } + } + return nil +} diff --git a/commands/job/job.go b/commands/job/job.go new file mode 100644 index 00000000..6b45e894 --- /dev/null +++ b/commands/job/job.go @@ -0,0 +1,20 @@ +package job + +import ( + "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/commands/job/artifact" + + "github.com/spf13/cobra" +) + +func NewCmdJob(f *cmdutils.Factory) *cobra.Command { + jobCmd := &cobra.Command{ + Use: "job [flags]", + Short: `Work with GitLab CI/CD jobs`, + Long: ``, + } + + cmdutils.EnableRepoOverride(jobCmd, f) + jobCmd.AddCommand(artifact.NewCmdArtifact(f)) + return jobCmd +} diff --git a/commands/job/job_test.go b/commands/job/job_test.go new file mode 100644 index 00000000..b66d2734 --- /dev/null +++ b/commands/job/job_test.go @@ -0,0 +1,72 @@ +package job + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +var tests = []struct { + name string + args string + expectedOut string + expectedErr string +}{ + { + name: "when no args should display the help message", + args: "", + expectedOut: "Use \"job [command] --help\" for more information about a command.\n", + expectedErr: "", + }, +} + +func TestJobCmd(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + wantedErr := "" + if len(test.expectedErr) > 0 { + wantedErr = test.expectedErr + } + + // Catching Stdout & Stderr + oldOut := os.Stdout + rOut, wOut, _ := os.Pipe() + os.Stdout = wOut + outC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, rOut) + outC <- buf.String() + }() + + oldErr := os.Stderr + rErr, wErr, _ := os.Pipe() + os.Stderr = wErr + errC := make(chan string) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, rErr) + errC <- buf.String() + }() + + err := NewCmdJob(&cmdutils.Factory{}).Execute() + + // Rollbacking Stdout & Stderr + wOut.Close() + os.Stdout = oldOut + stdout := <-outC + wErr.Close() + os.Stderr = oldErr + stderr := <-errC + + if assert.NoErrorf(t, err, "error running `job %s` : %v", test.args, err) { + assert.Contains(t, stderr, wantedErr) + assert.Contains(t, stdout, test.expectedOut) + } + }) + } +} diff --git a/commands/root.go b/commands/root.go index 69b6ace5..f0eb5466 100644 --- a/commands/root.go +++ b/commands/root.go @@ -19,6 +19,7 @@ import ( "gitlab.com/gitlab-org/cli/commands/help" incidentCmd "gitlab.com/gitlab-org/cli/commands/incident" issueCmd "gitlab.com/gitlab-org/cli/commands/issue" + jobCmd "gitlab.com/gitlab-org/cli/commands/job" labelCmd "gitlab.com/gitlab-org/cli/commands/label" mrCmd "gitlab.com/gitlab-org/cli/commands/mr" projectCmd "gitlab.com/gitlab-org/cli/commands/project" @@ -114,6 +115,7 @@ func NewCmdRoot(f *cmdutils.Factory, version, buildDate string) *cobra.Command { rootCmd.AddCommand(clusterCmd.NewCmdCluster(f)) rootCmd.AddCommand(issueCmd.NewCmdIssue(f)) rootCmd.AddCommand(incidentCmd.NewCmdIncident(f)) + rootCmd.AddCommand(jobCmd.NewCmdJob(f)) rootCmd.AddCommand(labelCmd.NewCmdLabel(f)) rootCmd.AddCommand(mrCmd.NewCmdMR(f)) rootCmd.AddCommand(pipelineCmd.NewCmdCI(f)) diff --git a/docs/source/ci/index.md b/docs/source/ci/index.md index 67f6e949..a4937a69 100644 --- a/docs/source/ci/index.md +++ b/docs/source/ci/index.md @@ -13,6 +13,10 @@ Please do not edit this file directly. Run `make gen-docs` instead. Work with GitLab CI/CD pipelines and jobs +```plaintext +glab ci [flags] +``` + ## Aliases ```plaintext diff --git a/docs/source/job/artifact.md b/docs/source/job/artifact.md new file mode 100644 index 00000000..4871630c --- /dev/null +++ b/docs/source/job/artifact.md @@ -0,0 +1,45 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab job artifact` + +Download all artifacts from the last pipeline + +```plaintext +glab job artifact [flags] +``` + +## Aliases + +```plaintext +push +``` + +## Examples + +```plaintext +glab job artifact main build +glab job artifact main deploy --path="artifacts/" + +``` + +## Options + +```plaintext + -p, --path string Path to download the artifact files (default "./") +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command + -R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL +``` diff --git a/docs/source/job/help.md b/docs/source/job/help.md new file mode 100644 index 00000000..cec25262 --- /dev/null +++ b/docs/source/job/help.md @@ -0,0 +1,25 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab job help` + +Help about any command + +```plaintext +glab job help [command] [flags] +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command + -R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL +``` diff --git a/docs/source/job/index.md b/docs/source/job/index.md new file mode 100644 index 00000000..fd9ce9a6 --- /dev/null +++ b/docs/source/job/index.md @@ -0,0 +1,30 @@ +--- +stage: Create +group: Code Review +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + + + +# `glab job` + +Work with GitLab CI/CD jobs + +## Options + +```plaintext + -R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for command +``` + +## Subcommands + +- [`artifact`](artifact.md)