diff --git a/api/secure_files.go b/api/secure_files.go new file mode 100644 index 0000000000000000000000000000000000000000..4b2d9a55bdfcfc71595a4f4aad9c5d693956002e --- /dev/null +++ b/api/secure_files.go @@ -0,0 +1,72 @@ +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 == 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) + 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 +} diff --git a/commands/project/repo.go b/commands/project/repo.go index 285b0cc87875a3bf895e02effa372e5a8971a8e1..4b33b0d11459c4ce3a990b492b4aec195de8ba03 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/create/create.go b/commands/project/securefile/create/create.go new file mode 100644 index 0000000000000000000000000000000000000000..5f697c3f018fc7d75d76f198348e4d48be2f8b0f --- /dev/null +++ b/commands/project/securefile/create/create.go @@ -0,0 +1,60 @@ +package create + +import ( + "fmt" + "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 { + apiClient, err := f.HttpClient() + if err != nil { + return err + } + + repo, err := f.BaseRepo() + if err != nil { + return err + } + + 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 fmt.Errorf("Error creating securefile: %v", err) + } + + fmt.Fprintln(f.IO.StdOut, "Created securefile with name", args[0]) + return nil + }, + } + 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/create/create_test.go b/commands/project/securefile/create/create_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2d0fa1f6cd95167b8d98db853f38c5cf4233b7ac --- /dev/null +++ b/commands/project/securefile/create/create_test.go @@ -0,0 +1,93 @@ +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 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) + } + }) + } +} + +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 0000000000000000000000000000000000000000..5ab2f8a4323abafb10abb68657d9d39f1a775057 --- /dev/null +++ b/commands/project/securefile/create/testdata/localfile.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/commands/project/securefile/download/download.go b/commands/project/securefile/download/download.go new file mode 100644 index 0000000000000000000000000000000000000000..2b46d4af3cb8afcb2b90bf78c1c6b03f596a535a --- /dev/null +++ b/commands/project/securefile/download/download.go @@ -0,0 +1,89 @@ +package download + +import ( + "fmt" + "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 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 download 1 --path="securefiles/file.txt" + - glab repo securefile download 1 --path="securefiles/file.txt" + `), + Long: ``, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) 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 fmt.Errorf("Secure file ID must be an integer: %s", args[0]) + } + + path, err := cmd.Flags().GetString("path") + if err != nil { + return fmt.Errorf("Unable to get path flag: %v", err) + } + + 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", "./downloaded.tmp", "Path to download the secure file to including filename and externsion.") + return securefileDownloadCmd +} + +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 fmt.Errorf("Error downloading securefile: %v", err) + } + + file, err := os.Create(path) + if err != nil { + return fmt.Errorf("Error creating file: %v", err) + } + defer func() { + closeErr := file.Close() + if closeErr != nil && err == nil { + err = closeErr + } + }() + + _, err = io.Copy(file, contents) + if err != nil { + return fmt.Errorf("Error writing to file: %v", err) + } + return nil +} diff --git a/commands/project/securefile/download/download_test.go b/commands/project/securefile/download/download_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1c06df8c2f938dd80f9905f0e3d39baf311db64b --- /dev/null +++ b/commands/project/securefile/download/download_test.go @@ -0,0 +1,98 @@ +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_SecurefileDownload(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 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) + } + + _, 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 0000000000000000000000000000000000000000..5ab2f8a4323abafb10abb68657d9d39f1a775057 --- /dev/null +++ b/commands/project/securefile/download/testdata/localfile.txt @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/commands/project/securefile/get/get.go b/commands/project/securefile/get/get.go new file mode 100644 index 0000000000000000000000000000000000000000..f7aacf59e76368a72d10a99662e1fe52c49ec17c --- /dev/null +++ b/commands/project/securefile/get/get.go @@ -0,0 +1,53 @@ +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 { + 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 fmt.Errorf("Secure file ID must be an integer: %s", args[0]) + } + + file, err := api.GetSecureFile(apiClient, repo.FullName(), fileID) + if err != nil { + return fmt.Errorf("Error getting securefile: %v", err) + } + + fileJSON, _ := json.Marshal(file) + fmt.Fprintln(f.IO.StdOut, string(fileJSON)) + return nil + }, + } + + return securefileGetCmd +} diff --git a/commands/project/securefile/get/get_test.go b/commands/project/securefile/get/get_test.go new file mode 100644 index 0000000000000000000000000000000000000000..184c3e7fd72803c0c069edf7736922179354d052 --- /dev/null +++ b/commands/project/securefile/get/get_test.go @@ -0,0 +1,93 @@ +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 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) + } + }) + } +} + +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 new file mode 100644 index 0000000000000000000000000000000000000000..239f7168e1756e4436a922a7ac01b6d64f877516 --- /dev/null +++ b/commands/project/securefile/list/list.go @@ -0,0 +1,77 @@ +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 { + 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.FullName()) + if err != nil { + return fmt.Errorf("Error listing securefiles: %v", 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/list/list_test.go b/commands/project/securefile/list/list_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0a0e87ac64f2f149b92d66ed50bc561b92263a49 --- /dev/null +++ b/commands/project/securefile/list/list_test.go @@ -0,0 +1,149 @@ +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 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) + } + }) + } +} + +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) +} diff --git a/commands/project/securefile/remove/remove.go b/commands/project/securefile/remove/remove.go new file mode 100644 index 0000000000000000000000000000000000000000..48fd0e325376938eee86b39505b06296768d37c5 --- /dev/null +++ b/commands/project/securefile/remove/remove.go @@ -0,0 +1,59 @@ +package remove + +import ( + "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 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 { + 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 fmt.Errorf("Secure file ID must be an integer: %s", args[0]) + } + + err = api.RemoveSecureFile(apiClient, repo.FullName(), fileID) + if err != nil { + return fmt.Errorf("Error removing securefile: %v", 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 0000000000000000000000000000000000000000..73278e3b4304262d54b969b03cfb84fc5869e613 --- /dev/null +++ b/commands/project/securefile/remove/remove_test.go @@ -0,0 +1,85 @@ +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 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) + } + }) + } +} + +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) +} diff --git a/commands/project/securefile/securefile.go b/commands/project/securefile/securefile.go new file mode 100644 index 0000000000000000000000000000000000000000..b776e2c2d9172d2c21aaf59d5a3df6c98df3a3b2 --- /dev/null +++ b/commands/project/securefile/securefile.go @@ -0,0 +1,35 @@ +package securefile + +import ( + "github.com/MakeNowJust/heredoc/v2" + "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" + securefileRemoveCmd "gitlab.com/gitlab-org/cli/commands/project/securefile/remove" +) + +func NewCmdSecurefile(f *cmdutils.Factory) *cobra.Command { + securefileCmd := &cobra.Command{ + Use: "securefile [flags]", + Short: `Manage project secure files.`, + 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) + + securefileCmd.AddCommand(securefileCreateCmd.NewCmdCreate(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/commands/project/securefile/securefile_test.go b/commands/project/securefile/securefile_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9617aa4b7d04e31c5e4283c3eeda7a6d9649eec1 --- /dev/null +++ b/commands/project/securefile/securefile_test.go @@ -0,0 +1,23 @@ +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 + defer func() { os.Stdout = old }() + + 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") +} diff --git a/docs/source/repo/index.md b/docs/source/repo/index.md index f0df0c564cd699da8e0a12597c9a58388b871efa..31b491d80b5ab6373b00b5825edd0016fa18a689 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/create.md b/docs/source/repo/securefile/create.md new file mode 100644 index 0000000000000000000000000000000000000000..11f7995f02e3f940b9691acd5a4d92e53e448dc6 --- /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/download.md b/docs/source/repo/securefile/download.md new file mode 100644 index 0000000000000000000000000000000000000000..36edb45ca1079230334f48dc49386cb6c7245ea1 --- /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 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 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 including filename and externsion. (default "./downloaded.tmp") +``` + +## 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/get.md b/docs/source/repo/securefile/get.md new file mode 100644 index 0000000000000000000000000000000000000000..0a030845bbbc3bc06c1a48f58c2e8dddf9652d34 --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..7bc1b92de6a9641f424c03084783571999be0aae --- /dev/null +++ b/docs/source/repo/securefile/index.md @@ -0,0 +1,41 @@ +--- +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. + +## 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 + -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 + +- [`create`](create.md) +- [`download`](download.md) +- [`get`](get.md) +- [`list`](list.md) +- [`remove`](remove.md) diff --git a/docs/source/repo/securefile/list.md b/docs/source/repo/securefile/list.md new file mode 100644 index 0000000000000000000000000000000000000000..5f7e01c3dc49c6e0f0941b62b3b9628822d2fc7e --- /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. +``` diff --git a/docs/source/repo/securefile/remove.md b/docs/source/repo/securefile/remove.md new file mode 100644 index 0000000000000000000000000000000000000000..c7531c78749891b29f33fcb2896c80e0b2a8a57f --- /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. +```