diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-09-22 10:30:19 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2021-09-22 10:45:04 +0200 |
commit | 2239bed79760f5eedf25771a1bd6c84114ea7ade (patch) | |
tree | 7174ae81e5b9407a1530f7a3a08a00c8763f67fc /client | |
parent | b6d3d1814961d95c84d4dac12e875b6fa3bdc3d6 (diff) |
Support printing curl command for document operations
Diffstat (limited to 'client')
-rw-r--r-- | client/go/cmd/api_key_test.go | 6 | ||||
-rw-r--r-- | client/go/cmd/cert_test.go | 10 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 18 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 37 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 5 | ||||
-rw-r--r-- | client/go/cmd/document.go | 30 | ||||
-rw-r--r-- | client/go/cmd/document_test.go | 26 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 13 | ||||
-rw-r--r-- | client/go/cmd/man_test.go | 2 | ||||
-rw-r--r-- | client/go/cmd/root.go | 4 | ||||
-rw-r--r-- | client/go/cmd/version_test.go | 4 | ||||
-rw-r--r-- | client/go/vespa/document.go | 53 | ||||
-rw-r--r-- | client/go/vespa/target.go | 33 | ||||
-rw-r--r-- | client/go/vespa/target_test.go | 2 |
14 files changed, 158 insertions, 85 deletions
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go index c00f520aa25..2497568604f 100644 --- a/client/go/cmd/api_key_test.go +++ b/client/go/cmd/api_key_test.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // Author: mpolden package cmd @@ -14,10 +14,10 @@ func TestAPIKey(t *testing.T) { homeDir := t.TempDir() keyFile := homeDir + "/.vespa/t1.api-key.pem" - out := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) + out, _ := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) assert.True(t, strings.HasPrefix(out, "Success: API private key written to "+keyFile+"\n")) - out = execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) + out, _ = execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) assert.True(t, strings.HasPrefix(out, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n")) assert.True(t, strings.Contains(out, "This is your public key")) } diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go index 36abdae1787..d93def2fa70 100644 --- a/client/go/cmd/cert_test.go +++ b/client/go/cmd/cert_test.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // Author: mpolden package cmd @@ -16,7 +16,7 @@ import ( func TestCert(t *testing.T) { homeDir := t.TempDir() pkgDir := mockApplicationPackage(t, false) - out := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + out, _ := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) app, err := vespa.ApplicationFromString("t1.a1.i1") assert.Nil(t, err) @@ -28,7 +28,7 @@ func TestCert(t *testing.T) { assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Certificate written to %s\nSuccess: Private key written to %s\n", pkgCertificate, certificate, privateKey), out) - out = execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + out, _ = execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) assert.Contains(t, out, fmt.Sprintf("Error: Application package %s already contains a certificate", appDir)) } @@ -41,13 +41,13 @@ func TestCertCompressedPackage(t *testing.T) { _, err = os.Create(zipFile) assert.Nil(t, err) - out := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + out, _ := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) assert.Contains(t, out, "Error: Cannot add certificate to compressed application package") err = os.Remove(zipFile) assert.Nil(t, err) - out = execute(command{args: []string{"cert", "-f", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + out, _ = execute(command{args: []string{"cert", "-f", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) assert.Contains(t, out, "Success: Certificate written to") assert.Contains(t, out, "Success: Private key written to") } diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go index 3d19e772875..f455ffa9957 100644 --- a/client/go/cmd/command_tester.go +++ b/client/go/cmd/command_tester.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // A helper for testing commands // Author: bratseth @@ -17,7 +17,6 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/stretchr/testify/assert" "github.com/vespa-engine/vespa/client/go/util" ) @@ -27,7 +26,7 @@ type command struct { moreArgs []string } -func execute(cmd command, t *testing.T, client *mockHttpClient) string { +func execute(cmd command, t *testing.T, client *mockHttpClient) (string, string) { if client != nil { util.ActiveHttpClient = client } @@ -56,19 +55,20 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) string { exitFunc = func(code int) {} // Capture stdout and execute command - var b bytes.Buffer - stdout = &b + var capturedOut bytes.Buffer + var capturedErr bytes.Buffer + stdout = &capturedOut + stderr = &capturedErr // Execute command and return output rootCmd.SetArgs(append(cmd.args, cmd.moreArgs...)) rootCmd.Execute() - out, err := ioutil.ReadAll(&b) - assert.Nil(t, err, "No error") - return string(out) + return capturedOut.String(), capturedErr.String() } func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArgs []string) string { - return execute(command{args: args, moreArgs: moreArgs}, t, client) + out, _ := execute(command{args: args, moreArgs: moreArgs}, t, client) + return out } type mockHttpClient struct { diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go index 70ad5d558e1..cf50f561f0f 100644 --- a/client/go/cmd/config_test.go +++ b/client/go/cmd/config_test.go @@ -8,23 +8,28 @@ import ( func TestConfig(t *testing.T) { homeDir := t.TempDir() - assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", execute(command{homeDir: homeDir, args: []string{"config", "set", "foo", "bar"}}, t, nil)) - assert.Equal(t, "foo = <unset>\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "foo"}}, t, nil)) - assert.Equal(t, "target = local\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "target"}}, t, nil)) - assert.Equal(t, "", execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, nil)) - assert.Equal(t, "target = cloud\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "target"}}, t, nil)) - assert.Equal(t, "", execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "http://127.0.0.1:8080"}}, t, nil)) - assert.Equal(t, "", execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "https://127.0.0.1"}}, t, nil)) - assert.Equal(t, "target = https://127.0.0.1\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "target"}}, t, nil)) + assertConfigCommand(t, "invalid option or value: \"foo\": \"bar\"\n", homeDir, "config", "set", "foo", "bar") + assertConfigCommand(t, "foo = <unset>\n", homeDir, "config", "get", "foo") + assertConfigCommand(t, "target = local\n", homeDir, "config", "get", "target") + assertConfigCommand(t, "", homeDir, "config", "set", "target", "cloud") + assertConfigCommand(t, "target = cloud\n", homeDir, "config", "get", "target") + assertConfigCommand(t, "", homeDir, "config", "set", "target", "http://127.0.0.1:8080") + assertConfigCommand(t, "", homeDir, "config", "set", "target", "https://127.0.0.1") + assertConfigCommand(t, "target = https://127.0.0.1\n", homeDir, "config", "get", "target") - assert.Equal(t, "invalid application: \"foo\"\n", execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "foo"}}, t, nil)) - assert.Equal(t, "application = <unset>\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "application"}}, t, nil)) - assert.Equal(t, "", execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, nil)) - assert.Equal(t, "application = t1.a1.i1\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "application"}}, t, nil)) + assertConfigCommand(t, "invalid application: \"foo\"\n", homeDir, "config", "set", "application", "foo") + assertConfigCommand(t, "application = <unset>\n", homeDir, "config", "get", "application") + assertConfigCommand(t, "", homeDir, "config", "set", "application", "t1.a1.i1") + assertConfigCommand(t, "application = t1.a1.i1\n", homeDir, "config", "get", "application") - assert.Equal(t, "application = t1.a1.i1\ncolor = auto\ntarget = https://127.0.0.1\nwait = 0\n", execute(command{homeDir: homeDir, args: []string{"config", "get"}}, t, nil)) + assertConfigCommand(t, "application = t1.a1.i1\ncolor = auto\ntarget = https://127.0.0.1\nwait = 0\n", homeDir, "config", "get") - assert.Equal(t, "", execute(command{homeDir: homeDir, args: []string{"config", "set", "wait", "60"}}, t, nil)) - assert.Equal(t, "wait option must be an integer >= 0, got \"foo\"\n", execute(command{homeDir: homeDir, args: []string{"config", "set", "wait", "foo"}}, t, nil)) - assert.Equal(t, "wait = 60\n", execute(command{homeDir: homeDir, args: []string{"config", "get", "wait"}}, t, nil)) + assertConfigCommand(t, "", homeDir, "config", "set", "wait", "60") + assertConfigCommand(t, "wait option must be an integer >= 0, got \"foo\"\n", homeDir, "config", "set", "wait", "foo") + assertConfigCommand(t, "wait = 60\n", homeDir, "config", "get", "wait") +} + +func assertConfigCommand(t *testing.T, expected, homeDir string, args ...string) { + out, _ := execute(command{homeDir: homeDir, args: args}, t, nil) + assert.Equal(t, expected, out) } diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go index f24ba0829f9..443f7e8846f 100644 --- a/client/go/cmd/deploy_test.go +++ b/client/go/cmd/deploy_test.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // deploy command tests // Author: bratseth @@ -130,9 +130,10 @@ func assertActivate(applicationPackage string, arguments []string, t *testing.T) if err := cfg.WriteSessionID(vespa.DefaultApplication, 42); err != nil { t.Fatal(err) } + out, _ := execute(command{args: arguments, homeDir: homeDir}, t, client) assert.Equal(t, "Success: Activated "+applicationPackage+" with session 42\n", - execute(command{args: arguments, homeDir: homeDir}, t, client)) + out) url := "http://127.0.0.1:19071/application/v2/tenant/default/session/42/active" assert.Equal(t, url, client.lastRequest.URL.String()) assert.Equal(t, "PUT", client.lastRequest.Method) diff --git a/client/go/cmd/document.go b/client/go/cmd/document.go index 78c6596f511..cc5fb948e3b 100644 --- a/client/go/cmd/document.go +++ b/client/go/cmd/document.go @@ -1,10 +1,12 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // vespa document command // author: bratseth package cmd import ( + "io" + "io/ioutil" "log" "strings" @@ -13,12 +15,15 @@ import ( "github.com/vespa-engine/vespa/client/go/vespa" ) +var printCurl bool + func init() { rootCmd.AddCommand(documentCmd) documentCmd.AddCommand(documentPutCmd) documentCmd.AddCommand(documentUpdateCmd) documentCmd.AddCommand(documentRemoveCmd) documentCmd.AddCommand(documentGetCmd) + documentCmd.PersistentFlags().BoolVarP(&printCurl, "verbose", "v", false, "Print the equivalent curl command for the document operation") } var documentCmd = &cobra.Command{ @@ -38,7 +43,7 @@ should be used instead of this.`, DisableAutoGenTag: true, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - printResult(vespa.Send(args[0], documentService()), false) + printResult(vespa.Send(args[0], documentService(), curlOutput()), false) }, } @@ -54,9 +59,9 @@ $ vespa document put id:mynamespace:music::a-head-full-of-dreams src/test/resour DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { if len(args) == 1 { - printResult(vespa.Put("", args[0], documentService()), false) + printResult(vespa.Put("", args[0], documentService(), curlOutput()), false) } else { - printResult(vespa.Put(args[0], args[1], documentService()), false) + printResult(vespa.Put(args[0], args[1], documentService(), curlOutput()), false) } }, } @@ -72,9 +77,9 @@ $ vespa document update id:mynamespace:music::a-head-full-of-dreams src/test/res DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { if len(args) == 1 { - printResult(vespa.Update("", args[0], documentService()), false) + printResult(vespa.Update("", args[0], documentService(), curlOutput()), false) } else { - printResult(vespa.Update(args[0], args[1], documentService()), false) + printResult(vespa.Update(args[0], args[1], documentService(), curlOutput()), false) } }, } @@ -90,9 +95,9 @@ $ vespa document remove id:mynamespace:music::a-head-full-of-dreams`, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { if strings.HasPrefix(args[0], "id:") { - printResult(vespa.RemoveId(args[0], documentService()), false) + printResult(vespa.RemoveId(args[0], documentService(), curlOutput()), false) } else { - printResult(vespa.RemoveOperation(args[0], documentService()), false) + printResult(vespa.RemoveOperation(args[0], documentService(), curlOutput()), false) } }, } @@ -104,12 +109,19 @@ var documentGetCmd = &cobra.Command{ DisableAutoGenTag: true, Example: `$ vespa document get id:mynamespace:music::a-head-full-of-dreams`, Run: func(cmd *cobra.Command, args []string) { - printResult(vespa.Get(args[0], documentService()), true) + printResult(vespa.Get(args[0], documentService(), curlOutput()), true) }, } func documentService() *vespa.Service { return getService("document", 0) } +func curlOutput() io.Writer { + if printCurl { + return stderr + } + return ioutil.Discard +} + func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) { if !result.Success { log.Print(color.Red("Error: "), result.Message) diff --git a/client/go/cmd/document_test.go b/client/go/cmd/document_test.go index c298d5ef285..8aecb538f89 100644 --- a/client/go/cmd/document_test.go +++ b/client/go/cmd/document_test.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // document command tests // Author: bratseth @@ -19,6 +19,11 @@ func TestDocumentSendPut(t *testing.T) { "put", "POST", "id:mynamespace:music::a-head-full-of-dreams", "testdata/A-Head-Full-of-Dreams-Put.json", t) } +func TestDocumentSendPutVerbose(t *testing.T) { + assertDocumentSend([]string{"document", "-v", "testdata/A-Head-Full-of-Dreams-Put.json"}, + "put", "POST", "id:mynamespace:music::a-head-full-of-dreams", "testdata/A-Head-Full-of-Dreams-Put.json", t) +} + func TestDocumentSendUpdate(t *testing.T) { assertDocumentSend([]string{"document", "testdata/A-Head-Full-of-Dreams-Update.json"}, "update", "PUT", "id:mynamespace:music::a-head-full-of-dreams", "testdata/A-Head-Full-of-Dreams-Update.json", t) @@ -93,11 +98,22 @@ func TestDocumentGet(t *testing.T) { func assertDocumentSend(arguments []string, expectedOperation string, expectedMethod string, expectedDocumentId string, expectedPayloadFile string, t *testing.T) { client := &mockHttpClient{} documentURL := documentServiceURL(client) - assert.Equal(t, - "Success: "+expectedOperation+" "+expectedDocumentId+"\n", - executeCommand(t, client, arguments, []string{})) expectedPath, _ := vespa.IdToURLPath(expectedDocumentId) - assert.Equal(t, documentURL+"/document/v1/"+expectedPath, client.lastRequest.URL.String()) + expectedURL := documentURL + "/document/v1/" + expectedPath + out, errOut := execute(command{args: arguments}, t, client) + + verbose := false + for _, a := range arguments { + if a == "-v" { + verbose = true + } + } + if verbose { + expectedCurl := "curl -X " + expectedMethod + " -H 'Content-Type: application/json' --data-binary @" + expectedPayloadFile + " " + expectedURL + "\n" + assert.Equal(t, expectedCurl, errOut) + } + assert.Equal(t, "Success: "+expectedOperation+" "+expectedDocumentId+"\n", out) + assert.Equal(t, expectedURL, client.lastRequest.URL.String()) assert.Equal(t, "application/json", client.lastRequest.Header.Get("Content-Type")) assert.Equal(t, expectedMethod, client.lastRequest.Method) diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go index 3493a4b32a8..f29a842aed2 100644 --- a/client/go/cmd/helpers.go +++ b/client/go/cmd/helpers.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // Helpers used by multiple sub-commands. // Author: mpolden @@ -147,7 +147,16 @@ func getTarget() vespa.Target { if err != nil { fatalErrHint(err, "Deployment to cloud requires a certificate. Try 'vespa cert'") } - return vespa.CloudTarget(deployment, kp, apiKey, vespa.LogOptions{Writer: stdout, Level: vespa.LogLevel(logLevelArg)}) + return vespa.CloudTarget(deployment, apiKey, + vespa.TLSOptions{ + KeyPair: kp, + CertificateFile: certificateFile, + PrivateKeyFile: privateKeyFile, + }, + vespa.LogOptions{ + Writer: stdout, + Level: vespa.LogLevel(logLevelArg), + }) } fatalErrHint(fmt.Errorf("Invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL") return nil diff --git a/client/go/cmd/man_test.go b/client/go/cmd/man_test.go index 59efc64b8de..f7c33c8b3a1 100644 --- a/client/go/cmd/man_test.go +++ b/client/go/cmd/man_test.go @@ -11,7 +11,7 @@ import ( func TestMan(t *testing.T) { tmpDir := t.TempDir() - out := execute(command{args: []string{"man", tmpDir}}, t, nil) + out, _ := execute(command{args: []string{"man", tmpDir}}, t, nil) assert.Equal(t, fmt.Sprintf("Success: Man pages written to %s\n", tmpDir), out) assert.True(t, util.PathExists(filepath.Join(tmpDir, "vespa.1"))) } diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go index 6bfca7fd613..cd8427c3ac6 100644 --- a/client/go/cmd/root.go +++ b/client/go/cmd/root.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // Root Cobra command: vespa // author: bratseth @@ -17,7 +17,6 @@ import ( var ( // TODO: add timeout flag - // TODO: add flag to show http request made rootCmd = &cobra.Command{ Use: "vespa command-name", Short: "The command-line tool for Vespa.ai", @@ -40,6 +39,7 @@ Vespa documentation: https://docs.vespa.ai`, color = aurora.NewAurora(false) stdout = colorable.NewColorableStdout() + stderr = colorable.NewColorableStderr() ) const ( diff --git a/client/go/cmd/version_test.go b/client/go/cmd/version_test.go index 3b0f73de408..9eeaaaa4692 100644 --- a/client/go/cmd/version_test.go +++ b/client/go/cmd/version_test.go @@ -14,7 +14,7 @@ func TestVersion(t *testing.T) { util.ActiveHttpClient = c sp = &mockSubprocess{} - out := execute(command{args: []string{"version"}}, t, nil) + out, _ := execute(command{args: []string{"version"}}, t, nil) assert.Contains(t, out, "vespa version 0.0.0-devel compiled with") assert.Contains(t, out, "New release available: 1.2.3\nhttps://github.com/vespa-engine/vespa/releases/tag/v1.2.3") } @@ -25,7 +25,7 @@ func TestVersionCheckHomebrew(t *testing.T) { util.ActiveHttpClient = c sp = &mockSubprocess{programPath: "/usr/local/bin/vespa", output: "/usr/local"} - out := execute(command{args: []string{"version"}}, t, nil) + out, _ := execute(command{args: []string{"version"}}, t, nil) assert.Contains(t, out, "vespa version 0.0.0-devel compiled with") assert.Contains(t, out, "New release available: 1.2.3\n"+ "https://github.com/vespa-engine/vespa/releases/tag/v1.2.3\n"+ diff --git a/client/go/vespa/document.go b/client/go/vespa/document.go index 7b750b86728..cfac1930199 100644 --- a/client/go/vespa/document.go +++ b/client/go/vespa/document.go @@ -1,4 +1,4 @@ -// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. // vespa document API client // Author: bratseth @@ -7,34 +7,36 @@ package vespa import ( "bytes" "encoding/json" + "io" "io/ioutil" "net/http" "net/url" "os" "time" + "github.com/vespa-engine/vespa/client/go/curl" "github.com/vespa-engine/vespa/client/go/util" ) // Sends the operation given in the file -func Send(jsonFile string, service *Service) util.OperationResult { - return sendOperation("", jsonFile, service, anyOperation) +func Send(jsonFile string, service *Service, curlOutput io.Writer) util.OperationResult { + return sendOperation("", jsonFile, service, anyOperation, curlOutput) } -func Put(documentId string, jsonFile string, service *Service) util.OperationResult { - return sendOperation(documentId, jsonFile, service, putOperation) +func Put(documentId string, jsonFile string, service *Service, curlOutput io.Writer) util.OperationResult { + return sendOperation(documentId, jsonFile, service, putOperation, curlOutput) } -func Update(documentId string, jsonFile string, service *Service) util.OperationResult { - return sendOperation(documentId, jsonFile, service, updateOperation) +func Update(documentId string, jsonFile string, service *Service, curlOutput io.Writer) util.OperationResult { + return sendOperation(documentId, jsonFile, service, updateOperation, curlOutput) } -func RemoveId(documentId string, service *Service) util.OperationResult { - return sendOperation(documentId, "", service, removeOperation) +func RemoveId(documentId string, service *Service, curlOutput io.Writer) util.OperationResult { + return sendOperation(documentId, "", service, removeOperation, curlOutput) } -func RemoveOperation(jsonFile string, service *Service) util.OperationResult { - return sendOperation("", jsonFile, service, removeOperation) +func RemoveOperation(jsonFile string, service *Service, curlOutput io.Writer) util.OperationResult { + return sendOperation("", jsonFile, service, removeOperation, curlOutput) } const ( @@ -44,7 +46,7 @@ const ( removeOperation string = "remove" ) -func sendOperation(documentId string, jsonFile string, service *Service, operation string) util.OperationResult { +func sendOperation(documentId string, jsonFile string, service *Service, operation string, curlOutput io.Writer) util.OperationResult { header := http.Header{} header.Add("Content-Type", "application/json") @@ -93,7 +95,7 @@ func sendOperation(documentId string, jsonFile string, service *Service, operati Header: header, Body: ioutil.NopCloser(bytes.NewReader(documentData)), } - response, err := service.Do(request, time.Second*60) + response, err := serviceDo(service, request, jsonFile, curlOutput) if response == nil { return util.Failure("Request failed: " + err.Error()) } @@ -132,7 +134,28 @@ func operationToHTTPMethod(operation string) string { panic("Unexpected document operation ''" + operation + "'") } -func Get(documentId string, service *Service) util.OperationResult { +func serviceDo(service *Service, request *http.Request, filename string, curlOutput io.Writer) (*http.Response, error) { + cmd, err := curl.RawArgs(request.URL.String()) + if err != nil { + return nil, err + } + cmd.Method = request.Method + for k, vs := range request.Header { + for _, v := range vs { + cmd.Header(k, v) + } + } + cmd.BodyFile = filename + cmd.Certificate = service.TLSOptions.CertificateFile + cmd.PrivateKey = service.TLSOptions.PrivateKeyFile + out := cmd.String() + "\n" + if _, err := io.WriteString(curlOutput, out); err != nil { + return nil, err + } + return service.Do(request, time.Second*60) +} + +func Get(documentId string, service *Service, curlOutput io.Writer) util.OperationResult { documentPath, documentPathError := IdToURLPath(documentId) if documentPathError != nil { return util.Failure("Invalid document id '" + documentId + "': " + documentPathError.Error()) @@ -147,7 +170,7 @@ func Get(documentId string, service *Service) util.OperationResult { URL: url, Method: "GET", } - response, err := service.Do(request, time.Second*60) + response, err := serviceDo(service, request, "", curlOutput) if response == nil { return util.Failure("Request failed: " + err.Error()) } diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go index 065471d2a1e..69dc876c1c8 100644 --- a/client/go/vespa/target.go +++ b/client/go/vespa/target.go @@ -31,9 +31,9 @@ const ( // Service represents a Vespa service. type Service struct { - BaseURL string - Name string - certificate tls.Certificate + BaseURL string + Name string + TLSOptions TLSOptions } // Target represents a Vespa platform, running named Vespa services. @@ -48,6 +48,13 @@ type Target interface { DiscoverServices(timeout time.Duration, runID int64) error } +// TLSOptions configures the certificate to use for service requests. +type TLSOptions struct { + KeyPair tls.Certificate + CertificateFile string + PrivateKeyFile string +} + // LogOptions configures the log output to produce when waiting for services. type LogOptions struct { Writer io.Writer @@ -61,8 +68,8 @@ type customTarget struct { // Do sends request to this service. Any required authentication happens automatically. func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Response, error) { - if s.certificate.Certificate != nil { - util.ActiveHttpClient.UseCertificate(s.certificate) + if s.TLSOptions.KeyPair.Certificate != nil { + util.ActiveHttpClient.UseCertificate(s.TLSOptions.KeyPair) } return util.HttpDo(request, timeout, s.Description()) } @@ -83,7 +90,7 @@ func (s *Service) Wait(timeout time.Duration) (int, error) { return 0, err } okFunc := func(status int, response []byte) (bool, error) { return status/100 == 2, nil } - return wait(okFunc, func() *http.Request { return req }, &s.certificate, timeout) + return wait(okFunc, func() *http.Request { return req }, &s.TLSOptions.KeyPair, timeout) } func (s *Service) Description() string { @@ -167,8 +174,8 @@ type cloudTarget struct { cloudAPI string targetType string deployment Deployment - keyPair tls.Certificate apiKey []byte + tlsOptions TLSOptions logOptions LogOptions queryURL string @@ -185,12 +192,12 @@ func (t *cloudTarget) Service(name string) (*Service, error) { if t.queryURL == "" { return nil, fmt.Errorf("service %s not discovered", name) } - return &Service{Name: name, BaseURL: t.queryURL, certificate: t.keyPair}, nil + return &Service{Name: name, BaseURL: t.queryURL, TLSOptions: t.tlsOptions}, nil case documentService: if t.documentURL == "" { return nil, fmt.Errorf("service %s not discovered", name) } - return &Service{Name: name, BaseURL: t.documentURL, certificate: t.keyPair}, nil + return &Service{Name: name, BaseURL: t.documentURL, TLSOptions: t.tlsOptions}, nil } return nil, fmt.Errorf("unknown service: %s", name) } @@ -245,7 +252,7 @@ func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout tim } return true, nil } - _, err = wait(jobSuccessFunc, requestFunc, &t.keyPair, timeout) + _, err = wait(jobSuccessFunc, requestFunc, &t.tlsOptions.KeyPair, timeout) return err } @@ -298,7 +305,7 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura endpointURL = resp.Endpoints[0].URL return true, nil } - if _, err = wait(endpointFunc, func() *http.Request { return req }, &t.keyPair, timeout); err != nil { + if _, err = wait(endpointFunc, func() *http.Request { return req }, &t.tlsOptions.KeyPair, timeout); err != nil { return err } if endpointURL == "" { @@ -320,13 +327,13 @@ func CustomTarget(baseURL string) Target { } // CloudTarget creates a Target for the Vespa Cloud platform. -func CloudTarget(deployment Deployment, keyPair tls.Certificate, apiKey []byte, logOptions LogOptions) Target { +func CloudTarget(deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target { return &cloudTarget{ cloudAPI: defaultCloudAPI, targetType: cloudTargetType, deployment: deployment, - keyPair: keyPair, apiKey: apiKey, + tlsOptions: tlsOptions, logOptions: logOptions, } } diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go index b524f73c5d3..31f145f0db3 100644 --- a/client/go/vespa/target_test.go +++ b/client/go/vespa/target_test.go @@ -106,8 +106,8 @@ func TestCloudTargetWait(t *testing.T) { Application: ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"}, Zone: ZoneID{Environment: "dev", Region: "us-north-1"}, }, - x509KeyPair, apiKey, + TLSOptions{KeyPair: x509KeyPair}, LogOptions{Writer: &logWriter}) if ct, ok := target.(*cloudTarget); ok { ct.cloudAPI = srv.URL |