diff --git a/internal/pkg/cli/command/apiKey/delete.go b/internal/pkg/cli/command/apiKey/delete.go index a5b0ddaa..8da15158 100644 --- a/internal/pkg/cli/command/apiKey/delete.go +++ b/internal/pkg/cli/command/apiKey/delete.go @@ -13,12 +13,14 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteAPIKeyOptions struct { apiKeyId string skipConfirmation bool + json bool } var ( @@ -58,7 +60,7 @@ func NewDeleteKeyCmd() *cobra.Command { exit.Error(err, "Failed to describe existing API key") } - if !options.skipConfirmation { + if !options.skipConfirmation && !options.json { confirmDeleteApiKey(keyToDelete.Name) } @@ -67,14 +69,23 @@ func NewDeleteKeyCmd() *cobra.Command { msg.FailMsg("Failed to delete API key %s: %s", style.Emphasis(keyToDelete.Name), err) exit.Errorf(err, "Failed to delete API key %s", keyToDelete.Name) } - msg.SuccessMsg("API key %s deleted", style.Emphasis(keyToDelete.Name)) // Check if the key is locally stored and clean it up if so managedKey, ok := secrets.GetProjectManagedKey(keyToDelete.ProjectId) if ok && managedKey.Id == keyToDelete.Id { secrets.DeleteProjectManagedKey(keyToDelete.ProjectId) - msg.SuccessMsg("Deleted local record for key %s (project %s)", style.Emphasis(keyToDelete.Id), style.Emphasis(keyToDelete.ProjectId)) } + + if options.json { + fmt.Println(text.IndentJSON(struct { + Deleted bool `json:"deleted"` + Name string `json:"name"` + Id string `json:"id"` + }{Deleted: true, Name: keyToDelete.Name, Id: keyToDelete.Id})) + return + } + + msg.SuccessMsg("API key %s deleted", style.Emphasis(keyToDelete.Name)) }, } @@ -82,6 +93,7 @@ func NewDeleteKeyCmd() *cobra.Command { _ = cmd.MarkFlagRequired("id") cmd.Flags().BoolVar(&options.skipConfirmation, "skip-confirmation", false, "Skip deletion confirmation prompt") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON (also skips confirmation prompt)") return cmd } diff --git a/internal/pkg/cli/command/auth/logout.go b/internal/pkg/cli/command/auth/logout.go index b0b5cdf8..dfeb9f03 100644 --- a/internal/pkg/cli/command/auth/logout.go +++ b/internal/pkg/cli/command/auth/logout.go @@ -1,14 +1,23 @@ package auth import ( + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/configuration/secrets" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) +type logoutCmdOptions struct { + json bool +} + func NewLogoutCmd() *cobra.Command { + options := logoutCmdOptions{} + cmd := &cobra.Command{ Use: "logout", Short: "Clears locally stored API keys (managed and default), service account details (client ID and secret), and user login token. Also clears organization and project target context.", @@ -18,12 +27,21 @@ func NewLogoutCmd() *cobra.Command { GroupID: help.GROUP_AUTH.ID, Run: func(cmd *cobra.Command, args []string) { secrets.ConfigFile.Clear() - msg.SuccessMsg("API keys and user access tokens cleared.") - state.ConfigFile.Clear() + + if options.json { + fmt.Println(text.IndentJSON(struct { + Status string `json:"status"` + }{Status: "logged_out"})) + return + } + + msg.SuccessMsg("API keys and user access tokens cleared.") msg.SuccessMsg("State cleared.") }, } + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + return cmd } diff --git a/internal/pkg/cli/command/auth/whoami.go b/internal/pkg/cli/command/auth/whoami.go index 527a9c43..f30cde1b 100644 --- a/internal/pkg/cli/command/auth/whoami.go +++ b/internal/pkg/cli/command/auth/whoami.go @@ -1,15 +1,24 @@ package auth import ( + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/oauth" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) +type whoamiCmdOptions struct { + json bool +} + func NewWhoAmICmd() *cobra.Command { + options := whoamiCmdOptions{} + cmd := &cobra.Command{ Use: "whoami", Short: "See the currently logged in user", @@ -18,7 +27,6 @@ func NewWhoAmICmd() *cobra.Command { `), GroupID: help.GROUP_AUTH.ID, Run: func(cmd *cobra.Command, args []string) { - token, err := oauth.Token(cmd.Context()) if err != nil { msg.FailMsg("Error retrieving oauth token: %s", err) @@ -36,9 +44,20 @@ func NewWhoAmICmd() *cobra.Command { exit.Error(err, "An auth token was fetched but an error occurred while parsing the token's claims") return } + + if options.json { + fmt.Println(text.IndentJSON(struct { + Email string `json:"email"` + OrgId string `json:"organization_id"` + }{Email: claims.Email, OrgId: claims.OrgId})) + return + } + msg.InfoMsg("Logged in as " + style.Emphasis(claims.Email)) }, } + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON") + return cmd } diff --git a/internal/pkg/cli/command/backup/backup_test.go b/internal/pkg/cli/command/backup/backup_test.go index 428de263..bf8c2be9 100644 --- a/internal/pkg/cli/command/backup/backup_test.go +++ b/internal/pkg/cli/command/backup/backup_test.go @@ -7,7 +7,6 @@ import ( "github.com/pinecone-io/cli/internal/pkg/cli/testutils" "github.com/pinecone-io/go-pinecone/v5/pinecone" - "github.com/stretchr/testify/assert" ) type mockBackupService struct { @@ -60,109 +59,3 @@ func TestMain(m *testing.M) { reset() os.Exit(code) } - -func Test_runCreateBackupCmd_RequiresIndexName(t *testing.T) { - svc := &mockBackupService{} - opts := createBackupCmdOptions{} - - err := runCreateBackupCmd(context.Background(), svc, opts) - - assert.Error(t, err) - assert.Nil(t, svc.lastCreateBackupReq) -} - -func Test_runCreateBackupCmd_PopulatesRequest(t *testing.T) { - svc := &mockBackupService{ - createBackupResp: &pinecone.Backup{BackupId: "b1"}, - } - opts := createBackupCmdOptions{ - indexName: "idx", - description: "desc", - name: "name", - } - - err := runCreateBackupCmd(context.Background(), svc, opts) - - if assert.NoError(t, err) { - if assert.NotNil(t, svc.lastCreateBackupReq) { - assert.Equal(t, "idx", svc.lastCreateBackupReq.IndexName) - if assert.NotNil(t, svc.lastCreateBackupReq.Description) { - assert.Equal(t, "desc", *svc.lastCreateBackupReq.Description) - } - if assert.NotNil(t, svc.lastCreateBackupReq.Name) { - assert.Equal(t, "name", *svc.lastCreateBackupReq.Name) - } - } - } -} - -func Test_runDescribeBackupCmd_RequiresBackupId(t *testing.T) { - svc := &mockBackupService{} - opts := describeBackupCmdOptions{} - - err := runDescribeBackupCmd(context.Background(), svc, opts) - - assert.Error(t, err) - assert.Empty(t, svc.lastDescribeBackupId) -} - -func Test_runDescribeBackupCmd_Succeeds(t *testing.T) { - svc := &mockBackupService{ - describeBackupResp: &pinecone.Backup{BackupId: "b1"}, - } - opts := describeBackupCmdOptions{ - backupId: "b1", - } - - err := runDescribeBackupCmd(context.Background(), svc, opts) - - assert.NoError(t, err) - assert.Equal(t, "b1", svc.lastDescribeBackupId) -} - -func Test_runListBackupsCmd_PopulatesParams(t *testing.T) { - svc := &mockBackupService{ - listBackupsResp: &pinecone.BackupList{}, - } - opts := listBackupsCmdOptions{ - indexName: "idx", - limit: 5, - paginationToken: "next", - } - - err := runListBackupsCmd(context.Background(), svc, opts) - - if assert.NoError(t, err) { - if assert.NotNil(t, svc.lastListBackupsParams) { - if assert.NotNil(t, svc.lastListBackupsParams.IndexName) { - assert.Equal(t, "idx", *svc.lastListBackupsParams.IndexName) - } - if assert.NotNil(t, svc.lastListBackupsParams.Limit) { - assert.Equal(t, 5, *svc.lastListBackupsParams.Limit) - } - if assert.NotNil(t, svc.lastListBackupsParams.PaginationToken) { - assert.Equal(t, "next", *svc.lastListBackupsParams.PaginationToken) - } - } - } -} - -func Test_runDeleteBackupCmd_RequiresBackupId(t *testing.T) { - svc := &mockBackupService{} - opts := deleteBackupCmdOptions{} - - err := runDeleteBackupCmd(context.Background(), svc, opts) - - assert.Error(t, err) - assert.Empty(t, svc.lastDeleteBackupId) -} - -func Test_runDeleteBackupCmd_Succeeds(t *testing.T) { - svc := &mockBackupService{} - opts := deleteBackupCmdOptions{backupId: "b1"} - - err := runDeleteBackupCmd(context.Background(), svc, opts) - - assert.NoError(t, err) - assert.Equal(t, "b1", svc.lastDeleteBackupId) -} diff --git a/internal/pkg/cli/command/backup/create.go b/internal/pkg/cli/command/backup/create.go index c2dfa833..7253d6f5 100644 --- a/internal/pkg/cli/command/backup/create.go +++ b/internal/pkg/cli/command/backup/create.go @@ -2,6 +2,7 @@ package backup import ( "context" + "fmt" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" @@ -96,8 +97,7 @@ func runCreateBackupCmd(ctx context.Context, svc BackupService, options createBa } if options.json { - json := text.IndentJSON(backup) - pcio.Println(json) + fmt.Println(text.IndentJSON(backup)) } else { msg.SuccessMsg("Backup %s created.\n", styleEmphasisId(backup)) presenters.PrintBackupTable(backup) diff --git a/internal/pkg/cli/command/backup/create_test.go b/internal/pkg/cli/command/backup/create_test.go new file mode 100644 index 00000000..19362782 --- /dev/null +++ b/internal/pkg/cli/command/backup/create_test.go @@ -0,0 +1,62 @@ +package backup + +import ( + "context" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/stretchr/testify/assert" +) + +func Test_runCreateBackupCmd_RequiresIndexName(t *testing.T) { + svc := &mockBackupService{} + opts := createBackupCmdOptions{} + + err := runCreateBackupCmd(context.Background(), svc, opts) + + assert.Error(t, err) + assert.Nil(t, svc.lastCreateBackupReq) +} + +func Test_runCreateBackupCmd_Succeeds(t *testing.T) { + svc := &mockBackupService{ + createBackupResp: &pinecone.Backup{BackupId: "b1"}, + } + opts := createBackupCmdOptions{ + indexName: "idx", + description: "desc", + name: "name", + } + + err := runCreateBackupCmd(context.Background(), svc, opts) + + if assert.NoError(t, err) { + if assert.NotNil(t, svc.lastCreateBackupReq) { + assert.Equal(t, "idx", svc.lastCreateBackupReq.IndexName) + if assert.NotNil(t, svc.lastCreateBackupReq.Description) { + assert.Equal(t, "desc", *svc.lastCreateBackupReq.Description) + } + if assert.NotNil(t, svc.lastCreateBackupReq.Name) { + assert.Equal(t, "name", *svc.lastCreateBackupReq.Name) + } + } + } +} + +func Test_runCreateBackupCmd_SucceedsJSON(t *testing.T) { + svc := &mockBackupService{ + createBackupResp: &pinecone.Backup{BackupId: "b1"}, + } + opts := createBackupCmdOptions{ + indexName: "idx", + json: true, + } + + out := testutils.CaptureStdout(t, func() { + err := runCreateBackupCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.Contains(t, out, `"b1"`) +} diff --git a/internal/pkg/cli/command/backup/delete.go b/internal/pkg/cli/command/backup/delete.go index 380d0aa6..f82e5955 100644 --- a/internal/pkg/cli/command/backup/delete.go +++ b/internal/pkg/cli/command/backup/delete.go @@ -2,19 +2,21 @@ package backup import ( "context" + "fmt" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" - "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteBackupCmdOptions struct { backupId string + json bool } func NewDeleteBackupCmd() *cobra.Command { @@ -40,19 +42,28 @@ func NewDeleteBackupCmd() *cobra.Command { cmd.Flags().StringVarP(&options.backupId, "id", "i", "", "ID of the backup to delete") _ = cmd.MarkFlagRequired("id") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") return cmd } func runDeleteBackupCmd(ctx context.Context, svc BackupService, options deleteBackupCmdOptions) error { if strings.TrimSpace(options.backupId) == "" { - return pcio.Errorf("--id is required") + return fmt.Errorf("--id is required") } if err := svc.DeleteBackup(ctx, options.backupId); err != nil { return err } + if options.json { + fmt.Println(text.IndentJSON(struct { + Deleted bool `json:"deleted"` + Id string `json:"id"` + }{Deleted: true, Id: options.backupId})) + return nil + } + msg.SuccessMsg("Backup %s deleted.\n", style.Emphasis(options.backupId)) return nil } diff --git a/internal/pkg/cli/command/backup/delete_test.go b/internal/pkg/cli/command/backup/delete_test.go new file mode 100644 index 00000000..71aa2b29 --- /dev/null +++ b/internal/pkg/cli/command/backup/delete_test.go @@ -0,0 +1,41 @@ +package backup + +import ( + "context" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/stretchr/testify/assert" +) + +func Test_runDeleteBackupCmd_RequiresBackupId(t *testing.T) { + svc := &mockBackupService{} + opts := deleteBackupCmdOptions{} + + err := runDeleteBackupCmd(context.Background(), svc, opts) + + assert.Error(t, err) + assert.Empty(t, svc.lastDeleteBackupId) +} + +func Test_runDeleteBackupCmd_Succeeds(t *testing.T) { + svc := &mockBackupService{} + opts := deleteBackupCmdOptions{backupId: "b1"} + + err := runDeleteBackupCmd(context.Background(), svc, opts) + + assert.NoError(t, err) + assert.Equal(t, "b1", svc.lastDeleteBackupId) +} + +func Test_runDeleteBackupCmd_SucceedsJSON(t *testing.T) { + svc := &mockBackupService{} + opts := deleteBackupCmdOptions{backupId: "b1", json: true} + + out := testutils.CaptureStdout(t, func() { + err := runDeleteBackupCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.JSONEq(t, `{"deleted":true,"id":"b1"}`, out) +} diff --git a/internal/pkg/cli/command/backup/describe.go b/internal/pkg/cli/command/backup/describe.go index d33791ca..72f3e0cd 100644 --- a/internal/pkg/cli/command/backup/describe.go +++ b/internal/pkg/cli/command/backup/describe.go @@ -2,6 +2,7 @@ package backup import ( "context" + "fmt" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" @@ -58,8 +59,7 @@ func runDescribeBackupCmd(ctx context.Context, svc BackupService, options descri } if options.json { - json := text.IndentJSON(resp) - pcio.Println(json) + fmt.Println(text.IndentJSON(resp)) } else { presenters.PrintBackupTable(resp) } diff --git a/internal/pkg/cli/command/backup/describe_test.go b/internal/pkg/cli/command/backup/describe_test.go new file mode 100644 index 00000000..5d62916f --- /dev/null +++ b/internal/pkg/cli/command/backup/describe_test.go @@ -0,0 +1,46 @@ +package backup + +import ( + "context" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/stretchr/testify/assert" +) + +func Test_runDescribeBackupCmd_RequiresBackupId(t *testing.T) { + svc := &mockBackupService{} + opts := describeBackupCmdOptions{} + + err := runDescribeBackupCmd(context.Background(), svc, opts) + + assert.Error(t, err) + assert.Empty(t, svc.lastDescribeBackupId) +} + +func Test_runDescribeBackupCmd_Succeeds(t *testing.T) { + svc := &mockBackupService{ + describeBackupResp: &pinecone.Backup{BackupId: "b1"}, + } + opts := describeBackupCmdOptions{backupId: "b1"} + + err := runDescribeBackupCmd(context.Background(), svc, opts) + + assert.NoError(t, err) + assert.Equal(t, "b1", svc.lastDescribeBackupId) +} + +func Test_runDescribeBackupCmd_SucceedsJSON(t *testing.T) { + svc := &mockBackupService{ + describeBackupResp: &pinecone.Backup{BackupId: "b1"}, + } + opts := describeBackupCmdOptions{backupId: "b1", json: true} + + out := testutils.CaptureStdout(t, func() { + err := runDescribeBackupCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.Contains(t, out, `"b1"`) +} diff --git a/internal/pkg/cli/command/backup/list.go b/internal/pkg/cli/command/backup/list.go index 24b156ff..9d096f1b 100644 --- a/internal/pkg/cli/command/backup/list.go +++ b/internal/pkg/cli/command/backup/list.go @@ -6,7 +6,7 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" - "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "fmt" "github.com/pinecone-io/cli/internal/pkg/utils/presenters" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/text" @@ -83,8 +83,7 @@ func runListBackupsCmd(ctx context.Context, svc BackupService, options listBacku } if options.json { - json := text.IndentJSON(resp) - pcio.Println(json) + fmt.Println(text.IndentJSON(resp)) } else { presenters.PrintBackupList(resp) } diff --git a/internal/pkg/cli/command/backup/list_test.go b/internal/pkg/cli/command/backup/list_test.go new file mode 100644 index 00000000..5a9f4e23 --- /dev/null +++ b/internal/pkg/cli/command/backup/list_test.go @@ -0,0 +1,53 @@ +package backup + +import ( + "context" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/pinecone-io/go-pinecone/v5/pinecone" + "github.com/stretchr/testify/assert" +) + +func Test_runListBackupsCmd_PopulatesParams(t *testing.T) { + svc := &mockBackupService{ + listBackupsResp: &pinecone.BackupList{}, + } + opts := listBackupsCmdOptions{ + indexName: "idx", + limit: 5, + paginationToken: "next", + } + + err := runListBackupsCmd(context.Background(), svc, opts) + + if assert.NoError(t, err) { + if assert.NotNil(t, svc.lastListBackupsParams) { + if assert.NotNil(t, svc.lastListBackupsParams.IndexName) { + assert.Equal(t, "idx", *svc.lastListBackupsParams.IndexName) + } + if assert.NotNil(t, svc.lastListBackupsParams.Limit) { + assert.Equal(t, 5, *svc.lastListBackupsParams.Limit) + } + if assert.NotNil(t, svc.lastListBackupsParams.PaginationToken) { + assert.Equal(t, "next", *svc.lastListBackupsParams.PaginationToken) + } + } + } +} + +func Test_runListBackupsCmd_SucceedsJSON(t *testing.T) { + svc := &mockBackupService{ + listBackupsResp: &pinecone.BackupList{ + Data: []*pinecone.Backup{{BackupId: "b1"}}, + }, + } + opts := listBackupsCmdOptions{json: true} + + out := testutils.CaptureStdout(t, func() { + err := runListBackupsCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.Contains(t, out, `"b1"`) +} diff --git a/internal/pkg/cli/command/collection/delete.go b/internal/pkg/cli/command/collection/delete.go index 9dea8da0..9da0d06a 100644 --- a/internal/pkg/cli/command/collection/delete.go +++ b/internal/pkg/cli/command/collection/delete.go @@ -1,16 +1,25 @@ package collection import ( + "context" + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteCollectionCmdOptions struct { name string + json bool +} + +type deleteCollectionService interface { + DeleteCollection(ctx context.Context, name string) error } func NewDeleteCollectionCmd() *cobra.Command { @@ -26,19 +35,36 @@ func NewDeleteCollectionCmd() *cobra.Command { ctx := cmd.Context() pc := sdk.NewPineconeClient(ctx) - err := pc.DeleteCollection(ctx, options.name) + err := runDeleteCollectionCmd(ctx, pc, options) if err != nil { msg.FailMsg("Failed to delete collection %s: %s\n", style.Emphasis(options.name), err) exit.Error(err, "Failed to delete collection") } - - msg.SuccessMsg("Collection %s deleted.\n", style.Emphasis(options.name)) }, } // required flags cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of collection to delete") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + _ = cmd.MarkFlagRequired("name") return cmd } + +func runDeleteCollectionCmd(ctx context.Context, svc deleteCollectionService, options deleteCollectionCmdOptions) error { + if err := svc.DeleteCollection(ctx, options.name); err != nil { + return err + } + + if options.json { + fmt.Println(text.IndentJSON(struct { + Deleted bool `json:"deleted"` + Name string `json:"name"` + }{Deleted: true, Name: options.name})) + return nil + } + + msg.SuccessMsg("Collection %s deleted.\n", style.Emphasis(options.name)) + return nil +} diff --git a/internal/pkg/cli/command/collection/delete_test.go b/internal/pkg/cli/command/collection/delete_test.go new file mode 100644 index 00000000..e3e51184 --- /dev/null +++ b/internal/pkg/cli/command/collection/delete_test.go @@ -0,0 +1,59 @@ +package collection + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/stretchr/testify/assert" +) + +type mockDeleteCollectionService struct { + lastDeleteArg string + deleteErr error +} + +func (m *mockDeleteCollectionService) DeleteCollection(ctx context.Context, name string) error { + m.lastDeleteArg = name + return m.deleteErr +} + +func TestMain(m *testing.M) { + reset := testutils.SilenceOutput() + code := m.Run() + reset() + os.Exit(code) +} + +func Test_runDeleteCollectionCmd_Succeeds(t *testing.T) { + svc := &mockDeleteCollectionService{} + opts := deleteCollectionCmdOptions{name: "my-collection"} + + err := runDeleteCollectionCmd(context.Background(), svc, opts) + + assert.NoError(t, err) + assert.Equal(t, "my-collection", svc.lastDeleteArg) +} + +func Test_runDeleteCollectionCmd_SucceedsJSON(t *testing.T) { + svc := &mockDeleteCollectionService{} + opts := deleteCollectionCmdOptions{name: "my-collection", json: true} + + out := testutils.CaptureStdout(t, func() { + err := runDeleteCollectionCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.JSONEq(t, `{"deleted":true,"name":"my-collection"}`, out) +} + +func Test_runDeleteCollectionCmd_PropagatesError(t *testing.T) { + svc := &mockDeleteCollectionService{deleteErr: errors.New("service error")} + opts := deleteCollectionCmdOptions{name: "my-collection"} + + err := runDeleteCollectionCmd(context.Background(), svc, opts) + + assert.Error(t, err) +} diff --git a/internal/pkg/cli/command/index/delete.go b/internal/pkg/cli/command/index/delete.go index 902da3bd..b6a83219 100644 --- a/internal/pkg/cli/command/index/delete.go +++ b/internal/pkg/cli/command/index/delete.go @@ -1,6 +1,8 @@ package index import ( + "context" + "fmt" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" @@ -8,11 +10,17 @@ import ( "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteCmdOptions struct { name string + json bool +} + +type deleteIndexService interface { + DeleteIndex(ctx context.Context, name string) error } func NewDeleteCmd() *cobra.Command { @@ -28,7 +36,7 @@ func NewDeleteCmd() *cobra.Command { ctx := cmd.Context() pc := sdk.NewPineconeClient(ctx) - err := pc.DeleteIndex(ctx, options.name) + err := runDeleteIndexCmd(ctx, pc, options) if err != nil { if strings.Contains(err.Error(), "not found") { msg.FailMsg("The index %s does not exist\n", style.Emphasis(options.name)) @@ -38,14 +46,30 @@ func NewDeleteCmd() *cobra.Command { exit.Errorf(err, "Failed to delete index %s", style.Emphasis(options.name)) } } - - msg.SuccessMsg("Index %s deleted.\n", style.Emphasis(options.name)) }, } // required flags cmd.Flags().StringVarP(&options.name, "name", "n", "", "name of index to delete") _ = cmd.MarkFlagRequired("name") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") return cmd } + +func runDeleteIndexCmd(ctx context.Context, svc deleteIndexService, options deleteCmdOptions) error { + if err := svc.DeleteIndex(ctx, options.name); err != nil { + return err + } + + if options.json { + fmt.Println(text.IndentJSON(struct { + Deleted bool `json:"deleted"` + Name string `json:"name"` + }{Deleted: true, Name: options.name})) + return nil + } + + msg.SuccessMsg("Index %s deleted.\n", style.Emphasis(options.name)) + return nil +} diff --git a/internal/pkg/cli/command/index/delete_test.go b/internal/pkg/cli/command/index/delete_test.go new file mode 100644 index 00000000..18b27960 --- /dev/null +++ b/internal/pkg/cli/command/index/delete_test.go @@ -0,0 +1,52 @@ +package index + +import ( + "context" + "errors" + "testing" + + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" + "github.com/stretchr/testify/assert" +) + +type mockDeleteIndexService struct { + lastDeleteArg string + deleteErr error +} + +func (m *mockDeleteIndexService) DeleteIndex(ctx context.Context, name string) error { + m.lastDeleteArg = name + return m.deleteErr +} + +func Test_runDeleteIndexCmd_Succeeds(t *testing.T) { + svc := &mockDeleteIndexService{} + opts := deleteCmdOptions{name: "my-index"} + + err := runDeleteIndexCmd(context.Background(), svc, opts) + + assert.NoError(t, err) + assert.Equal(t, "my-index", svc.lastDeleteArg) +} + +func Test_runDeleteIndexCmd_SucceedsJSON(t *testing.T) { + svc := &mockDeleteIndexService{} + opts := deleteCmdOptions{name: "my-index", json: true} + + out := testutils.CaptureStdout(t, func() { + err := runDeleteIndexCmd(context.Background(), svc, opts) + assert.NoError(t, err) + }) + + assert.JSONEq(t, `{"deleted":true,"name":"my-index"}`, out) +} + +func Test_runDeleteIndexCmd_PropagatesError(t *testing.T) { + svc := &mockDeleteIndexService{deleteErr: errors.New("not found")} + opts := deleteCmdOptions{name: "missing"} + + err := runDeleteIndexCmd(context.Background(), svc, opts) + + assert.Error(t, err) + assert.Equal(t, "missing", svc.lastDeleteArg) +} diff --git a/internal/pkg/cli/command/index/namespace/delete.go b/internal/pkg/cli/command/index/namespace/delete.go index 36cd56e3..455790fd 100644 --- a/internal/pkg/cli/command/index/namespace/delete.go +++ b/internal/pkg/cli/command/index/namespace/delete.go @@ -2,19 +2,21 @@ package namespace import ( "context" + "fmt" "strings" "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" - "github.com/pinecone-io/cli/internal/pkg/utils/pcio" "github.com/pinecone-io/cli/internal/pkg/utils/sdk" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) type deleteNamespaceCmdOptions struct { indexName string name string + json bool } func NewDeleteNamespaceCmd() *cobra.Command { @@ -60,20 +62,29 @@ func NewDeleteNamespaceCmd() *cobra.Command { cmd.Flags().StringVar(&options.name, "name", "", "name of the namespace to delete") _ = cmd.MarkFlagRequired("index-name") _ = cmd.MarkFlagRequired("name") + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") return cmd } func runDeleteNamespaceCmd(ctx context.Context, ic NamespaceService, options deleteNamespaceCmdOptions) error { if strings.TrimSpace(options.name) == "" { - return pcio.Errorf("--name is required") + return fmt.Errorf("--name is required") } - err := ic.DeleteNamespace(ctx, options.name) - if err != nil { + if err := ic.DeleteNamespace(ctx, options.name); err != nil { return err } - msg.SuccessMsg("Namespace %s deleted successfully.", options.name) + if options.json { + fmt.Println(text.IndentJSON(struct { + Deleted bool `json:"deleted"` + Namespace string `json:"namespace"` + Index string `json:"index"` + }{Deleted: true, Namespace: options.name, Index: options.indexName})) + return nil + } + + msg.SuccessMsg("Namespace %s deleted successfully.", options.name) return nil } diff --git a/internal/pkg/cli/command/index/namespace/delete_test.go b/internal/pkg/cli/command/index/namespace/delete_test.go index d9a7dd8d..f3052bb6 100644 --- a/internal/pkg/cli/command/index/namespace/delete_test.go +++ b/internal/pkg/cli/command/index/namespace/delete_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/pinecone-io/cli/internal/pkg/cli/testutils" "github.com/stretchr/testify/assert" ) @@ -31,3 +32,19 @@ func Test_runDeleteNamespaceCmd_Succeeds(t *testing.T) { assert.NoError(t, err) assert.Equal(t, options.name, svc.lastDeleteArg) } + +func Test_runDeleteNamespaceCmd_SucceedsJSON(t *testing.T) { + svc := &mockNamespaceService{} + options := deleteNamespaceCmdOptions{ + name: "tenant-a", + indexName: "my-index", + json: true, + } + + out := testutils.CaptureStdout(t, func() { + err := runDeleteNamespaceCmd(context.Background(), svc, options) + assert.NoError(t, err) + }) + + assert.JSONEq(t, `{"deleted":true,"namespace":"tenant-a","index":"my-index"}`, out) +} diff --git a/internal/pkg/cli/command/logout/logout.go b/internal/pkg/cli/command/logout/logout.go index c685d153..d9d392cd 100644 --- a/internal/pkg/cli/command/logout/logout.go +++ b/internal/pkg/cli/command/logout/logout.go @@ -1,14 +1,23 @@ package logout import ( + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/configuration/secrets" "github.com/pinecone-io/cli/internal/pkg/utils/configuration/state" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) +type logoutCmdOptions struct { + json bool +} + func NewLogoutCmd() *cobra.Command { + options := logoutCmdOptions{} + cmd := &cobra.Command{ Use: "logout", Short: "Clears locally stored API keys (managed and default), service account details (client ID and secret), and user login token. Also clears organization and project target context. This command is an alias for 'pc auth logout'.", @@ -18,12 +27,21 @@ func NewLogoutCmd() *cobra.Command { GroupID: help.GROUP_AUTH.ID, Run: func(cmd *cobra.Command, args []string) { secrets.ConfigFile.Clear() - msg.SuccessMsg("API keys and user access tokens cleared.") - state.ConfigFile.Clear() + + if options.json { + fmt.Println(text.IndentJSON(struct { + Status string `json:"status"` + }{Status: "logged_out"})) + return + } + + msg.SuccessMsg("API keys and user access tokens cleared.") msg.SuccessMsg("State cleared.") }, } + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output result as JSON") + return cmd } diff --git a/internal/pkg/cli/command/version/version.go b/internal/pkg/cli/command/version/version.go index b45a00a0..3bcbd9b4 100644 --- a/internal/pkg/cli/command/version/version.go +++ b/internal/pkg/cli/command/version/version.go @@ -1,13 +1,21 @@ package version import ( + "fmt" + "github.com/pinecone-io/cli/internal/build" "github.com/pinecone-io/cli/internal/pkg/utils/help" - "github.com/pinecone-io/cli/internal/pkg/utils/pcio" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) +type versionCmdOptions struct { + json bool +} + func NewVersionCmd() *cobra.Command { + options := versionCmdOptions{} + cmd := &cobra.Command{ Use: "version", Short: "See version information for the CLI", @@ -15,11 +23,22 @@ func NewVersionCmd() *cobra.Command { pc version `), Run: func(cmd *cobra.Command, args []string) { - pcio.Printf("Version: %s\n", build.Version) - pcio.Printf("SHA: %s\n", build.Commit) - pcio.Printf("Built: %s\n", build.Date) + if options.json { + fmt.Println(text.IndentJSON(struct { + Version string `json:"version"` + Sha string `json:"sha"` + Built string `json:"built"` + }{Version: build.Version, Sha: build.Commit, Built: build.Date})) + return + } + + fmt.Printf("Version: %s\n", build.Version) + fmt.Printf("SHA: %s\n", build.Commit) + fmt.Printf("Built: %s\n", build.Date) }, } + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON") + return cmd } diff --git a/internal/pkg/cli/command/whoami/whoami.go b/internal/pkg/cli/command/whoami/whoami.go index 11b45531..9359908b 100644 --- a/internal/pkg/cli/command/whoami/whoami.go +++ b/internal/pkg/cli/command/whoami/whoami.go @@ -1,15 +1,24 @@ package whoami import ( + "fmt" + "github.com/pinecone-io/cli/internal/pkg/utils/exit" "github.com/pinecone-io/cli/internal/pkg/utils/help" "github.com/pinecone-io/cli/internal/pkg/utils/msg" "github.com/pinecone-io/cli/internal/pkg/utils/oauth" "github.com/pinecone-io/cli/internal/pkg/utils/style" + "github.com/pinecone-io/cli/internal/pkg/utils/text" "github.com/spf13/cobra" ) +type whoamiCmdOptions struct { + json bool +} + func NewWhoAmICmd() *cobra.Command { + options := whoamiCmdOptions{} + cmd := &cobra.Command{ Use: "whoami", Short: "See the currently logged in user", @@ -18,7 +27,6 @@ func NewWhoAmICmd() *cobra.Command { `), GroupID: help.GROUP_AUTH.ID, Run: func(cmd *cobra.Command, args []string) { - token, err := oauth.Token(cmd.Context()) if err != nil { msg.FailMsg("Error retrieving oauth token: %s", err) @@ -34,9 +42,20 @@ func NewWhoAmICmd() *cobra.Command { msg.FailMsg("An auth token was fetched but an error occurred while parsing the token's claims: %s", err) exit.Error(err, "An auth token was fetched but an error occurred while parsing the token's claims") } + + if options.json { + fmt.Println(text.IndentJSON(struct { + Email string `json:"email"` + OrgId string `json:"organization_id"` + }{Email: claims.Email, OrgId: claims.OrgId})) + return + } + msg.InfoMsg("Logged in as " + style.Emphasis(claims.Email)) }, } + cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON") + return cmd } diff --git a/internal/pkg/cli/testutils/test_utils.go b/internal/pkg/cli/testutils/test_utils.go index 74042d28..b4e98fa5 100644 --- a/internal/pkg/cli/testutils/test_utils.go +++ b/internal/pkg/cli/testutils/test_utils.go @@ -1,6 +1,12 @@ package testutils import ( + "bytes" + "io" + "os" + "strings" + "testing" + "github.com/pinecone-io/cli/internal/pkg/utils/pcio" ) @@ -9,3 +15,27 @@ func SilenceOutput() func() { pcio.SetQuiet(true) return func() { pcio.SetQuiet(false) } } + +// CaptureStdout redirects os.Stdout to a pipe for the duration of f, +// returning everything written to stdout as a trimmed string. +func CaptureStdout(t *testing.T, f func()) string { + t.Helper() + + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("CaptureStdout: os.Pipe: %v", err) + } + + prev := os.Stdout + os.Stdout = w + defer func() { os.Stdout = prev }() + + f() + w.Close() + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + t.Fatalf("CaptureStdout: reading pipe: %v", err) + } + return strings.TrimSpace(buf.String()) +}