Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/pkg/cli/command/index/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package index
import (
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/backup"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/collection"
importcmd "github.com/pinecone-io/cli/internal/pkg/cli/command/index/import"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/namespace"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/record"
"github.com/pinecone-io/cli/internal/pkg/cli/command/index/restore"
Expand Down Expand Up @@ -56,6 +57,7 @@ func NewIndexCmd() *cobra.Command {
cmd.AddCommand(backup.NewBackupCmd())
cmd.AddCommand(restore.NewRestoreCmd())
cmd.AddCommand(collection.NewCollectionCmd())
cmd.AddCommand(importcmd.NewImportCmd())

return cmd
}
82 changes: 82 additions & 0 deletions internal/pkg/cli/command/index/import/cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package importcmd

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/sdk"
"github.com/pinecone-io/cli/internal/pkg/utils/style"
"github.com/pinecone-io/cli/internal/pkg/utils/text"
"github.com/spf13/cobra"
)

type cancelImportCmdOptions struct {
indexName string
importId string
json bool
}

// NewCancelImportCmd returns the "import cancel" subcommand.
func NewCancelImportCmd() *cobra.Command {
options := cancelImportCmdOptions{}

cmd := &cobra.Command{
Use: "cancel",
Short: "Cancel an in-progress import operation",
Long: help.Long(`
Cancel an import operation that is currently pending or in progress.
Already-completed or failed imports cannot be cancelled.
`),
Example: help.Examples(`
pc index import cancel --index-name my-index --id import-123
`),
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
pc := sdk.NewPineconeClient(ctx)
ic, err := sdk.NewIndexConnection(ctx, pc, options.indexName, "")
if err != nil {
msg.FailJSON(options.json, "Failed to connect to index: %s\n", err)
exit.Error(err, "Failed to connect to index")
}

err = runCancelImportCmd(ctx, ic, options)
if err != nil {
msg.FailJSON(options.json, "Failed to cancel import: %s\n", err)
exit.Error(err, "Failed to cancel import")
}
},
}

cmd.Flags().StringVarP(&options.indexName, "index-name", "n", "", "Name of the index the import belongs to")
cmd.Flags().StringVarP(&options.importId, "id", "i", "", "ID of the import to cancel")
cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON")
_ = cmd.MarkFlagRequired("index-name")
_ = cmd.MarkFlagRequired("id")

return cmd
}

func runCancelImportCmd(ctx context.Context, svc ImportService, options cancelImportCmdOptions) error {
if strings.TrimSpace(options.importId) == "" {
return fmt.Errorf("--id is required")
}

if err := svc.CancelImport(ctx, options.importId); err != nil {
return err
}

if options.json {
fmt.Println(text.IndentJSON(struct {
Cancelled bool `json:"cancelled"`
Id string `json:"id"`
}{Cancelled: true, Id: options.importId}))
return nil
}

msg.SuccessMsg("Import %s cancelled.\n", style.Emphasis(options.importId))
return nil
}
55 changes: 55 additions & 0 deletions internal/pkg/cli/command/index/import/cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package importcmd

import (
"context"
"errors"
"testing"

"github.com/pinecone-io/cli/internal/pkg/cli/testutils"
"github.com/stretchr/testify/assert"
)

func Test_runCancelImportCmd_RequiresID(t *testing.T) {
svc := &mockImportService{}
opts := cancelImportCmdOptions{}

err := runCancelImportCmd(context.Background(), svc, opts)

assert.Error(t, err)
assert.Empty(t, svc.lastCancelImportId)
}

func Test_runCancelImportCmd_Succeeds(t *testing.T) {
svc := &mockImportService{}
opts := cancelImportCmdOptions{importId: "import-1"}

err := runCancelImportCmd(context.Background(), svc, opts)

if assert.NoError(t, err) {
assert.Equal(t, "import-1", svc.lastCancelImportId)
}
}

func Test_runCancelImportCmd_SucceedsJSON(t *testing.T) {
svc := &mockImportService{}
opts := cancelImportCmdOptions{importId: "import-1", json: true}

out := testutils.CaptureStdout(t, func() {
err := runCancelImportCmd(context.Background(), svc, opts)
assert.NoError(t, err)
})

assert.Contains(t, out, `"import-1"`)
assert.Contains(t, out, `"cancelled": true`)
}

func Test_runCancelImportCmd_PropagatesError(t *testing.T) {
svc := &mockImportService{
cancelImportErr: errors.New("cancel failed"),
}
opts := cancelImportCmdOptions{importId: "import-1"}

err := runCancelImportCmd(context.Background(), svc, opts)

assert.EqualError(t, err, "cancel failed")
}
60 changes: 60 additions & 0 deletions internal/pkg/cli/command/index/import/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package importcmd

import (
"context"

"github.com/pinecone-io/cli/internal/pkg/utils/help"
"github.com/pinecone-io/go-pinecone/v5/pinecone"
"github.com/spf13/cobra"
)

// ImportService abstracts the Pinecone IndexConnection for unit testing across import commands.
type ImportService interface {
StartImport(ctx context.Context, uri string, integrationId *string, errorMode *string) (*pinecone.StartImportResponse, error)
DescribeImport(ctx context.Context, id string) (*pinecone.Import, error)
ListImports(ctx context.Context, limit *int32, paginationToken *string) (*pinecone.ListImportsResponse, error)
CancelImport(ctx context.Context, id string) error
}

