From c66ebccf54e206236bb616dc5fb7e154209f746d Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Fri, 28 Mar 2025 16:46:18 +0000 Subject: [PATCH 01/21] feat(securefile): add project secure file support - add commands for maintaining project secure files - add create, download, get, list and remove Delivers #7790 --- api/secure_files.go | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 api/secure_files.go diff --git a/api/secure_files.go b/api/secure_files.go new file mode 100644 index 000000000..d4999fc60 --- /dev/null +++ b/api/secure_files.go @@ -0,0 +1,62 @@ +package api + +import ( + "io" + + gitlab "gitlab.com/gitlab-org/api/client-go" +) + +var CreateSecureFile = func(client *gitlab.Client, projectID interface{}, filename string, content io.Reader) error { + if client == nil { + client = apiClient.Lab() + } + + _, _, err := client.SecureFiles.CreateSecureFile(projectID, content, filename) + return err +} + +var DownloadSecureFile = func(client *gitlab.Client, projectID interface{}, id int) (io.Reader, error) { + if client == nil { + client = apiClient.Lab() + } + + reader, _, err := client.SecureFiles.DownloadSecureFile(projectID, id) + if err != nil { + return nil, err + } + return reader, nil +} + +var GetSecureFile = func(client *gitlab.Client, projectID interface{}, id int) (*gitlab.SecureFile, error) { + if client == nil { + client = apiClient.Lab() + } + + file, _, err := client.SecureFiles.ShowSecureFileDetails(projectID, id) + return file, err +} + +var ListSecureFiles = func(client *gitlab.Client, l *gitlab.ListProjectSecureFilesOptions, projectID interface{}) ([]*gitlab.SecureFile, error) { + if client == nil { + client = apiClient.Lab() + } + + if l.PerPage == 0 { + l.PerPage = DefaultListLimit + } + + files, _, err := client.SecureFiles.ListProjectSecureFiles(projectID, nil) + if err != nil { + return nil, err + } + return files, nil +} + +var RemoveSecureFile = func(client *gitlab.Client, projectID interface{}, id int) error { + if client == nil { + client = apiClient.Lab() + } + + _, err := client.SecureFiles.RemoveSecureFile(projectID, id) + return err +} -- GitLab From 8d69f3255598ed6077319e66841473fb0ee681ac Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Fri, 28 Mar 2025 17:20:45 +0000 Subject: [PATCH 02/21] feat(securefile): start adding download subcommand --- commands/project/repo.go | 2 + .../project/securefile/download/download.go | 73 +++++++++++++++++++ commands/project/securefile/securefile.go | 21 ++++++ docs/source/repo/index.md | 1 + docs/source/repo/securefile/download.md | 44 +++++++++++ docs/source/repo/securefile/index.md | 30 ++++++++ 6 files changed, 171 insertions(+) create mode 100644 commands/project/securefile/download/download.go create mode 100644 commands/project/securefile/securefile.go create mode 100644 docs/source/repo/securefile/download.md create mode 100644 docs/source/repo/securefile/index.md diff --git a/commands/project/repo.go b/commands/project/repo.go index 285b0cc87..4b33b0d11 100644 --- a/commands/project/repo.go +++ b/commands/project/repo.go @@ -12,6 +12,7 @@ import ( repoCmdMirror "gitlab.com/gitlab-org/cli/commands/project/mirror" repoCmdPublish "gitlab.com/gitlab-org/cli/commands/project/publish" repoCmdSearch "gitlab.com/gitlab-org/cli/commands/project/search" + repoCmdSecurefile "gitlab.com/gitlab-org/cli/commands/project/securefile" repoCmdTransfer "gitlab.com/gitlab-org/cli/commands/project/transfer" repoCmdView "gitlab.com/gitlab-org/cli/commands/project/view" @@ -34,6 +35,7 @@ func NewCmdRepo(f *cmdutils.Factory) *cobra.Command { repoCmd.AddCommand(repoCmdDelete.NewCmdDelete(f)) repoCmd.AddCommand(repoCmdFork.NewCmdFork(f, nil)) repoCmd.AddCommand(repoCmdSearch.NewCmdSearch(f)) + repoCmd.AddCommand(repoCmdSecurefile.NewCmdSecurefile(f)) repoCmd.AddCommand(repoCmdTransfer.NewCmdTransfer(f)) repoCmd.AddCommand(repoCmdView.NewCmdView(f)) repoCmd.AddCommand(repoCmdMirror.NewCmdMirror(f)) diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go new file mode 100644 index 000000000..38af7be39 --- /dev/null +++ b/commands/project/securefile/download/download.go @@ -0,0 +1,73 @@ +package download + +import ( + "io" + "os" + "strconv" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/spf13/cobra" + gitlab "gitlab.com/gitlab-org/api/client-go" + + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/internal/glrepo" +) + +func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { + securefileDownloadCmd := &cobra.Command{ + Use: "download [flags]", + Short: `Download project secure file.`, + Example: heredoc.Doc(` + Download a project's secure file using the file's ID. + - glab project securefile 1 + - glab repo securefile 1 + + Download a project's secure file using the file's ID to a given path. + - glab project securefile 1 --path="securefiles/" + - glab repo securefile 1 --path="securefiles/" + `), + Long: ``, + Args: cobra.ExactArgs(1), + 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 + } + fileID, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + path, err := cmd.Flags().GetString("path") + if err != nil { + return err + } + return saveFile(apiClient, repo, fileID, path) + }, + } + securefileDownloadCmd.Flags().StringP("path", "p", "./", "Path to download the secure file to.") + return securefileDownloadCmd +} + +func saveFile(apiClient *gitlab.Client, repo glrepo.Interface, fileID int, path string) error { + contents, err := api.DownloadSecureFile(apiClient, repo, fileID) + if err != nil { + return err + } + + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, contents) + if err != nil { + return err + } + return nil +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go new file mode 100644 index 000000000..8ca373976 --- /dev/null +++ b/commands/project/securefile/securefile.go @@ -0,0 +1,21 @@ +package securefile + +import ( + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + + securefileDownloadCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/download" +) + +func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { + securefileCmd := &cobra.Command{ + Use: "securefile [flags]", + Short: `Manage project secure files.`, + Long: ``, + } + + cmdutils.EnableRepoOverride(securefileCmd, f) + + securefileCmd.AddCommand(securefileDownloadCmd.NewCmdDownload(f)) + return securefileCmd +} diff --git a/docs/source/repo/index.md b/docs/source/repo/index.md index f0df0c564..31b491d80 100644 --- a/docs/source/repo/index.md +++ b/docs/source/repo/index.md @@ -37,5 +37,6 @@ project - [`mirror`](mirror.md) - [`publish`](publish/index.md) - [`search`](search.md) +- [`securefile`](securefile/index.md) - [`transfer`](transfer.md) - [`view`](view.md) diff --git a/docs/source/repo/securefile/download.md b/docs/source/repo/securefile/download.md new file mode 100644 index 000000000..46b687d9d --- /dev/null +++ b/docs/source/repo/securefile/download.md @@ -0,0 +1,44 @@ +--- +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 repo securefile download` + +Download project secure file. + +```plaintext +glab repo securefile download [flags] +``` + +## Examples + +```plaintext + Download a project's secure file using the file's ID. +- glab project securefile 1 +- glab repo securefile 1 + +Download a project's secure file using the file's ID to a given path. +- glab project securefile 1 --path="securefiles/" +- glab repo securefile 1 --path="securefiles/" + +``` + +## Options + +```plaintext + -p, --path string Path to download the secure file to. (default "./") +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md new file mode 100644 index 000000000..6947ec1d5 --- /dev/null +++ b/docs/source/repo/securefile/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 repo securefile` + +Manage project secure files. + +## Options + +```plaintext + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. +``` + +## Subcommands + +- [`download`](download.md) -- GitLab From 495bad0b46f01debcdcb0bed41cb87875dd595fd Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Fri, 28 Mar 2025 17:28:24 +0000 Subject: [PATCH 03/21] Add test for main cmd --- .../project/securefile/securefile_test.go | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 commands/project/securefile/securefile_test.go diff --git a/commands/project/securefile/securefile_test.go b/commands/project/securefile/securefile_test.go new file mode 100644 index 000000000..9b0077a53 --- /dev/null +++ b/commands/project/securefile/securefile_test.go @@ -0,0 +1,22 @@ +package securefile + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "gitlab.com/gitlab-org/cli/commands/cmdutils" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_Securefile(t *testing.T) { + old := os.Stdout // keep backup of the real stdout + r, w, _ := os.Pipe() + os.Stdout = w + + assert.Nil(t, NewCmdSecurefile(&cmdutils.Factory{}).Execute()) + + out := test.ReturnBuffer(old, r, w) + + assert.Contains(t, out, "Use \"securefile [command] --help\" for more information about a command.\n") +} -- GitLab From a317caffd7552e47bf4fce423f10c7da33ef03ab Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Fri, 28 Mar 2025 18:44:58 +0000 Subject: [PATCH 04/21] feat(securefile): start adding list subcommand --- commands/project/securefile/list/list.go | 79 +++++++++++++++++++++++ commands/project/securefile/securefile.go | 2 + docs/source/repo/securefile/index.md | 1 + docs/source/repo/securefile/list.md | 59 +++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 commands/project/securefile/list/list.go create mode 100644 docs/source/repo/securefile/list.md diff --git a/commands/project/securefile/list/list.go b/commands/project/securefile/list/list.go new file mode 100644 index 000000000..1a05900d6 --- /dev/null +++ b/commands/project/securefile/list/list.go @@ -0,0 +1,79 @@ +package list + +import ( + "encoding/json" + "fmt" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/spf13/cobra" + gitlab "gitlab.com/gitlab-org/api/client-go" + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func NewCmdList(f *cmdutils.Factory) *cobra.Command { + securefileListCmd := &cobra.Command{ + Use: "list [flags]", + Short: `List project secure files.`, + Long: ``, + Aliases: []string{"ls"}, + Example: heredoc.Doc(` + List all secure files. + - glab project securefile list + - glab repo securefile list + + List all secure files with cmd alias. + - glab project securefile ls + - glab repo securefile ls + + List specific page of secure files. + - glab project securefile list --page 2 + - glab repo securefile list --page 2 + + List specific page of secure files with custom page size. + - glab project securefile list --page 2 --per-page 10 + - glab repo securefile list --page 2 --per-page 10 + `), + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + + apiClient, err := f.HttpClient() + if err != nil { + return err + } + + repo, err := f.BaseRepo() + if err != nil { + return err + } + + l := &gitlab.ListProjectSecureFilesOptions{ + Page: 1, + PerPage: api.DefaultListLimit, + } + + if p, _ := cmd.Flags().GetInt("page"); p != 0 { + l.Page = p + } + if p, _ := cmd.Flags().GetInt("per-page"); p != 0 { + l.PerPage = p + } + + files, err := api.ListSecureFiles(apiClient, l, repo) + if err != nil { + return err + } + + fileListJSON, _ := json.Marshal(files) + fmt.Fprintln(f.IO.StdOut, string(fileListJSON)) + + return nil + }, + } + + securefileListCmd.Flags().IntP("page", "p", 1, "Page number.") + securefileListCmd.Flags().IntP("per-page", "P", 30, "Number of items to list per page.") + + return securefileListCmd +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go index 8ca373976..412803224 100644 --- a/commands/project/securefile/securefile.go +++ b/commands/project/securefile/securefile.go @@ -5,6 +5,7 @@ import ( "gitlab.com/gitlab-org/cli/commands/cmdutils" securefileDownloadCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/download" + securefileListCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/list" ) func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { @@ -17,5 +18,6 @@ func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { cmdutils.EnableRepoOverride(securefileCmd, f) securefileCmd.AddCommand(securefileDownloadCmd.NewCmdDownload(f)) + securefileCmd.AddCommand(securefileListCmd.NewCmdList(f)) return securefileCmd } diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md index 6947ec1d5..385bf5340 100644 --- a/docs/source/repo/securefile/index.md +++ b/docs/source/repo/securefile/index.md @@ -28,3 +28,4 @@ Manage project secure files. ## Subcommands - [`download`](download.md) +- [`list`](list.md) diff --git a/docs/source/repo/securefile/list.md b/docs/source/repo/securefile/list.md new file mode 100644 index 000000000..5f7e01c3d --- /dev/null +++ b/docs/source/repo/securefile/list.md @@ -0,0 +1,59 @@ +--- +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 repo securefile list` + +List project secure files. + +```plaintext +glab repo securefile list [flags] +``` + +## Aliases + +```plaintext +ls +``` + +## Examples + +```plaintext +List all secure files. +- glab project securefile list +- glab repo securefile list + +List all secure files with cmd alias. +- glab project securefile ls +- glab repo securefile ls + +List specific page of secure files. +- glab project securefile list --page 2 +- glab repo securefile list --page 2 + +List specific page of secure files with custom page size. +- glab project securefile list --page 2 --per-page 10 +- glab repo securefile list --page 2 --per-page 10 + +``` + +## Options + +```plaintext + -p, --page int Page number. (default 1) + -P, --per-page int Number of items to list per page. (default 30) +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` -- GitLab From efd4a59ecf5b2d68b0078cbbc1c82e5ae4a8252e Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sat, 29 Mar 2025 11:39:21 +0000 Subject: [PATCH 05/21] feat(securefile): start adding remove subcommand --- .../project/securefile/download/download.go | 8 +-- commands/project/securefile/remove/remove.go | 49 +++++++++++++++++++ commands/project/securefile/securefile.go | 2 + docs/source/repo/securefile/download.md | 8 +-- docs/source/repo/securefile/index.md | 1 + docs/source/repo/securefile/remove.md | 49 +++++++++++++++++++ 6 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 commands/project/securefile/remove/remove.go create mode 100644 docs/source/repo/securefile/remove.md diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index 38af7be39..8a088acc2 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -20,12 +20,12 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { Short: `Download project secure file.`, Example: heredoc.Doc(` Download a project's secure file using the file's ID. - - glab project securefile 1 - - glab repo securefile 1 + - glab project securefile download 1 + - glab repo securefile download 1 Download a project's secure file using the file's ID to a given path. - - glab project securefile 1 --path="securefiles/" - - glab repo securefile 1 --path="securefiles/" + - glab project securefile download 1 --path="securefiles/" + - glab repo securefile download 1 --path="securefiles/" `), Long: ``, Args: cobra.ExactArgs(1), diff --git a/commands/project/securefile/remove/remove.go b/commands/project/securefile/remove/remove.go new file mode 100644 index 000000000..0848bd707 --- /dev/null +++ b/commands/project/securefile/remove/remove.go @@ -0,0 +1,49 @@ +package remove + +import ( + "strconv" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func NewCmdRemove(f *cmdutils.Factory) *cobra.Command { + securefileRemoveCmd := &cobra.Command{ + Use: "remove ", + Short: `Remove a project secure file.`, + Long: ``, + Aliases: []string{"rm", "delete"}, + Args: cobra.ExactArgs(1), + Example: heredoc.Doc(` + Remove a project's secure file using the file's ID. + - glab project securefile remove 1 + - glab repo securefile remove 1 + + Remove a project's secure file with rm cmd alias. + - glab project securefile rm 1 + - glab repo securefile rm 1 + + Remove a project's secure file with delete cmd alias. + - glab project securefile delete 1 + - glab repo securefile delete 1 + `), + 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 + } + fileID, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + return api.RemoveSecureFile(apiClient, repo, fileID) + }, + } + return securefileRemoveCmd +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go index 412803224..16ecf0c46 100644 --- a/commands/project/securefile/securefile.go +++ b/commands/project/securefile/securefile.go @@ -6,6 +6,7 @@ import ( securefileDownloadCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/download" securefileListCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/list" + securefileRemoveCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/remove" ) func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { @@ -19,5 +20,6 @@ func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { securefileCmd.AddCommand(securefileDownloadCmd.NewCmdDownload(f)) securefileCmd.AddCommand(securefileListCmd.NewCmdList(f)) + securefileCmd.AddCommand(securefileRemoveCmd.NewCmdRemove(f)) return securefileCmd } diff --git a/docs/source/repo/securefile/download.md b/docs/source/repo/securefile/download.md index 46b687d9d..54c376aad 100644 --- a/docs/source/repo/securefile/download.md +++ b/docs/source/repo/securefile/download.md @@ -21,12 +21,12 @@ glab repo securefile download [flags] ```plaintext Download a project's secure file using the file's ID. -- glab project securefile 1 -- glab repo securefile 1 +- glab project securefile download 1 +- glab repo securefile download 1 Download a project's secure file using the file's ID to a given path. -- glab project securefile 1 --path="securefiles/" -- glab repo securefile 1 --path="securefiles/" +- glab project securefile download 1 --path="securefiles/" +- glab repo securefile download 1 --path="securefiles/" ``` diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md index 385bf5340..028604735 100644 --- a/docs/source/repo/securefile/index.md +++ b/docs/source/repo/securefile/index.md @@ -29,3 +29,4 @@ Manage project secure files. - [`download`](download.md) - [`list`](list.md) +- [`remove`](remove.md) diff --git a/docs/source/repo/securefile/remove.md b/docs/source/repo/securefile/remove.md new file mode 100644 index 000000000..c7531c787 --- /dev/null +++ b/docs/source/repo/securefile/remove.md @@ -0,0 +1,49 @@ +--- +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 repo securefile remove` + +Remove a project secure file. + +```plaintext +glab repo securefile remove [flags] +``` + +## Aliases + +```plaintext +rm +delete +``` + +## Examples + +```plaintext +Remove a project's secure file using the file's ID. +- glab project securefile remove 1 +- glab repo securefile remove 1 + +Remove a project's secure file with rm cmd alias. +- glab project securefile rm 1 +- glab repo securefile rm 1 + +Remove a project's secure file with delete cmd alias. +- glab project securefile delete 1 +- glab repo securefile delete 1 + +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` -- GitLab From df6477d4ec88df091e651922a02564ae9509e561 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sat, 29 Mar 2025 11:51:48 +0000 Subject: [PATCH 06/21] feat(securefile): start adding get subcommand --- commands/project/securefile/get/get.go | 56 +++++++++++++++++++++++ commands/project/securefile/securefile.go | 2 + docs/source/repo/securefile/get.md | 34 ++++++++++++++ docs/source/repo/securefile/index.md | 1 + 4 files changed, 93 insertions(+) create mode 100644 commands/project/securefile/get/get.go create mode 100644 docs/source/repo/securefile/get.md diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go new file mode 100644 index 000000000..4ae391c47 --- /dev/null +++ b/commands/project/securefile/get/get.go @@ -0,0 +1,56 @@ +package get + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func NewCmdGet(f *cmdutils.Factory) *cobra.Command { + securefileGetCmd := &cobra.Command{ + Use: "get ", + Short: `Get details of a project secure file.`, + Long: ``, + Args: cobra.ExactArgs(1), + Example: heredoc.Doc(` + Get details of a project's secure file using the file's ID. + - glab project securefile get 1 + - glab repo securefile get 1 + `), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + + apiClient, err := f.HttpClient() + if err != nil { + return err + } + + repo, err := f.BaseRepo() + if err != nil { + return err + } + + fileID, err := strconv.Atoi(args[0]) + if err != nil { + return err + } + + file, err := api.GetSecureFile(apiClient, repo, fileID) + if err != nil { + return err + } + + fileJSON, _ := json.Marshal(file) + fmt.Fprintln(f.IO.StdOut, string(fileJSON)) + + return nil + }, + } + + return securefileGetCmd +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go index 16ecf0c46..36a58ea26 100644 --- a/commands/project/securefile/securefile.go +++ b/commands/project/securefile/securefile.go @@ -5,6 +5,7 @@ import ( "gitlab.com/gitlab-org/cli/commands/cmdutils" securefileDownloadCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/download" + securefileGetCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/get" securefileListCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/list" securefileRemoveCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/remove" ) @@ -19,6 +20,7 @@ func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { cmdutils.EnableRepoOverride(securefileCmd, f) securefileCmd.AddCommand(securefileDownloadCmd.NewCmdDownload(f)) + securefileCmd.AddCommand(securefileGetCmd.NewCmdGet(f)) securefileCmd.AddCommand(securefileListCmd.NewCmdList(f)) securefileCmd.AddCommand(securefileRemoveCmd.NewCmdRemove(f)) return securefileCmd diff --git a/docs/source/repo/securefile/get.md b/docs/source/repo/securefile/get.md new file mode 100644 index 000000000..0a030845b --- /dev/null +++ b/docs/source/repo/securefile/get.md @@ -0,0 +1,34 @@ +--- +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 repo securefile get` + +Get details of a project secure file. + +```plaintext +glab repo securefile get [flags] +``` + +## Examples + +```plaintext +Get details of a project's secure file using the file's ID. +- glab project securefile get 1 +- glab repo securefile get 1 + +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md index 028604735..c14dd6224 100644 --- a/docs/source/repo/securefile/index.md +++ b/docs/source/repo/securefile/index.md @@ -28,5 +28,6 @@ Manage project secure files. ## Subcommands - [`download`](download.md) +- [`get`](get.md) - [`list`](list.md) - [`remove`](remove.md) -- GitLab From 62ab131ad55102f8200a324f071977e6a5be25aa Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sat, 29 Mar 2025 12:22:01 +0000 Subject: [PATCH 07/21] feat(securefile): start adding create subcommand --- commands/project/securefile/create/create.go | 51 ++++++++++++++++++++ commands/project/securefile/securefile.go | 2 + docs/source/repo/securefile/create.md | 34 +++++++++++++ docs/source/repo/securefile/index.md | 1 + 4 files changed, 88 insertions(+) create mode 100644 commands/project/securefile/create/create.go create mode 100644 docs/source/repo/securefile/create.md diff --git a/commands/project/securefile/create/create.go b/commands/project/securefile/create/create.go new file mode 100644 index 000000000..cc2ed7d66 --- /dev/null +++ b/commands/project/securefile/create/create.go @@ -0,0 +1,51 @@ +package create + +import ( + "io" + "os" + + "github.com/MakeNowJust/heredoc/v2" + "github.com/spf13/cobra" + "gitlab.com/gitlab-org/cli/api" + "gitlab.com/gitlab-org/cli/commands/cmdutils" +) + +func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { + securefileCreateCmd := &cobra.Command{ + Use: "create ", + Short: `Create a new project secure file.`, + Example: heredoc.Doc(` + Create a project's secure file with file name using the contents of file path. + - glab project securefile create "newfile.txt" "securefiles/localfile.txt" + - glab repo securefile create "newfile.txt" "securefiles/localfile.txt" + `), + 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 + } + reader, err := getReaderFromFilePath(args[1]) + if err != nil { + return err + } + + return api.CreateSecureFile(apiClient, repo, args[0], reader) + }, + } + return securefileCreateCmd +} + +func getReaderFromFilePath(filePath string) (io.Reader, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + + return file, nil +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go index 36a58ea26..5a35e3421 100644 --- a/commands/project/securefile/securefile.go +++ b/commands/project/securefile/securefile.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "gitlab.com/gitlab-org/cli/commands/cmdutils" + securefileCreateCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/create" securefileDownloadCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/download" securefileGetCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/get" securefileListCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/list" @@ -19,6 +20,7 @@ func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { cmdutils.EnableRepoOverride(securefileCmd, f) + securefileCmd.AddCommand(securefileCreateCmd.NewCmdCreate(f)) securefileCmd.AddCommand(securefileDownloadCmd.NewCmdDownload(f)) securefileCmd.AddCommand(securefileGetCmd.NewCmdGet(f)) securefileCmd.AddCommand(securefileListCmd.NewCmdList(f)) diff --git a/docs/source/repo/securefile/create.md b/docs/source/repo/securefile/create.md new file mode 100644 index 000000000..11f7995f0 --- /dev/null +++ b/docs/source/repo/securefile/create.md @@ -0,0 +1,34 @@ +--- +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 repo securefile create` + +Create a new project secure file. + +```plaintext +glab repo securefile create [flags] +``` + +## Examples + +```plaintext +Create a project's secure file with file name using the contents of file path. +- glab project securefile create "newfile.txt" "securefiles/localfile.txt" +- glab repo securefile create "newfile.txt" "securefiles/localfile.txt" + +``` + +## Options inherited from parent commands + +```plaintext + --help Show help for this command. + -R, --repo OWNER/REPO Select another repository. Can use either OWNER/REPO or `GROUP/NAMESPACE/REPO` format. Also accepts full URL or Git URL. +``` diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md index c14dd6224..27ca94395 100644 --- a/docs/source/repo/securefile/index.md +++ b/docs/source/repo/securefile/index.md @@ -27,6 +27,7 @@ Manage project secure files. ## Subcommands +- [`create`](create.md) - [`download`](download.md) - [`get`](get.md) - [`list`](list.md) -- GitLab From 1331c7094cb03f0ccb93750c6cc327df1c8538cd Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 17:38:04 +0100 Subject: [PATCH 08/21] test(securefile): start adding tests --- commands/project/securefile/create/create.go | 2 +- .../project/securefile/download/download.go | 2 +- commands/project/securefile/get/get.go | 4 +- commands/project/securefile/get/get_test.go | 94 +++++++++++++++++++ commands/project/securefile/list/list.go | 2 +- commands/project/securefile/remove/remove.go | 16 +++- .../project/securefile/remove/remove_test.go | 86 +++++++++++++++++ 7 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 commands/project/securefile/get/get_test.go create mode 100644 commands/project/securefile/remove/remove_test.go diff --git a/commands/project/securefile/create/create.go b/commands/project/securefile/create/create.go index cc2ed7d66..67615511c 100644 --- a/commands/project/securefile/create/create.go +++ b/commands/project/securefile/create/create.go @@ -35,7 +35,7 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { return err } - return api.CreateSecureFile(apiClient, repo, args[0], reader) + return api.CreateSecureFile(apiClient, repo.FullName(), args[0], reader) }, } return securefileCreateCmd diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index 8a088acc2..8fe9b27c6 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -54,7 +54,7 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { } func saveFile(apiClient *gitlab.Client, repo glrepo.Interface, fileID int, path string) error { - contents, err := api.DownloadSecureFile(apiClient, repo, fileID) + contents, err := api.DownloadSecureFile(apiClient, repo.FullName(), fileID) if err != nil { return err } diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go index 4ae391c47..d99ba687a 100644 --- a/commands/project/securefile/get/get.go +++ b/commands/project/securefile/get/get.go @@ -37,10 +37,10 @@ func NewCmdGet(f *cmdutils.Factory) *cobra.Command { fileID, err := strconv.Atoi(args[0]) if err != nil { - return err + return fmt.Errorf("Secure file ID must be an integer: %s", args[0]) } - file, err := api.GetSecureFile(apiClient, repo, fileID) + file, err := api.GetSecureFile(apiClient, repo.FullName(), fileID) if err != nil { return err } diff --git a/commands/project/securefile/get/get_test.go b/commands/project/securefile/get/get_test.go new file mode 100644 index 000000000..bb78e2598 --- /dev/null +++ b/commands/project/securefile/get/get_test.go @@ -0,0 +1,94 @@ +package get + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_SecurefileGet(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + testCases := []struct { + Name string + ExpectedMsg []string + wantErr bool + cli string + wantStderr string + httpMocks []httpMock + }{ + { + Name: "Get securefile", + ExpectedMsg: []string{"{\"id\":1,\"name\":\"myfile.jks\",\"checksum\":\"16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac\",\"checksum_algorithm\":\"sha256\",\"created_at\":\"2022-02-22T22:22:22Z\",\"expires_at\":null,\"metadata\":null}\n"}, + cli: "1", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files/1", + http.StatusOK, + `{ + "id": 1, + "name": "myfile.jks", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }`, + }, + }, + }, + { + Name: "Get a securefile with invalid file ID", + cli: "abc", + httpMocks: []httpMock{}, + wantErr: true, + wantStderr: "Secure file ID must be an integer: abc", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathOnly, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + out, err := runCommand(fakeHTTP, false, tc.cli) + if err != nil { + if tc.wantErr == true { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) + } + return + } + } + + for _, msg := range tc.ExpectedMsg { + require.Contains(t, out.String(), msg) + } + }) + } +} + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) + _, _ = factory.HttpClient() + cmd := NewCmdGet(factory) + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} diff --git a/commands/project/securefile/list/list.go b/commands/project/securefile/list/list.go index 1a05900d6..f86db10b8 100644 --- a/commands/project/securefile/list/list.go +++ b/commands/project/securefile/list/list.go @@ -60,7 +60,7 @@ func NewCmdList(f *cmdutils.Factory) *cobra.Command { l.PerPage = p } - files, err := api.ListSecureFiles(apiClient, l, repo) + files, err := api.ListSecureFiles(apiClient, l, repo.FullName()) if err != nil { return err } diff --git a/commands/project/securefile/remove/remove.go b/commands/project/securefile/remove/remove.go index 0848bd707..b77cf6ac1 100644 --- a/commands/project/securefile/remove/remove.go +++ b/commands/project/securefile/remove/remove.go @@ -1,6 +1,7 @@ package remove import ( + "fmt" "strconv" "github.com/MakeNowJust/heredoc/v2" @@ -30,19 +31,28 @@ func NewCmdRemove(f *cmdutils.Factory) *cobra.Command { - glab repo securefile delete 1 `), RunE: func(cmd *cobra.Command, args []string) error { - repo, err := f.BaseRepo() + apiClient, err := f.HttpClient() if err != nil { return err } - apiClient, err := f.HttpClient() + + repo, err := f.BaseRepo() if err != nil { return err } + fileID, err := strconv.Atoi(args[0]) if err != nil { return err } - return api.RemoveSecureFile(apiClient, repo, fileID) + + err = api.RemoveSecureFile(apiClient, repo.FullName(), fileID) + if err != nil { + return err + } + fmt.Fprintln(f.IO.StdOut, "Deleted securefile with ID", fileID) + + return nil }, } return securefileRemoveCmd diff --git a/commands/project/securefile/remove/remove_test.go b/commands/project/securefile/remove/remove_test.go new file mode 100644 index 000000000..a08363812 --- /dev/null +++ b/commands/project/securefile/remove/remove_test.go @@ -0,0 +1,86 @@ +package remove + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_SecurefileRemove(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + testCases := []struct { + Name string + ExpectedMsg []string + wantErr bool + cli string + wantStderr string + httpMocks []httpMock + }{ + { + Name: "Remove a securefile", + ExpectedMsg: []string{"Deleted securefile with ID 1"}, + cli: "1", + httpMocks: []httpMock{ + { + http.MethodDelete, + "/api/v4/projects/OWNER/REPO/secure_files/1", + http.StatusNoContent, + "", + }, + }, + }, + { + Name: "Remove a securefile with invalid file ID", + cli: "abc", + httpMocks: []httpMock{}, + wantErr: true, + wantStderr: "Secure file ID must be an integer: abc", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathOnly, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + out, err := runCommand(fakeHTTP, false, tc.cli) + if err != nil { + if tc.wantErr == true { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) + } + return + } + } + + for _, msg := range tc.ExpectedMsg { + require.Contains(t, out.String(), msg) + } + }) + } +} + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) + _, _ = factory.HttpClient() + cmd := NewCmdRemove(factory) + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} -- GitLab From 7409f20045dd5865ee549232f4d165d8b62f17ff Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 18:14:29 +0100 Subject: [PATCH 09/21] test(securefile): start adding tests --- commands/project/securefile/create/create.go | 6 ++++-- commands/project/securefile/download/download.go | 11 ++++++++--- commands/project/securefile/get/get.go | 2 -- commands/project/securefile/list/list.go | 4 +--- commands/project/securefile/remove/remove.go | 4 ++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/commands/project/securefile/create/create.go b/commands/project/securefile/create/create.go index 67615511c..093d84020 100644 --- a/commands/project/securefile/create/create.go +++ b/commands/project/securefile/create/create.go @@ -22,14 +22,16 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { Long: ``, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - repo, err := f.BaseRepo() + apiClient, err := f.HttpClient() if err != nil { return err } - apiClient, err := f.HttpClient() + + repo, err := f.BaseRepo() if err != nil { return err } + reader, err := getReaderFromFilePath(args[1]) if err != nil { return err diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index 8fe9b27c6..2d7ae6676 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -1,6 +1,7 @@ package download import ( + "fmt" "io" "os" "strconv" @@ -30,22 +31,26 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { Long: ``, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - repo, err := f.BaseRepo() + apiClient, err := f.HttpClient() if err != nil { return err } - apiClient, err := f.HttpClient() + + repo, err := f.BaseRepo() if err != nil { return err } + fileID, err := strconv.Atoi(args[0]) if err != nil { - return err + return fmt.Errorf("Secure file ID must be an integer: %s", args[0]) } + path, err := cmd.Flags().GetString("path") if err != nil { return err } + return saveFile(apiClient, repo, fileID, path) }, } diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go index d99ba687a..a4d6e287c 100644 --- a/commands/project/securefile/get/get.go +++ b/commands/project/securefile/get/get.go @@ -23,8 +23,6 @@ func NewCmdGet(f *cmdutils.Factory) *cobra.Command { - glab repo securefile get 1 `), RunE: func(cmd *cobra.Command, args []string) error { - var err error - apiClient, err := f.HttpClient() if err != nil { return err diff --git a/commands/project/securefile/list/list.go b/commands/project/securefile/list/list.go index f86db10b8..1565ddc80 100644 --- a/commands/project/securefile/list/list.go +++ b/commands/project/securefile/list/list.go @@ -36,8 +36,6 @@ func NewCmdList(f *cmdutils.Factory) *cobra.Command { `), Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - var err error - apiClient, err := f.HttpClient() if err != nil { return err @@ -56,6 +54,7 @@ func NewCmdList(f *cmdutils.Factory) *cobra.Command { if p, _ := cmd.Flags().GetInt("page"); p != 0 { l.Page = p } + if p, _ := cmd.Flags().GetInt("per-page"); p != 0 { l.PerPage = p } @@ -67,7 +66,6 @@ func NewCmdList(f *cmdutils.Factory) *cobra.Command { fileListJSON, _ := json.Marshal(files) fmt.Fprintln(f.IO.StdOut, string(fileListJSON)) - return nil }, } diff --git a/commands/project/securefile/remove/remove.go b/commands/project/securefile/remove/remove.go index b77cf6ac1..77747156d 100644 --- a/commands/project/securefile/remove/remove.go +++ b/commands/project/securefile/remove/remove.go @@ -43,15 +43,15 @@ func NewCmdRemove(f *cmdutils.Factory) *cobra.Command { fileID, err := strconv.Atoi(args[0]) if err != nil { - return err + return fmt.Errorf("Secure file ID must be an integer: %s", args[0]) } err = api.RemoveSecureFile(apiClient, repo.FullName(), fileID) if err != nil { return err } - fmt.Fprintln(f.IO.StdOut, "Deleted securefile with ID", fileID) + fmt.Fprintln(f.IO.StdOut, "Deleted securefile with ID", fileID) return nil }, } -- GitLab From 638cb02839405743c83a5b59a62a73c4e1f1fe95 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 18:39:39 +0100 Subject: [PATCH 10/21] test(securefile): start adding test for list --- api/secure_files.go | 2 +- commands/project/securefile/get/get.go | 1 - commands/project/securefile/list/list_test.go | 150 ++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 commands/project/securefile/list/list_test.go diff --git a/api/secure_files.go b/api/secure_files.go index d4999fc60..53cb0b142 100644 --- a/api/secure_files.go +++ b/api/secure_files.go @@ -45,7 +45,7 @@ var ListSecureFiles = func(client *gitlab.Client, l *gitlab.ListProjectSecureFil l.PerPage = DefaultListLimit } - files, _, err := client.SecureFiles.ListProjectSecureFiles(projectID, nil) + files, _, err := client.SecureFiles.ListProjectSecureFiles(projectID, l) if err != nil { return nil, err } diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go index a4d6e287c..409aa2140 100644 --- a/commands/project/securefile/get/get.go +++ b/commands/project/securefile/get/get.go @@ -45,7 +45,6 @@ func NewCmdGet(f *cmdutils.Factory) *cobra.Command { fileJSON, _ := json.Marshal(file) fmt.Fprintln(f.IO.StdOut, string(fileJSON)) - return nil }, } diff --git a/commands/project/securefile/list/list_test.go b/commands/project/securefile/list/list_test.go new file mode 100644 index 000000000..6ff0ec2e5 --- /dev/null +++ b/commands/project/securefile/list/list_test.go @@ -0,0 +1,150 @@ +package list + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_SecurefileList(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + testCases := []struct { + Name string + ExpectedMsg []string + wantErr bool + cli string + wantStderr string + httpMocks []httpMock + }{ + { + Name: "List securefiles", + ExpectedMsg: []string{"[{\"id\":1,\"name\":\"myfile.jks\",\"checksum\":\"16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac\",\"checksum_algorithm\":\"sha256\",\"created_at\":\"2022-02-22T22:22:22Z\",\"expires_at\":null,\"metadata\":null}]\n"}, + cli: "", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files?page=1&per_page=30", + http.StatusOK, + `[{ + "id": 1, + "name": "myfile.jks", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }]`, + }, + }, + }, + { + Name: "Get a securefile with custom pagination values", + ExpectedMsg: []string{"[{\"id\":1,\"name\":\"myfile.jks\",\"checksum\":\"16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac\",\"checksum_algorithm\":\"sha256\",\"created_at\":\"2022-02-22T22:22:22Z\",\"expires_at\":null,\"metadata\":null}]\n"}, + cli: "--page 2 --per-page 10", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files?page=2&per_page=10", + http.StatusOK, + `[{ + "id": 1, + "name": "myfile.jks", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }]`, + }, + }, + }, + { + Name: "Get a securefile with page defaults per page number", + ExpectedMsg: []string{"[{\"id\":1,\"name\":\"myfile.jks\",\"checksum\":\"16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac\",\"checksum_algorithm\":\"sha256\",\"created_at\":\"2022-02-22T22:22:22Z\",\"expires_at\":null,\"metadata\":null}]\n"}, + cli: "--page 2", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files?page=2&per_page=30", + http.StatusOK, + `[{ + "id": 1, + "name": "myfile.jks", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }]`, + }, + }, + }, + { + Name: "Get a securefile with per page defaults page number", + ExpectedMsg: []string{"[{\"id\":1,\"name\":\"myfile.jks\",\"checksum\":\"16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac\",\"checksum_algorithm\":\"sha256\",\"created_at\":\"2022-02-22T22:22:22Z\",\"expires_at\":null,\"metadata\":null}]\n"}, + cli: "--per-page 10", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files?page=1&per_page=10", + http.StatusOK, + `[{ + "id": 1, + "name": "myfile.jks", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }]`, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathAndQuerystring, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + out, err := runCommand(fakeHTTP, false, tc.cli) + if err != nil { + if tc.wantErr == true { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) + } + return + } + } + + for _, msg := range tc.ExpectedMsg { + require.Contains(t, out.String(), msg) + } + }) + } +} + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) + _, _ = factory.HttpClient() + cmd := NewCmdList(factory) + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} -- GitLab From 2409014daea38d47ca25e09895b2b833129df15e Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 19:00:17 +0100 Subject: [PATCH 11/21] test(securefile): start adding test for create --- commands/project/securefile/create/create.go | 9 +- .../project/securefile/create/create_test.go | 94 +++++++++++++++++++ .../securefile/create/testdata/localfile.txt | 1 + 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 commands/project/securefile/create/create_test.go create mode 100644 commands/project/securefile/create/testdata/localfile.txt diff --git a/commands/project/securefile/create/create.go b/commands/project/securefile/create/create.go index 093d84020..c5427e380 100644 --- a/commands/project/securefile/create/create.go +++ b/commands/project/securefile/create/create.go @@ -1,6 +1,7 @@ package create import ( + "fmt" "io" "os" @@ -33,11 +34,17 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { } reader, err := getReaderFromFilePath(args[1]) + if err != nil { + return fmt.Errorf("Unable to read file at: %s", args[1]) + } + + err = api.CreateSecureFile(apiClient, repo.FullName(), args[0], reader) if err != nil { return err } - return api.CreateSecureFile(apiClient, repo.FullName(), args[0], reader) + fmt.Fprintln(f.IO.StdOut, "Created securefile with name", args[0]) + return nil }, } return securefileCreateCmd diff --git a/commands/project/securefile/create/create_test.go b/commands/project/securefile/create/create_test.go new file mode 100644 index 000000000..c8347d7fe --- /dev/null +++ b/commands/project/securefile/create/create_test.go @@ -0,0 +1,94 @@ +package create + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_SecurefileCreate(t *testing.T) { + type httpMock struct { + method string + path string + status int + body string + } + + testCases := []struct { + Name string + ExpectedMsg []string + wantErr bool + cli string + wantStderr string + httpMocks []httpMock + }{ + { + Name: "Create securefile", + ExpectedMsg: []string{"Created securefile with name newfile.txt"}, + cli: "newfile.txt testdata/localfile.txt", + httpMocks: []httpMock{ + { + http.MethodPost, + "/api/v4/projects/OWNER/REPO/secure_files", + http.StatusOK, + `{ + "id": 1, + "name": "newfile.txt", + "checksum": "16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac", + "checksum_algorithm": "sha256", + "created_at": "2022-02-22T22:22:22.000Z", + "expires_at": null, + "metadata": null + }`, + }, + }, + }, + { + Name: "Get a securefile with invalid file path", + cli: "newfile.txt testdata/missingfile.txt", + httpMocks: []httpMock{}, + wantErr: true, + wantStderr: "Unable to read file at: testdata/missingfile.txt", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathOnly, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewStringResponse(mock.status, mock.body)) + } + + out, err := runCommand(fakeHTTP, false, tc.cli) + if err != nil { + if tc.wantErr == true { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) + } + return + } + } + + for _, msg := range tc.ExpectedMsg { + require.Contains(t, out.String(), msg) + } + }) + } +} + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) + _, _ = factory.HttpClient() + cmd := NewCmdCreate(factory) + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} diff --git a/commands/project/securefile/create/testdata/localfile.txt b/commands/project/securefile/create/testdata/localfile.txt new file mode 100644 index 000000000..5ab2f8a43 --- /dev/null +++ b/commands/project/securefile/create/testdata/localfile.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file -- GitLab From 75b32a4ffd1d1fcb2e7f9fc3dfbb9eb4be4a8ee0 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:09:48 +0100 Subject: [PATCH 12/21] test(securefile): start adding test for download --- .../project/securefile/download/download.go | 14 ++- .../securefile/download/download_test.go | 99 +++++++++++++++++++ .../download/testdata/localfile.txt | 1 + 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 commands/project/securefile/download/download_test.go create mode 100644 commands/project/securefile/download/testdata/localfile.txt diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index 2d7ae6676..d1d11c016 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -25,8 +25,8 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { - glab repo securefile download 1 Download a project's secure file using the file's ID to a given path. - - glab project securefile download 1 --path="securefiles/" - - glab repo securefile download 1 --path="securefiles/" + - glab project securefile download 1 --path="securefiles/file.txt" + - glab repo securefile download 1 --path="securefiles/file.txt" `), Long: ``, Args: cobra.ExactArgs(1), @@ -51,10 +51,16 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { return err } - return saveFile(apiClient, repo, fileID, path) + err = saveFile(apiClient, repo, fileID, path) + if err != nil { + return err + } + + fmt.Fprintln(f.IO.StdOut, "Downloaded securefile with ID", fileID) + return nil }, } - securefileDownloadCmd.Flags().StringP("path", "p", "./", "Path to download the secure file to.") + securefileDownloadCmd.Flags().StringP("path", "p", "./downloaded.tmp", "Path to download the secure file to including filename and externsion.") return securefileDownloadCmd } diff --git a/commands/project/securefile/download/download_test.go b/commands/project/securefile/download/download_test.go new file mode 100644 index 000000000..34de640bb --- /dev/null +++ b/commands/project/securefile/download/download_test.go @@ -0,0 +1,99 @@ +package download + +import ( + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/cli/commands/cmdtest" + "gitlab.com/gitlab-org/cli/pkg/httpmock" + "gitlab.com/gitlab-org/cli/test" +) + +func Test_SecurefileCreate(t *testing.T) { + type httpMock struct { + method string + path string + status int + } + + testCases := []struct { + Name string + ExpectedMsg []string + ExpectedFileLocation string + wantErr bool + cli string + wantStderr string + httpMocks []httpMock + }{ + { + Name: "Download securefile to current folder", + ExpectedMsg: []string{"Downloaded securefile with ID 1"}, + ExpectedFileLocation: "downloaded.tmp", + cli: "1", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files/1/download", + http.StatusOK, + }, + }, + }, + { + Name: "Download securefile to custom folder", + ExpectedMsg: []string{"Downloaded securefile with ID 1"}, + ExpectedFileLocation: "testdata/new.txt", + cli: "1 --path=testdata/new.txt", + httpMocks: []httpMock{ + { + http.MethodGet, + "/api/v4/projects/OWNER/REPO/secure_files/1/download", + http.StatusOK, + }, + }, + }, + } + + defer os.Remove("downloaded.tmp") + defer os.Remove("testdata/new.txt") + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + fakeHTTP := &httpmock.Mocker{ + MatchURL: httpmock.PathOnly, + } + defer fakeHTTP.Verify(t) + + for _, mock := range tc.httpMocks { + fakeHTTP.RegisterResponder(mock.method, mock.path, httpmock.NewFileResponse(mock.status, "testdata/localfile.txt")) + } + + out, err := runCommand(fakeHTTP, false, tc.cli) + if err != nil { + if tc.wantErr == true { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) + } + return + } + } + + for _, msg := range tc.ExpectedMsg { + require.Contains(t, out.String(), msg) + } + + _, err = os.Stat(tc.ExpectedFileLocation) + require.NoError(t, err) + }) + } +} + +func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { + ios, _, stdout, stderr := cmdtest.InitIOStreams(isTTY, "") + factory := cmdtest.InitFactory(ios, rt) + _, _ = factory.HttpClient() + cmd := NewCmdDownload(factory) + return cmdtest.ExecuteCommand(cmd, cli, stdout, stderr) +} diff --git a/commands/project/securefile/download/testdata/localfile.txt b/commands/project/securefile/download/testdata/localfile.txt new file mode 100644 index 000000000..5ab2f8a43 --- /dev/null +++ b/commands/project/securefile/download/testdata/localfile.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file -- GitLab From 39b83498af5aaa1302714eed6251f99a83f33030 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:20:57 +0100 Subject: [PATCH 13/21] docs(securefile): update docs for download --- docs/source/repo/securefile/download.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/repo/securefile/download.md b/docs/source/repo/securefile/download.md index 54c376aad..36edb45ca 100644 --- a/docs/source/repo/securefile/download.md +++ b/docs/source/repo/securefile/download.md @@ -25,15 +25,15 @@ glab repo securefile download [flags] - glab repo securefile download 1 Download a project's secure file using the file's ID to a given path. -- glab project securefile download 1 --path="securefiles/" -- glab repo securefile download 1 --path="securefiles/" +- glab project securefile download 1 --path="securefiles/file.txt" +- glab repo securefile download 1 --path="securefiles/file.txt" ``` ## Options ```plaintext - -p, --path string Path to download the secure file to. (default "./") + -p, --path string Path to download the secure file to including filename and externsion. (default "./downloaded.tmp") ``` ## Options inherited from parent commands -- GitLab From 86701ce6c5e746f3f90e300e2c57b7b66c6afb8c Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:40:28 +0100 Subject: [PATCH 14/21] refactor(securefile): add more specific error messages --- commands/project/securefile/create/create.go | 2 +- commands/project/securefile/download/download.go | 8 ++++---- commands/project/securefile/get/get.go | 2 +- commands/project/securefile/list/list.go | 2 +- commands/project/securefile/remove/remove.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/commands/project/securefile/create/create.go b/commands/project/securefile/create/create.go index c5427e380..5f697c3f0 100644 --- a/commands/project/securefile/create/create.go +++ b/commands/project/securefile/create/create.go @@ -40,7 +40,7 @@ func NewCmdCreate(f *cmdutils.Factory) *cobra.Command { err = api.CreateSecureFile(apiClient, repo.FullName(), args[0], reader) if err != nil { - return err + return fmt.Errorf("Error creating securefile: %v", err) } fmt.Fprintln(f.IO.StdOut, "Created securefile with name", args[0]) diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index d1d11c016..91e9e62c7 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -48,7 +48,7 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { path, err := cmd.Flags().GetString("path") if err != nil { - return err + return fmt.Errorf("Unable to get path flag: %v", err) } err = saveFile(apiClient, repo, fileID, path) @@ -67,18 +67,18 @@ func NewCmdDownload(f *cmdutils.Factory) *cobra.Command { func saveFile(apiClient *gitlab.Client, repo glrepo.Interface, fileID int, path string) error { contents, err := api.DownloadSecureFile(apiClient, repo.FullName(), fileID) if err != nil { - return err + return fmt.Errorf("Error downloading securefile: %v", err) } file, err := os.Create(path) if err != nil { - return err + return fmt.Errorf("Error creating file: %v", err) } defer file.Close() _, err = io.Copy(file, contents) if err != nil { - return err + return fmt.Errorf("Error writing to file: %v", err) } return nil } diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go index 409aa2140..f7aacf59e 100644 --- a/commands/project/securefile/get/get.go +++ b/commands/project/securefile/get/get.go @@ -40,7 +40,7 @@ func NewCmdGet(f *cmdutils.Factory) *cobra.Command { file, err := api.GetSecureFile(apiClient, repo.FullName(), fileID) if err != nil { - return err + return fmt.Errorf("Error getting securefile: %v", err) } fileJSON, _ := json.Marshal(file) diff --git a/commands/project/securefile/list/list.go b/commands/project/securefile/list/list.go index 1565ddc80..239f7168e 100644 --- a/commands/project/securefile/list/list.go +++ b/commands/project/securefile/list/list.go @@ -61,7 +61,7 @@ func NewCmdList(f *cmdutils.Factory) *cobra.Command { files, err := api.ListSecureFiles(apiClient, l, repo.FullName()) if err != nil { - return err + return fmt.Errorf("Error listing securefiles: %v", err) } fileListJSON, _ := json.Marshal(files) diff --git a/commands/project/securefile/remove/remove.go b/commands/project/securefile/remove/remove.go index 77747156d..48fd0e325 100644 --- a/commands/project/securefile/remove/remove.go +++ b/commands/project/securefile/remove/remove.go @@ -48,7 +48,7 @@ func NewCmdRemove(f *cmdutils.Factory) *cobra.Command { err = api.RemoveSecureFile(apiClient, repo.FullName(), fileID) if err != nil { - return err + return fmt.Errorf("Error removing securefile: %v", err) } fmt.Fprintln(f.IO.StdOut, "Deleted securefile with ID", fileID) -- GitLab From ab39df0fb7750e10b758abe16c233b7ebdec9de9 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:42:54 +0100 Subject: [PATCH 15/21] test(securefile): correct download test name --- commands/project/securefile/download/download_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/project/securefile/download/download_test.go b/commands/project/securefile/download/download_test.go index 34de640bb..5e92981ec 100644 --- a/commands/project/securefile/download/download_test.go +++ b/commands/project/securefile/download/download_test.go @@ -12,7 +12,7 @@ import ( "gitlab.com/gitlab-org/cli/test" ) -func Test_SecurefileCreate(t *testing.T) { +func Test_SecurefileDownload(t *testing.T) { type httpMock struct { method string path string -- GitLab From 276169b2e09974295aed381aacb159bbd470194a Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:45:33 +0100 Subject: [PATCH 16/21] fix(securefile): handle error when closing file --- commands/project/securefile/download/download.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index 91e9e62c7..e2485616b 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -74,7 +74,12 @@ func saveFile(apiClient *gitlab.Client, repo glrepo.Interface, fileID int, path if err != nil { return fmt.Errorf("Error creating file: %v", err) } - defer file.Close() + defer func() { + closeErr := file.Close() + if err == nil { + err = closeErr + } + }() _, err = io.Copy(file, contents) if err != nil { -- GitLab From 5a66e7d6fc4cea082799b92b620eb8271349f380 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 21:47:29 +0100 Subject: [PATCH 17/21] test(securefile): reset stdout to prevent panics --- commands/project/securefile/securefile_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/commands/project/securefile/securefile_test.go b/commands/project/securefile/securefile_test.go index 9b0077a53..9617aa4b7 100644 --- a/commands/project/securefile/securefile_test.go +++ b/commands/project/securefile/securefile_test.go @@ -13,6 +13,7 @@ func Test_Securefile(t *testing.T) { old := os.Stdout // keep backup of the real stdout r, w, _ := os.Pipe() os.Stdout = w + defer func() { os.Stdout = old }() assert.Nil(t, NewCmdSecurefile(&cmdutils.Factory{}).Execute()) -- GitLab From baa8c8e47796489d75e4a4177be43c8cd788294f Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 22:02:53 +0100 Subject: [PATCH 18/21] docs(securefile): add more info on secure files --- commands/project/securefile/securefile.go | 8 +++++++- docs/source/repo/securefile/index.md | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go index 5a35e3421..b776e2c2d 100644 --- a/commands/project/securefile/securefile.go +++ b/commands/project/securefile/securefile.go @@ -1,6 +1,7 @@ package securefile import ( + "github.com/MakeNowJust/heredoc/v2" "github.com/spf13/cobra" "gitlab.com/gitlab-org/cli/commands/cmdutils" @@ -15,7 +16,12 @@ func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { securefileCmd := &cobra.Command{ Use: "securefile [flags]", Short: `Manage project secure files.`, - Long: ``, + Long: heredoc.Docf(` + You can securely store up to 100 files for use in CI/CD pipelines as secure files. + These files are stored securely outside of your project's repository and are not + version controlled. It is safe to store sensitive information in these files. + Secure files support both plain text and binary file types but must be 5 MB or less. + `), } cmdutils.EnableRepoOverride(securefileCmd, f) diff --git a/docs/source/repo/securefile/index.md b/docs/source/repo/securefile/index.md index 27ca94395..7bc1b92de 100644 --- a/docs/source/repo/securefile/index.md +++ b/docs/source/repo/securefile/index.md @@ -13,6 +13,13 @@ Please do not edit this file directly. Run `make gen-docs` instead. Manage project secure files. +## Synopsis + +You can securely store up to 100 files for use in CI/CD pipelines as secure files. +These files are stored securely outside of your project's repository and are not +version controlled. It is safe to store sensitive information in these files. +Secure files support both plain text and binary file types but must be 5 MB or less. + ## Options ```plaintext -- GitLab From 5d9d788584bea0fcdd9813181ea8e1c2b5b736e5 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 22:18:15 +0100 Subject: [PATCH 19/21] fix(securefile): ensure list options are defaulted when not provided --- api/secure_files.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/api/secure_files.go b/api/secure_files.go index 53cb0b142..4b2d9a55b 100644 --- a/api/secure_files.go +++ b/api/secure_files.go @@ -41,8 +41,18 @@ var ListSecureFiles = func(client *gitlab.Client, l *gitlab.ListProjectSecureFil client = apiClient.Lab() } - if l.PerPage == 0 { - l.PerPage = DefaultListLimit + if l == nil { + l = &gitlab.ListProjectSecureFilesOptions{ + Page: 1, + PerPage: DefaultListLimit, + } + } else { + if l.PerPage == 0 { + l.PerPage = DefaultListLimit + } + if l.Page == 0 { + l.Page = 1 + } } files, _, err := client.SecureFiles.ListProjectSecureFiles(projectID, l) -- GitLab From 62adc9bcc62271925834d2775345494488e7085c Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 22:18:53 +0100 Subject: [PATCH 20/21] test(securefile): improve error assertion --- commands/project/securefile/create/create_test.go | 11 +++++------ commands/project/securefile/download/download_test.go | 11 +++++------ commands/project/securefile/get/get_test.go | 11 +++++------ commands/project/securefile/list/list_test.go | 11 +++++------ commands/project/securefile/remove/remove_test.go | 11 +++++------ 5 files changed, 25 insertions(+), 30 deletions(-) diff --git a/commands/project/securefile/create/create_test.go b/commands/project/securefile/create/create_test.go index c8347d7fe..2d0fa1f6c 100644 --- a/commands/project/securefile/create/create_test.go +++ b/commands/project/securefile/create/create_test.go @@ -69,14 +69,13 @@ func Test_SecurefileCreate(t *testing.T) { } out, err := runCommand(fakeHTTP, false, tc.cli) - if err != nil { - if tc.wantErr == true { - if assert.Error(t, err) { - require.Equal(t, tc.wantStderr, err.Error()) - } - return + if tc.wantErr { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) } + return } + require.NoError(t, err) for _, msg := range tc.ExpectedMsg { require.Contains(t, out.String(), msg) diff --git a/commands/project/securefile/download/download_test.go b/commands/project/securefile/download/download_test.go index 5e92981ec..1c06df8c2 100644 --- a/commands/project/securefile/download/download_test.go +++ b/commands/project/securefile/download/download_test.go @@ -71,14 +71,13 @@ func Test_SecurefileDownload(t *testing.T) { } out, err := runCommand(fakeHTTP, false, tc.cli) - if err != nil { - if tc.wantErr == true { - if assert.Error(t, err) { - require.Equal(t, tc.wantStderr, err.Error()) - } - return + if tc.wantErr { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) } + return } + require.NoError(t, err) for _, msg := range tc.ExpectedMsg { require.Contains(t, out.String(), msg) diff --git a/commands/project/securefile/get/get_test.go b/commands/project/securefile/get/get_test.go index bb78e2598..184c3e7fd 100644 --- a/commands/project/securefile/get/get_test.go +++ b/commands/project/securefile/get/get_test.go @@ -69,14 +69,13 @@ func Test_SecurefileGet(t *testing.T) { } out, err := runCommand(fakeHTTP, false, tc.cli) - if err != nil { - if tc.wantErr == true { - if assert.Error(t, err) { - require.Equal(t, tc.wantStderr, err.Error()) - } - return + if tc.wantErr { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) } + return } + require.NoError(t, err) for _, msg := range tc.ExpectedMsg { require.Contains(t, out.String(), msg) diff --git a/commands/project/securefile/list/list_test.go b/commands/project/securefile/list/list_test.go index 6ff0ec2e5..0a0e87ac6 100644 --- a/commands/project/securefile/list/list_test.go +++ b/commands/project/securefile/list/list_test.go @@ -125,14 +125,13 @@ func Test_SecurefileList(t *testing.T) { } out, err := runCommand(fakeHTTP, false, tc.cli) - if err != nil { - if tc.wantErr == true { - if assert.Error(t, err) { - require.Equal(t, tc.wantStderr, err.Error()) - } - return + if tc.wantErr { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) } + return } + require.NoError(t, err) for _, msg := range tc.ExpectedMsg { require.Contains(t, out.String(), msg) diff --git a/commands/project/securefile/remove/remove_test.go b/commands/project/securefile/remove/remove_test.go index a08363812..73278e3b4 100644 --- a/commands/project/securefile/remove/remove_test.go +++ b/commands/project/securefile/remove/remove_test.go @@ -61,14 +61,13 @@ func Test_SecurefileRemove(t *testing.T) { } out, err := runCommand(fakeHTTP, false, tc.cli) - if err != nil { - if tc.wantErr == true { - if assert.Error(t, err) { - require.Equal(t, tc.wantStderr, err.Error()) - } - return + if tc.wantErr { + if assert.Error(t, err) { + require.Equal(t, tc.wantStderr, err.Error()) } + return } + require.NoError(t, err) for _, msg := range tc.ExpectedMsg { require.Contains(t, out.String(), msg) -- GitLab From d63f4de2b7627cc8b8ab75dbed647acd5db0d940 Mon Sep 17 00:00:00 2001 From: Heidi Berry Date: Sun, 30 Mar 2025 22:19:17 +0100 Subject: [PATCH 21/21] fix(securefile): only return close error if not nil --- commands/project/securefile/download/download.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go index e2485616b..2b46d4af3 100644 --- a/commands/project/securefile/download/download.go +++ b/commands/project/securefile/download/download.go @@ -76,7 +76,7 @@ func saveFile(apiClient *gitlab.Client, repo glrepo.Interface, fileID int, path } defer func() { closeErr := file.Close() - if err == nil { + if closeErr != nil && err == nil { err = closeErr } }() -- GitLab