From ddd214add63e64e9ce530c43e07513067aaa55f1 Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Fri, 24 Jan 2025 13:25:02 +0100 Subject: [PATCH 1/2] Add option for ignoring non-existent documents --- client/go/internal/cli/cmd/document.go | 26 +++++++++++---------- client/go/internal/cli/cmd/document_test.go | 15 ++++++++++-- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/client/go/internal/cli/cmd/document.go b/client/go/internal/cli/cmd/document.go index 267f0e91c29..6ff85fd27e7 100644 --- a/client/go/internal/cli/cmd/document.go +++ b/client/go/internal/cli/cmd/document.go @@ -101,10 +101,10 @@ func sendOperation(op document.Operation, args []string, timeoutSecs int, waiter service.CurlWriter.InputFile = filename } result := client.Send(doc) - return printResult(cli, operationResult(false, doc, service, result), false) + return printResult(cli, operationResult(false, doc, service, result), false, false) } -func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool, cli *CLI, fieldSet string, headers []string) error { +func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool, cli *CLI, fieldSet string, headers []string, ignoreMissing bool) error { parsedIds := make([]document.Id, 0, len(ids)) for _, id := range ids { parsedId, err := document.ParseId(id) @@ -121,7 +121,7 @@ func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool for _, docId := range parsedIds { result := client.Get(docId, fieldSet) - if err := printResult(cli, operationResult(true, document.Document{Id: docId}, service, result), true); err != nil { + if err := printResult(cli, operationResult(true, document.Document{Id: docId}, service, result), true, ignoreMissing); err != nil { return err } } @@ -266,7 +266,7 @@ $ vespa document remove id:mynamespace:music::a-head-full-of-dreams`, } doc := document.Document{Id: id, Operation: document.OperationRemove} result := client.Send(doc) - return printResult(cli, operationResult(false, doc, service, result), false) + return printResult(cli, operationResult(false, doc, service, result), false, false) } else { return sendOperation(document.OperationRemove, args, timeoutSecs, waiter, printCurl, cli, headers) } @@ -278,11 +278,12 @@ $ vespa document remove id:mynamespace:music::a-head-full-of-dreams`, func newDocumentGetCmd(cli *CLI) *cobra.Command { var ( - printCurl bool - timeoutSecs int - waitSecs int - fieldSet string - headers []string + printCurl bool + ignoreMissing bool + timeoutSecs int + waitSecs int + fieldSet string + headers []string ) cmd := &cobra.Command{ Use: "get id", @@ -293,10 +294,11 @@ func newDocumentGetCmd(cli *CLI) *cobra.Command { Example: `$ vespa document get id:mynamespace:music::a-head-full-of-dreams...`, RunE: func(cmd *cobra.Command, args []string) error { waiter := cli.waiter(time.Duration(waitSecs)*time.Second, cmd) - return readDocuments(args, timeoutSecs, waiter, printCurl, cli, fieldSet, headers) + return readDocuments(args, timeoutSecs, waiter, printCurl, cli, fieldSet, headers, ignoreMissing) }, } cmd.Flags().StringVar(&fieldSet, "field-set", "", "Fields to include when reading document") + cmd.Flags().BoolVar(&ignoreMissing, "ignore-missing", false, "Ignore failure when getting non-existent documents") addDocumentFlags(cli, cmd, &printCurl, &timeoutSecs, &waitSecs, &headers) return cmd } @@ -309,7 +311,7 @@ func documentService(cli *CLI, waiter *Waiter) (*vespa.Service, error) { return waiter.Service(target, cli.config.cluster()) } -func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess bool) error { +func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess, ignoreFailure bool) error { out := cli.Stdout if !result.Success { out = cli.Stderr @@ -332,7 +334,7 @@ func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess bool) er fmt.Fprintln(out, result.Payload) } - if !result.Success { + if !result.Success && !ignoreFailure { err := errHint(fmt.Errorf("document operation failed")) err.quiet = true return err diff --git a/client/go/internal/cli/cmd/document_test.go b/client/go/internal/cli/cmd/document_test.go index d521ec012c7..3b50b4a5eaa 100644 --- a/client/go/internal/cli/cmd/document_test.go +++ b/client/go/internal/cli/cmd/document_test.go @@ -7,7 +7,7 @@ package cmd import ( "bytes" "encoding/json" - "fmt" + "errors" "io" "os" "strconv" @@ -131,6 +131,17 @@ func TestDocumentGet(t *testing.T) { []string{"id:mynamespace:music::a-head-full-of-dreams"}, t) } +func TestDocumentGetIgnoreMissing(t *testing.T) { + client := &mock.HTTPClient{} + client.NextResponseString(404, "{\"message\":\"not found\"}") + cli, stdout, stderr := newTestCLI(t) + cli.httpClient = client + assert.Nil(t, cli.Run("document", "get", "-t", "http://127.0.0.1:8080", "--ignore-missing", + "id:mynamespace:music::no-such-doc-1", "id:mynamespace:music::a-head-full-of-dreams")) + assert.Equal(t, "Success: Read id:mynamespace:music::a-head-full-of-dreams\n", stdout.String()) + assert.Equal(t, "Error: Invalid document operation: Status 404\n{\n \"message\": \"not found\"\n}\n", stderr.String()) +} + func TestDocumentGetWithHeader(t *testing.T) { client := &mock.HTTPClient{} assertDocumentGet(client, []string{"document", "get", "--header", "X-Foo: Bar", "id:mynamespace:music::a-head-full-of-dreams"}, @@ -224,7 +235,7 @@ func assertDocumentGet(client *mock.HTTPClient, args []string, documentIds []str func assertDocumentTransportError(t *testing.T, errorMessage string) { client := &mock.HTTPClient{} - client.NextResponseError(fmt.Errorf("%s", errorMessage)) + client.NextResponseError(errors.New(errorMessage)) cli, _, stderr := newTestCLI(t) cli.httpClient = client assert.NotNil(t, cli.Run("-t", "http://127.0.0.1:8080", "document", "put", From bcd9bd2bf5cbf6789c3f6dc8c053b42f22eb71db Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Mon, 27 Jan 2025 10:15:52 +0100 Subject: [PATCH 2/2] Ignore only 404 --- client/go/internal/cli/cmd/document.go | 34 ++++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/client/go/internal/cli/cmd/document.go b/client/go/internal/cli/cmd/document.go index 6ff85fd27e7..f9ea4d85b9e 100644 --- a/client/go/internal/cli/cmd/document.go +++ b/client/go/internal/cli/cmd/document.go @@ -101,10 +101,10 @@ func sendOperation(op document.Operation, args []string, timeoutSecs int, waiter service.CurlWriter.InputFile = filename } result := client.Send(doc) - return printResult(cli, operationResult(false, doc, service, result), false, false) + return printResult(cli, operationResult(false, doc, service, result), false) } -func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool, cli *CLI, fieldSet string, headers []string, ignoreMissing bool) error { +func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool, cli *CLI, fieldSet string, headers []string, ignoreNotFound bool) error { parsedIds := make([]document.Id, 0, len(ids)) for _, id := range ids { parsedId, err := document.ParseId(id) @@ -121,8 +121,11 @@ func readDocuments(ids []string, timeoutSecs int, waiter *Waiter, printCurl bool for _, docId := range parsedIds { result := client.Get(docId, fieldSet) - if err := printResult(cli, operationResult(true, document.Document{Id: docId}, service, result), true, ignoreMissing); err != nil { - return err + if err := printResult(cli, operationResult(true, document.Document{Id: docId}, service, result), true); err != nil { + ignoreErr := ignoreNotFound && result.HTTPStatus == 404 + if !ignoreErr { + return err + } } } @@ -266,7 +269,7 @@ $ vespa document remove id:mynamespace:music::a-head-full-of-dreams`, } doc := document.Document{Id: id, Operation: document.OperationRemove} result := client.Send(doc) - return printResult(cli, operationResult(false, doc, service, result), false, false) + return printResult(cli, operationResult(false, doc, service, result), false) } else { return sendOperation(document.OperationRemove, args, timeoutSecs, waiter, printCurl, cli, headers) } @@ -278,12 +281,12 @@ $ vespa document remove id:mynamespace:music::a-head-full-of-dreams`, func newDocumentGetCmd(cli *CLI) *cobra.Command { var ( - printCurl bool - ignoreMissing bool - timeoutSecs int - waitSecs int - fieldSet string - headers []string + printCurl bool + ignoreNotFound bool + timeoutSecs int + waitSecs int + fieldSet string + headers []string ) cmd := &cobra.Command{ Use: "get id", @@ -294,11 +297,11 @@ func newDocumentGetCmd(cli *CLI) *cobra.Command { Example: `$ vespa document get id:mynamespace:music::a-head-full-of-dreams...`, RunE: func(cmd *cobra.Command, args []string) error { waiter := cli.waiter(time.Duration(waitSecs)*time.Second, cmd) - return readDocuments(args, timeoutSecs, waiter, printCurl, cli, fieldSet, headers, ignoreMissing) + return readDocuments(args, timeoutSecs, waiter, printCurl, cli, fieldSet, headers, ignoreNotFound) }, } cmd.Flags().StringVar(&fieldSet, "field-set", "", "Fields to include when reading document") - cmd.Flags().BoolVar(&ignoreMissing, "ignore-missing", false, "Ignore failure when getting non-existent documents") + cmd.Flags().BoolVar(&ignoreNotFound, "ignore-missing", false, "Do not treat non-existent document as an error") addDocumentFlags(cli, cmd, &printCurl, &timeoutSecs, &waitSecs, &headers) return cmd } @@ -311,7 +314,7 @@ func documentService(cli *CLI, waiter *Waiter) (*vespa.Service, error) { return waiter.Service(target, cli.config.cluster()) } -func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess, ignoreFailure bool) error { +func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess bool) error { out := cli.Stdout if !result.Success { out = cli.Stderr @@ -333,8 +336,7 @@ func printResult(cli *CLI, result OperationResult, payloadOnlyOnSuccess, ignoreF } fmt.Fprintln(out, result.Payload) } - - if !result.Success && !ignoreFailure { + if !result.Success { err := errHint(fmt.Errorf("document operation failed")) err.quiet = true return err