var importHelp = help.Long(`
Manage imports for a serverless index. An import loads vector data from
a storage provider (S3, GCS, or Azure) directly into an index without requiring you to
push records through the upsert API. For secure data sources, you can configure a
storage integration through the Pinecone console.

Use these commands to start, describe, list, and cancel import operations.

Docs:
Import data: https://docs.pinecone.io/guides/index-data/import-data
Storage integrations: https://docs.pinecone.io/guides/operations/integrations/manage-storage-integrations
`)

// NewImportCmd returns the parent "import" command with all subcommands attached.
func NewImportCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "import",
Short: "Manage imports for a serverless index",
Long: importHelp,
GroupID: help.GROUP_INDEX_MANAGEMENT.ID,
Example: help.Examples(`
# Start an import from an S3 URI
pc index import start --index-name my-index --uri s3://my-bucket/data/

# List imports for an index
pc index import list --index-name my-index

# Describe a specific import
pc index import describe --index-name my-index --id import-123

# Cancel an in-progress import
pc index import cancel --index-name my-index --id import-123
`),
}

cmd.AddCommand(NewStartImportCmd())
cmd.AddCommand(NewDescribeImportCmd())
cmd.AddCommand(NewListImportsCmd())
cmd.AddCommand(NewCancelImportCmd())

return cmd
}
80 changes: 80 additions & 0 deletions internal/pkg/cli/command/index/import/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package importcmd

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/presenters"
"github.com/pinecone-io/cli/internal/pkg/utils/sdk"
"github.com/pinecone-io/cli/internal/pkg/utils/text"
"github.com/spf13/cobra"
)

type describeImportCmdOptions struct {
indexName string
importId string
json bool
}

// NewDescribeImportCmd returns the "import describe" subcommand.
func NewDescribeImportCmd() *cobra.Command {
options := describeImportCmdOptions{}

cmd := &cobra.Command{
Use: "describe",
Short: "Describe an import operation by ID",
Long: help.Long(`
Show the current status and details of an import operation, including
percent complete, records imported, and any error messages.
`),
Example: help.Examples(`
pc index import describe --index-name my-index --id import-123
`),
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
pc := sdk.NewPineconeClient(ctx)
ic, err := sdk.NewIndexConnection(ctx, pc, options.indexName, "")
if err != nil {
msg.FailJSON(options.json, "Failed to connect to index: %s\n", err)
exit.Error(err, "Failed to connect to index")
}

err = runDescribeImportCmd(ctx, ic, options)
if err != nil {
msg.FailJSON(options.json, "Failed to describe import: %s\n", err)
exit.Error(err, "Failed to describe import")
}
},
}

cmd.Flags().StringVarP(&options.indexName, "index-name", "n", "", "Name of the index the import belongs to")
cmd.Flags().StringVarP(&options.importId, "id", "i", "", "ID of the import to describe")
cmd.Flags().BoolVarP(&options.json, "json", "j", false, "Output as JSON")
_ = cmd.MarkFlagRequired("index-name")
_ = cmd.MarkFlagRequired("id")

return cmd
}

func runDescribeImportCmd(ctx context.Context, svc ImportService, options describeImportCmdOptions) error {
if strings.TrimSpace(options.importId) == "" {
return fmt.Errorf("--id is required")
}

resp, err := svc.DescribeImport(ctx, options.importId)
if err != nil {
return err
}

if options.json {
fmt.Println(text.IndentJSON(resp))
} else {
presenters.PrintImportTable(resp)
}

return nil
}
69 changes: 69 additions & 0 deletions internal/pkg/cli/command/index/import/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package importcmd

import (
"context"
"errors"
"testing"
"time"

"github.com/pinecone-io/cli/internal/pkg/cli/testutils"
"github.com/pinecone-io/go-pinecone/v5/pinecone"
"github.com/stretchr/testify/assert"
)

func Test_runDescribeImportCmd_RequiresID(t *testing.T) {
svc := &mockImportService{}
opts := describeImportCmdOptions{}

err := runDescribeImportCmd(context.Background(), svc, opts)

assert.Error(t, err)
assert.Empty(t, svc.lastDescribeImportId)
}

func Test_runDescribeImportCmd_Succeeds(t *testing.T) {
now := time.Now()
svc := &mockImportService{
describeImportResp: &pinecone.Import{
Id: "import-1",
Status: pinecone.InProgress,
Uri: "s3://my-bucket/data/",
CreatedAt: &now,
},
}
opts := describeImportCmdOptions{importId: "import-1"}

err := runDescribeImportCmd(context.Background(), svc, opts)

if assert.NoError(t, err) {
assert.Equal(t, "import-1", svc.lastDescribeImportId)
}
}

func Test_runDescribeImportCmd_SucceedsJSON(t *testing.T) {
svc := &mockImportService{
describeImportResp: &pinecone.Import{
Id: "import-1",
Status: pinecone.Completed,
},
}
opts := describeImportCmdOptions{importId: "import-1", json: true}

out := testutils.CaptureStdout(t, func() {
err := runDescribeImportCmd(context.Background(), svc, opts)
assert.NoError(t, err)
})

assert.Contains(t, out, `"import-1"`)
}

func Test_runDescribeImportCmd_PropagatesError(t *testing.T) {
svc := &mockImportService{
describeImportErr: errors.New("not found"),
}
opts := describeImportCmdOptions{importId: "import-1"}

err := runDescribeImportCmd(context.Background(), svc, opts)

assert.EqualError(t, err, "not found")
}
Loading
Loading