diff --git a/cmd/cli/app/artifact/artifact_list.go b/cmd/cli/app/artifact/artifact_list.go index 32b3b31d4d..234b6583cb 100644 --- a/cmd/cli/app/artifact/artifact_list.go +++ b/cmd/cli/app/artifact/artifact_list.go @@ -4,14 +4,12 @@ package artifact import ( - "context" "fmt" "strings" "time" "github.com/spf13/cobra" "github.com/spf13/viper" - "google.golang.org/grpc" "github.com/mindersec/minder/cmd/cli/app" "github.com/mindersec/minder/internal/util" @@ -25,69 +23,76 @@ var listCmd = &cobra.Command{ Use: "list", Short: "List artifacts from a provider", Long: `The artifact list subcommand will list artifacts from a provider.`, - RunE: cli.GRPCClientWrapRunE(listCommand), -} + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := viper.BindPFlags(cmd.Flags()); err != nil { + return fmt.Errorf("error binding flags: %w", err) + } -// listCommand is the artifact list subcommand -func listCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error { - client := minderv1.NewArtifactServiceClient(conn) + format := viper.GetString("output") + if !app.IsOutputFormatSupported(format) { + return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) + } - provider := viper.GetString("provider") - project := viper.GetString("project") - format := viper.GetString("output") - fromFilter := viper.GetString("from") + return nil + }, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() - // Ensure the output format is supported - if !app.IsOutputFormatSupported(format) { - return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) - } + client, closer, err := cli.GetCLIClient(cmd, minderv1.NewArtifactServiceClient) + if err != nil { + return err + } + defer closer() - // No longer print usage on returned error, since we've parsed our inputs - // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 - cmd.SilenceUsage = true + provider := viper.GetString("provider") + project := viper.GetString("project") + format := viper.GetString("output") + fromFilter := viper.GetString("from") - artifactList, err := client.ListArtifacts(ctx, &minderv1.ListArtifactsRequest{ - Context: &minderv1.Context{Provider: &provider, Project: &project}, - From: fromFilter, - }, - ) - - if err != nil { - return cli.MessageAndError("Couldn't list artifacts", err) - } - - switch format { - case app.Table: - t := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), - []string{"ID", "Type", "Owner", "Name", "Repository", "Visibility", "Creation date"}) - for _, artifact := range artifactList.Results { - t.AddRow( - artifact.ArtifactPk, - artifact.Type, - artifact.GetOwner(), - artifact.GetName(), - artifact.Repository, - artifact.Visibility, - artifact.CreatedAt.AsTime().Format(time.RFC3339), - ) + cmd.SilenceUsage = true - } - t.Render() - case app.JSON: - out, err := util.GetJsonFromProto(artifactList) + artifactList, err := client.ListArtifacts(ctx, &minderv1.ListArtifactsRequest{ + Context: &minderv1.Context{Provider: &provider, Project: &project}, + From: fromFilter, + }) if err != nil { - return cli.MessageAndError("Error getting json from proto", err) + return cli.MessageAndError("Couldn't list artifacts", err) } - cmd.Println(out) - case app.YAML: - out, err := util.GetYamlFromProto(artifactList) - if err != nil { - return cli.MessageAndError("Error getting yaml from proto", err) + + switch format { + case app.Table: + t := table.New(table.Simple, layouts.Default, cmd.OutOrStdout(), + []string{"ID", "Type", "Owner", "Name", "Repository", "Visibility", "Creation date"}) + for _, artifact := range artifactList.Results { + t.AddRow( + artifact.ArtifactPk, + artifact.Type, + artifact.GetOwner(), + artifact.GetName(), + artifact.Repository, + artifact.Visibility, + artifact.CreatedAt.AsTime().Format(time.RFC3339), + ) + } + t.Render() + + case app.JSON: + out, err := util.GetJsonFromProto(artifactList) + if err != nil { + return cli.MessageAndError("Error getting json from proto", err) + } + cmd.Println(out) + + case app.YAML: + out, err := util.GetYamlFromProto(artifactList) + if err != nil { + return cli.MessageAndError("Error getting yaml from proto", err) + } + cmd.Println(out) } - cmd.Println(out) - } - return nil + return nil + }, } func init() { diff --git a/cmd/cli/app/artifact/artifact_list_test.go b/cmd/cli/app/artifact/artifact_list_test.go new file mode 100644 index 0000000000..a8cf7780fe --- /dev/null +++ b/cmd/cli/app/artifact/artifact_list_test.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package artifact + +import ( + "context" + "testing" + + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/mindersec/minder/internal/util/cli" + minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" + mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock" +) + +//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state +func TestArtifactListCommand(t *testing.T) { + setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) + + mockResp := &minderv1.ListArtifactsResponse{} + cli.LoadFixture(t, "mock_artifact_list.json", mockResp) + + client.EXPECT(). + ListArtifacts(gomock.Any(), gomock.Any()). + Return(mockResp, nil). + Times(1) + + return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + } + + tests := []cli.CmdTestCase{ + { + Name: "list artifacts - table output", + Args: []string{"artifact", "list", "-o", "table"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list.table", + }, + { + Name: "list artifacts - yaml output", + Args: []string{"artifact", "list", "-o", "yaml"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list.yaml", + }, + { + Name: "list artifacts with from filter", + Args: []string{"artifact", "list", "--from", "repository=org/repo", "-o", "table"}, + MockSetup: setupSuccess, + GoldenFileName: "artifact_list_from.table", + }, + { + Name: "server error handling", + Args: []string{"artifact", "list"}, + MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { + t.Helper() + client := mockv1.NewMockArtifactServiceClient(ctrl) + client.EXPECT(). + ListArtifacts(gomock.Any(), gomock.Any()). + Return(nil, status.Error(codes.Internal, "internal server error")). + Times(1) + + return cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client) + }, + ExpectedError: "internal server error", + }, + } + + cli.RunCmdTests(t, tests, ArtifactCmd) +} diff --git a/cmd/cli/app/artifact/fixture/mock_artifact_list.json b/cmd/cli/app/artifact/fixture/mock_artifact_list.json new file mode 100644 index 0000000000..e5ceb08fed --- /dev/null +++ b/cmd/cli/app/artifact/fixture/mock_artifact_list.json @@ -0,0 +1,13 @@ +{ + "results": [ + { + "artifactPk": "111", + "name": "artifact-1", + "type": "image", + "owner": "owner-1", + "repository": "org/repo", + "visibility": "public", + "createdAt": "2024-01-02T15:04:05Z" + } + ] +} diff --git a/cmd/cli/app/artifact/testdata/artifact_list.table.golden b/cmd/cli/app/artifact/testdata/artifact_list.table.golden new file mode 100644 index 0000000000..7ae0d4a250 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list.table.golden @@ -0,0 +1,3 @@ + ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE +───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── + 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z diff --git a/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden b/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden new file mode 100644 index 0000000000..65772ae1a7 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list.yaml.golden @@ -0,0 +1,9 @@ +results: + - artifactPk: "111" + createdAt: "2024-01-02T15:04:05Z" + name: artifact-1 + owner: owner-1 + repository: org/repo + type: image + visibility: public + diff --git a/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden b/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden new file mode 100644 index 0000000000..7ae0d4a250 --- /dev/null +++ b/cmd/cli/app/artifact/testdata/artifact_list_from.table.golden @@ -0,0 +1,3 @@ + ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE +───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼─────────────────────────── + 111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z diff --git a/cmd/cli/app/profile/get_test.go b/cmd/cli/app/profile/get_test.go index 9a2d6b92f8..2784b39c1a 100644 --- a/cmd/cli/app/profile/get_test.go +++ b/cmd/cli/app/profile/get_test.go @@ -18,7 +18,6 @@ import ( //nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state func TestGetCommand(t *testing.T) { - testID := "00000000-0000-0000-0000-000000000001" testName := "test-profile" tests := []cli.CmdTestCase{ @@ -39,23 +38,6 @@ func TestGetCommand(t *testing.T) { }, GoldenFileName: "get_by_name_table.txt", }, - { - Name: "get by id json success", - Args: []string{"profile", "get", "--id", testID, "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockProfileServiceClient(ctrl) - mockProf := &minderv1.Profile{} - cli.LoadFixture(t, "mock_profile_get.json", mockProf) - - client.EXPECT(). - GetProfileById(gomock.Any(), gomock.Any()). - Return(&minderv1.GetProfileByIdResponse{Profile: mockProf}, nil) - - return cli.WithRPCClient[minderv1.ProfileServiceClient](context.Background(), client) - }, - GoldenFileName: "get_by_id.json", - }, { Name: "failure missing id and name", Args: []string{"profile", "get"}, diff --git a/cmd/cli/app/profile/list_test.go b/cmd/cli/app/profile/list_test.go index aff6fab7c4..c157ea9c09 100644 --- a/cmd/cli/app/profile/list_test.go +++ b/cmd/cli/app/profile/list_test.go @@ -37,24 +37,6 @@ func TestListCommand(t *testing.T) { }, GoldenFileName: "list_profiles_table.txt", }, - { - Name: "list profiles json success", - Args: []string{"profile", "list", "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockProfileServiceClient(ctrl) - - mockResp := &minderv1.ListProfilesResponse{} - cli.LoadFixture(t, "mock_profile_list.json", mockResp) - - client.EXPECT(). - ListProfiles(gomock.Any(), gomock.Any()). - Return(mockResp, nil) - - return cli.WithRPCClient[minderv1.ProfileServiceClient](context.Background(), client) - }, - GoldenFileName: "list_profiles.json", - }, { Name: "list profiles yaml success", Args: []string{"profile", "list", "-o", "yaml"}, diff --git a/cmd/cli/app/profile/testdata/get_by_id.json.golden b/cmd/cli/app/profile/testdata/get_by_id.json.golden deleted file mode 100644 index 4f83231319..0000000000 --- a/cmd/cli/app/profile/testdata/get_by_id.json.golden +++ /dev/null @@ -1,20 +0,0 @@ -{ - "profile": { - "context": { - "provider": "github", - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "11111111-1111-1111-1111-111111111111", - "name": "mock-profile", - "repository": [ - { - "type": "dependabot_configured", - "def": { - "package_ecosystem": "gomod" - } - } - ], - "remediate": "off", - "alert": "on" - } -} diff --git a/cmd/cli/app/profile/testdata/list_profiles.json.golden b/cmd/cli/app/profile/testdata/list_profiles.json.golden deleted file mode 100644 index 93e2580d9a..0000000000 --- a/cmd/cli/app/profile/testdata/list_profiles.json.golden +++ /dev/null @@ -1,60 +0,0 @@ -{ - "profiles": [ - { - "context": { - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "11111111-1111-1111-1111-111111111111", - "name": "mock-artifact-profile", - "artifact": [ - { - "type": "artifact_signature", - "params": { - "name": "mock-artifact", - "tags": [ - "latest" - ] - }, - "def": { - "is_signed": true, - "is_verified": true - }, - "name": "Mock ensure artifacts are signed" - } - ], - "remediate": "off", - "alert": "on", - "displayName": "Mock Artifact Signature Profile" - }, - { - "context": { - "project": "00000000-0000-0000-0000-000000000000" - }, - "id": "22222222-2222-2222-2222-222222222222", - "name": "mock-branch-protection", - "repository": [ - { - "type": "branch_protection_enabled", - "params": { - "branch": "main" - }, - "def": {}, - "name": "Mock enable branch protection" - }, - { - "type": "branch_protection_require_pull_request_approving_review_count", - "params": { - "branch": "main" - }, - "def": { - "required_approving_review_count": 2 - }, - "name": "Mock require 2 reviews" - } - ], - "remediate": "on", - "alert": "off", - "displayName": "Mock Branch Protection Profile" - } - ] -} diff --git a/cmd/cli/app/repo/repo_get_test.go b/cmd/cli/app/repo/repo_get_test.go index 13b6e679a5..2af70602d6 100644 --- a/cmd/cli/app/repo/repo_get_test.go +++ b/cmd/cli/app/repo/repo_get_test.go @@ -24,25 +24,6 @@ func TestGetCommand(t *testing.T) { ) tests := []cli.CmdTestCase{ - { - Name: "get repository by name - json output", - Args: []string{"repo", "get", "-n", repoName, "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockRepositoryServiceClient(ctrl) - - mockResp := &minderv1.GetRepositoryByNameResponse{} - cli.LoadFixture(t, "mock_repo_get.json", mockResp) - - client.EXPECT(). - GetRepositoryByName(gomock.Any(), gomock.Any()). - Return(mockResp, nil). - Times(1) - - return cli.WithRPCClient[minderv1.RepositoryServiceClient](context.Background(), client) - }, - GoldenFileName: "get_name_json.txt", - }, { Name: "get repository by id - yaml output", Args: []string{"repo", "get", "-i", repoID, "-o", "yaml"}, diff --git a/cmd/cli/app/repo/repo_list_test.go b/cmd/cli/app/repo/repo_list_test.go index eb8bbc15be..a441f463ba 100644 --- a/cmd/cli/app/repo/repo_list_test.go +++ b/cmd/cli/app/repo/repo_list_test.go @@ -38,25 +38,6 @@ func TestListCommand(t *testing.T) { }, GoldenFileName: "list_table.txt", }, - { - Name: "list repositories - json output", - Args: []string{"repo", "list", "-o", "json"}, - MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context { - t.Helper() - client := mockv1.NewMockRepositoryServiceClient(ctrl) - - mockResp := &minderv1.ListRepositoriesResponse{} - cli.LoadFixture(t, "mock_repo_list.json", mockResp) - - client.EXPECT(). - ListRepositories(gomock.Any(), gomock.Any()). - Return(mockResp, nil). - Times(1) - - return cli.WithRPCClient[minderv1.RepositoryServiceClient](context.Background(), client) - }, - GoldenFileName: "list_json.txt", - }, { Name: "list repositories - empty result", Args: []string{"repo", "list", "-o", "table"}, diff --git a/cmd/cli/app/repo/testdata/get_name_json.txt.golden b/cmd/cli/app/repo/testdata/get_name_json.txt.golden deleted file mode 100644 index 876ee15e3a..0000000000 --- a/cmd/cli/app/repo/testdata/get_name_json.txt.golden +++ /dev/null @@ -1,36 +0,0 @@ -{ - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-repo", - "repoId": "123456789", - "hookId": "987654321", - "hookUrl": "https://api.github.com/repos/mock-owner/mock-repo/hooks/987654321", - "deployUrl": "https://api.github.com/repos/mock-owner/mock-repo/deployments", - "cloneUrl": "https://github.com/mock-owner/mock-repo.git", - "defaultBranch": "main", - "properties": { - "github/clone_url": "https://github.com/mock-owner/mock-repo.git", - "github/default_branch": "main", - "github/deploy_url": "https://api.github.com/repos/mock-owner/mock-repo/deployments", - "github/hook_id": { - "minder.internal.type": "int64", - "minder.internal.value": "987654321" - }, - "github/hook_url": "https://api.github.com/repos/mock-owner/mock-repo/hooks/987654321", - "github/license": "MIT", - "github/primary_language": "Go", - "github/repo_id": { - "minder.internal.type": "int64", - "minder.internal.value": "123456789" - }, - "github/repo_name": "mock-repo", - "github/repo_owner": "mock-owner", - "is_archived": false, - "is_fork": false, - "is_private": false, - "name": "mock-owner/mock-repo", - "upstream_id": "123456789" - } -} diff --git a/cmd/cli/app/repo/testdata/list_json.txt.golden b/cmd/cli/app/repo/testdata/list_json.txt.golden deleted file mode 100644 index ef1b4221ad..0000000000 --- a/cmd/cli/app/repo/testdata/list_json.txt.golden +++ /dev/null @@ -1,20 +0,0 @@ -{ - "results": [ - { - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-frontend-repo", - "repoId": "1122334455" - }, - { - "context": { - "provider": "github" - }, - "owner": "mock-owner", - "name": "mock-backend-repo", - "repoId": "9988776655" - } - ] -} diff --git a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go index db051b9c02..db649ba174 100644 --- a/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go +++ b/pkg/api/protobuf/go/minder/v1/mock/mock_artifact.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + // Code generated by MockGen. DO NOT EDIT. // Source: github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1 (interfaces: ArtifactServiceClient) // @@ -42,6 +45,26 @@ func (m *MockArtifactServiceClient) EXPECT() *MockArtifactServiceClientMockRecor return m.recorder } +// ListArtifacts mocks base method. +func (m *MockArtifactServiceClient) ListArtifacts(ctx context.Context, in *v1.ListArtifactsRequest, opts ...grpc.CallOption) (*v1.ListArtifactsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ListArtifacts", varargs...) + ret0, _ := ret[0].(*v1.ListArtifactsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListArtifacts indicates an expected call of ListArtifacts. +func (mr *MockArtifactServiceClientMockRecorder) ListArtifacts(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifacts", reflect.TypeOf((*MockArtifactServiceClient)(nil).ListArtifacts), varargs...) +} + // GetArtifactById mocks base method. func (m *MockArtifactServiceClient) GetArtifactById(ctx context.Context, in *v1.GetArtifactByIdRequest, opts ...grpc.CallOption) (*v1.GetArtifactByIdResponse, error) { m.ctrl.T.Helper() @@ -81,23 +104,3 @@ func (mr *MockArtifactServiceClientMockRecorder) GetArtifactByName(ctx, in any, varargs := append([]any{ctx, in}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetArtifactByName", reflect.TypeOf((*MockArtifactServiceClient)(nil).GetArtifactByName), varargs...) } - -// ListArtifacts mocks base method. -func (m *MockArtifactServiceClient) ListArtifacts(ctx context.Context, in *v1.ListArtifactsRequest, opts ...grpc.CallOption) (*v1.ListArtifactsResponse, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, in} - for _, a := range opts { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ListArtifacts", varargs...) - ret0, _ := ret[0].(*v1.ListArtifactsResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListArtifacts indicates an expected call of ListArtifacts. -func (mr *MockArtifactServiceClientMockRecorder) ListArtifacts(ctx, in any, opts ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, in}, opts...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifacts", reflect.TypeOf((*MockArtifactServiceClient)(nil).ListArtifacts), varargs...) -}