summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@oath.com>2021-09-22 11:44:52 +0200
committerGitHub <noreply@github.com>2021-09-22 11:44:52 +0200
commita00203c5f216d3a5c06a3a3ce8c2f6d3b8e4716c (patch)
tree6d56a9bb60b7f59bb55e7e34a1eca6eb08ac68e6
parent55a5df7e0c64a8fcfa8e69d7a85111a7f18c48c1 (diff)
parent2239bed79760f5eedf25771a1bd6c84114ea7ade (diff)
Merge pull request #19237 from vespa-engine/mpolden/print-curl-cmd
Support printing curl command for document operations
-rw-r--r--client/go/cmd/api_key_test.go6
-rw-r--r--client/go/cmd/cert_test.go10
-rw-r--r--client/go/cmd/command_tester.go18
-rw-r--r--client/go/cmd/config_test.go37
-rw-r--r--client/go/cmd/curl.go95
-rw-r--r--client/go/cmd/curl_test.go37
-rw-r--r--client/go/cmd/deploy_test.go5
-rw-r--r--client/go/cmd/document.go30
-rw-r--r--client/go/cmd/document_test.go26
-rw-r--r--client/go/cmd/helpers.go13
-rw-r--r--client/go/cmd/man_test.go2
-rw-r--r--client/go/cmd/root.go4
-rw-r--r--client/go/cmd/version_test.go4
-rw-r--r--client/go/curl/curl.go104
-rw-r--r--client/go/curl/curl_test.go45
-rw-r--r--client/go/vespa/document.go53
-rw-r--r--client/go/vespa/target.go33
-rw-r--r--client/go/vespa/target_test.go2
18 files changed, 326 insertions, 198 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/curl.go b/client/go/cmd/curl.go
index f6b40e10f35..bd9fad1b47e 100644
--- a/client/go/cmd/curl.go
+++ b/client/go/cmd/curl.go
@@ -1,22 +1,19 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package cmd
import (
- "fmt"
"log"
"os"
- "os/exec"
"strings"
- "github.com/kballard/go-shellquote"
"github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/curl"
)
var curlDryRun bool
-var curlPath string
func init() {
rootCmd.AddCommand(curlCmd)
- curlCmd.Flags().StringVarP(&curlPath, "path", "p", "", "The path to curl. If this is unset, curl from PATH is used")
curlCmd.Flags().BoolVarP(&curlDryRun, "dry-run", "n", false, "Print the curl command that would be executed")
}
@@ -50,16 +47,20 @@ $ vespa curl -t local -- -v /search/?yql=query
return
}
service := getService("query", 0)
- c := &curl{privateKeyPath: privateKeyFile, certificatePath: certificateFile}
+ url := joinURL(service.BaseURL, args[len(args)-1])
+ rawArgs := args[:len(args)-1]
+ c, err := curl.RawArgs(url, rawArgs...)
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+ c.PrivateKey = privateKeyFile
+ c.Certificate = certificateFile
+
if curlDryRun {
- cmd, err := c.command(service.BaseURL, args...)
- if err != nil {
- fatalErr(err, "Failed to create curl command")
- return
- }
- log.Print(shellquote.Join(cmd.Args...))
+ log.Print(c.String())
} else {
- if err := c.run(service.BaseURL, args...); err != nil {
+ if err := c.Run(os.Stdout, os.Stderr); err != nil {
fatalErr(err, "Failed to run curl")
return
}
@@ -67,72 +68,8 @@ $ vespa curl -t local -- -v /search/?yql=query
},
}
-type curl struct {
- path string
- certificatePath string
- privateKeyPath string
-}
-
-func (c *curl) run(baseURL string, args ...string) error {
- cmd, err := c.command(baseURL, args...)
- if err != nil {
- return err
- }
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- if err := cmd.Start(); err != nil {
- return err
- }
- return cmd.Wait()
-}
-
-func (c *curl) command(baseURL string, args ...string) (*exec.Cmd, error) {
- if len(args) == 0 {
- return nil, fmt.Errorf("need at least one argument")
- }
-
- if c.path == "" {
- resolvedPath, err := resolveCurlPath()
- if err != nil {
- return nil, err
- }
- c.path = resolvedPath
- }
-
- path := args[len(args)-1]
- args = args[:len(args)-1]
- if !hasOption("--key", args) && c.privateKeyPath != "" {
- args = append(args, "--key", c.privateKeyPath)
- }
- if !hasOption("--cert", args) && c.certificatePath != "" {
- args = append(args, "--cert", c.certificatePath)
- }
-
+func joinURL(baseURL, path string) string {
baseURL = strings.TrimSuffix(baseURL, "/")
path = strings.TrimPrefix(path, "/")
- args = append(args, baseURL+"/"+path)
-
- return exec.Command(c.path, args...), nil
-}
-
-func hasOption(option string, args []string) bool {
- for _, arg := range args {
- if arg == option {
- return true
- }
- }
- return false
-}
-
-func resolveCurlPath() (string, error) {
- var curlPath string
- var err error
- curlPath, err = exec.LookPath("curl")
- if err != nil {
- curlPath, err = exec.LookPath("curl.exe")
- if err != nil {
- return "", err
- }
- }
- return curlPath, nil
+ return baseURL + "/" + path
}
diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go
index c3163e731ce..340eacd0bd3 100644
--- a/client/go/cmd/curl_test.go
+++ b/client/go/cmd/curl_test.go
@@ -1,9 +1,9 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package cmd
import (
"fmt"
"path/filepath"
- "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -13,41 +13,10 @@ func TestCurl(t *testing.T) {
homeDir := t.TempDir()
httpClient := &mockHttpClient{}
convergeServices(httpClient)
- out := execute(command{homeDir: homeDir, args: []string{"curl", "-n", "-p", "/usr/bin/curl", "-a", "t1.a1.i1", "--", "-v", "--data-urlencode", "arg=with space", "/search"}}, t, httpClient)
+ out, _ := execute(command{homeDir: homeDir, args: []string{"curl", "-n", "-a", "t1.a1.i1", "--", "-v", "--data-urlencode", "arg=with space", "/search"}}, t, httpClient)
- expected := fmt.Sprintf("/usr/bin/curl -v --data-urlencode 'arg=with space' --key %s --cert %s https://127.0.0.1:8080/search\n",
+ expected := fmt.Sprintf("curl --key %s --cert %s -v --data-urlencode 'arg=with space' https://127.0.0.1:8080/search\n",
filepath.Join(homeDir, ".vespa", "t1.a1.i1", "data-plane-private-key.pem"),
filepath.Join(homeDir, ".vespa", "t1.a1.i1", "data-plane-public-cert.pem"))
assert.Equal(t, expected, out)
}
-
-func TestCurlCommand(t *testing.T) {
- c := &curl{path: "/usr/bin/curl", privateKeyPath: "/tmp/priv-key", certificatePath: "/tmp/cert-key"}
- assertCurl(t, c, "/usr/bin/curl -v --key /tmp/priv-key --cert /tmp/cert-key https://example.com/", "-v", "/")
-
- c = &curl{path: "/usr/bin/curl", privateKeyPath: "/tmp/priv-key", certificatePath: "/tmp/cert-key"}
- assertCurl(t, c, "/usr/bin/curl -v --cert my-cert --key my-key https://example.com/", "-v", "--cert", "my-cert", "--key", "my-key", "/")
-
- c = &curl{path: "/usr/bin/curl2"}
- assertCurl(t, c, "/usr/bin/curl2 -v https://example.com/foo", "-v", "/foo")
-
- c = &curl{path: "/usr/bin/curl"}
- assertCurl(t, c, "/usr/bin/curl -v https://example.com/foo/bar", "-v", "/foo/bar")
-
- c = &curl{path: "/usr/bin/curl"}
- assertCurl(t, c, "/usr/bin/curl -v https://example.com/foo/bar", "-v", "foo/bar")
-
- c = &curl{path: "/usr/bin/curl"}
- assertCurlURL(t, c, "/usr/bin/curl -v https://example.com/foo/bar", "https://example.com/", "-v", "foo/bar")
-}
-
-func assertCurl(t *testing.T, c *curl, expectedOutput string, args ...string) {
- assertCurlURL(t, c, expectedOutput, "https://example.com", args...)
-}
-
-func assertCurlURL(t *testing.T, c *curl, expectedOutput string, url string, args ...string) {
- cmd, err := c.command("https://example.com", args...)
- assert.Nil(t, err)
-
- assert.Equal(t, expectedOutput, strings.Join(cmd.Args, " "))
-}
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/curl/curl.go b/client/go/curl/curl.go
new file mode 100644
index 00000000000..44c3a0ad2a9
--- /dev/null
+++ b/client/go/curl/curl.go
@@ -0,0 +1,104 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package curl
+
+import (
+ "io"
+ "net/url"
+ "os/exec"
+ "runtime"
+
+ "github.com/kballard/go-shellquote"
+)
+
+type header struct {
+ key string
+ value string
+}
+
+type Command struct {
+ Path string
+ Method string
+ PrivateKey string
+ Certificate string
+ BodyFile string
+ url *url.URL
+ headers []header
+ rawArgs []string
+}
+
+func (c *Command) Args() []string {
+ var args []string
+ if c.PrivateKey != "" {
+ args = append(args, "--key", c.PrivateKey)
+ }
+ if c.Certificate != "" {
+ args = append(args, "--cert", c.Certificate)
+ }
+ if c.Method != "" {
+ args = append(args, "-X", c.Method)
+ }
+ for _, header := range c.headers {
+ args = append(args, "-H", header.key+": "+header.value)
+ }
+ if c.BodyFile != "" {
+ args = append(args, "--data-binary", "@"+c.BodyFile)
+ }
+ args = append(args, c.rawArgs...)
+ args = append(args, c.url.String())
+ return args
+}
+
+func (c *Command) String() string {
+ args := []string{c.Path}
+ args = append(args, c.Args()...)
+ return shellquote.Join(args...)
+}
+
+func (c *Command) Header(key, value string) {
+ c.headers = append(c.headers, header{key: key, value: value})
+}
+
+func (c *Command) Param(key, value string) {
+ query := c.url.Query()
+ query.Set(key, value)
+ c.url.RawQuery = query.Encode()
+}
+
+func (c *Command) Run(stdout, stderr io.Writer) error {
+ cmd := exec.Command(c.Path, c.Args()...)
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ return cmd.Wait()
+}
+
+func Post(url string) (*Command, error) { return curl("POST", url) }
+
+func Get(url string) (*Command, error) { return curl("", url) }
+
+func RawArgs(url string, args ...string) (*Command, error) {
+ c, err := curl("", url)
+ if err != nil {
+ return nil, err
+ }
+ c.rawArgs = args
+ return c, nil
+}
+
+func curl(method, rawurl string) (*Command, error) {
+ path := "curl"
+ if runtime.GOOS == "windows" {
+ path = "curl.exe"
+ }
+ realURL, err := url.Parse(rawurl)
+ if err != nil {
+ return nil, err
+ }
+ return &Command{
+ Path: path,
+ Method: method,
+ url: realURL,
+ }, nil
+}
diff --git a/client/go/curl/curl_test.go b/client/go/curl/curl_test.go
new file mode 100644
index 00000000000..90bf274f7a2
--- /dev/null
+++ b/client/go/curl/curl_test.go
@@ -0,0 +1,45 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package curl
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPost(t *testing.T) {
+ c, err := Post("https://example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ c.PrivateKey = "key.pem"
+ c.Certificate = "cert.pem"
+ c.BodyFile = "file.json"
+ c.Header("Content-Type", "application/json")
+
+ assert.Equal(t, "curl --key key.pem --cert cert.pem -X POST -H 'Content-Type: application/json' --data-binary @file.json https://example.com", c.String())
+}
+
+func TestGet(t *testing.T) {
+ c, err := Get("https://example.com")
+ if err != nil {
+ t.Fatal(err)
+ }
+ c.PrivateKey = "key.pem"
+ c.Certificate = "cert.pem"
+ c.Param("yql", "select * from sources * where title contains 'foo';")
+ c.Param("hits", "5")
+
+ assert.Equal(t, `curl --key key.pem --cert cert.pem https://example.com\?hits=5\&yql=select+%2A+from+sources+%2A+where+title+contains+%27foo%27%3B`, c.String())
+}
+
+func TestRawArgs(t *testing.T) {
+ c, err := RawArgs("https://example.com/search", "-v", "-m", "10", "-H", "foo: bar")
+ if err != nil {
+ t.Fatal(err)
+ }
+ c.PrivateKey = "key.pem"
+ c.Certificate = "cert.pem"
+
+ assert.Equal(t, `curl --key key.pem --cert cert.pem -v -m 10 -H 'foo: bar' https://example.com/search`, c.String())
+}
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