summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/go/cmd/api_key.go12
-rw-r--r--client/go/cmd/api_key_test.go8
-rw-r--r--client/go/cmd/cert.go24
-rw-r--r--client/go/cmd/cert_test.go17
-rw-r--r--client/go/cmd/command_tester.go13
-rw-r--r--client/go/cmd/config.go163
-rw-r--r--client/go/cmd/config_test.go34
-rw-r--r--client/go/cmd/curl.go143
-rw-r--r--client/go/cmd/curl_test.go53
-rw-r--r--client/go/cmd/deploy.go61
-rw-r--r--client/go/cmd/deploy_test.go11
-rw-r--r--client/go/cmd/helpers.go48
-rw-r--r--client/go/cmd/root.go1
-rw-r--r--client/go/go.mod1
-rw-r--r--client/go/go.sum2
-rw-r--r--client/go/vespa/deploy.go2
16 files changed, 434 insertions, 159 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go
index c94faa0d5e3..90cbdbc5bc1 100644
--- a/client/go/cmd/api_key.go
+++ b/client/go/cmd/api_key.go
@@ -7,7 +7,6 @@ import (
"fmt"
"io/ioutil"
"log"
- "path/filepath"
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/util"
@@ -29,16 +28,17 @@ var apiKeyCmd = &cobra.Command{
DisableAutoGenTag: true,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
- configDir := configDir("")
- if configDir == "" {
- return
- }
app, err := vespa.ApplicationFromString(getApplication())
if err != nil {
fatalErr(err, "Could not parse application")
return
}
- apiKeyFile := filepath.Join(configDir, app.Tenant+".api-key.pem")
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
+ apiKeyFile := cfg.APIKeyPath(app.Tenant)
if util.PathExists(apiKeyFile) && !overwriteKey {
printErrHint(fmt.Errorf("File %s already exists", apiKeyFile), "Use -f to overwrite it")
printPublicKey(apiKeyFile, app.Tenant)
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go
index 0e50fd6d669..c00f520aa25 100644
--- a/client/go/cmd/api_key_test.go
+++ b/client/go/cmd/api_key_test.go
@@ -11,13 +11,13 @@ import (
)
func TestAPIKey(t *testing.T) {
- configDir := t.TempDir()
- keyFile := configDir + "/.vespa/t1.api-key.pem"
+ homeDir := t.TempDir()
+ keyFile := homeDir + "/.vespa/t1.api-key.pem"
- out := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, configDir: configDir}, 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"}, configDir: configDir}, 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.go b/client/go/cmd/cert.go
index e1e11b6f73e..078c0704f9d 100644
--- a/client/go/cmd/cert.go
+++ b/client/go/cmd/cert.go
@@ -28,20 +28,34 @@ var certCmd = &cobra.Command{
DisableAutoGenTag: true,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
- app := getApplication()
+ app, err := vespa.ApplicationFromString(getApplication())
+ if err != nil {
+ fatalErr(err)
+ return
+ }
pkg, err := vespa.ApplicationPackageFrom(applicationSource(args))
if err != nil {
fatalErr(err)
return
}
- configDir := configDir(app)
- if configDir == "" {
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err)
return
}
securityDir := filepath.Join(pkg.Path, "security")
pkgCertificateFile := filepath.Join(securityDir, "clients.pem")
- privateKeyFile := filepath.Join(configDir, "data-plane-private-key.pem")
- certificateFile := filepath.Join(configDir, "data-plane-public-cert.pem")
+ privateKeyFile, err := cfg.PrivateKeyPath(app)
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+ certificateFile, err := cfg.CertificatePath(app)
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+
if !overwriteCertificate {
for _, file := range []string{pkgCertificateFile, privateKeyFile, certificateFile} {
if util.PathExists(file) {
diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go
index e655f76b0f1..174b5fe5e9d 100644
--- a/client/go/cmd/cert_test.go
+++ b/client/go/cmd/cert_test.go
@@ -11,20 +11,23 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
func TestCert(t *testing.T) {
- tmpDir := t.TempDir()
- mockApplicationPackage(t, tmpDir)
- out := execute(command{args: []string{"cert", "-a", "t1.a1.i1", tmpDir}, configDir: tmpDir}, t, nil)
+ homeDir := t.TempDir()
+ mockApplicationPackage(t, homeDir)
+ out := execute(command{args: []string{"cert", "-a", "t1.a1.i1", homeDir}, homeDir: homeDir}, t, nil)
- pkgCertificate := filepath.Join(tmpDir, "security", "clients.pem")
- certificate := filepath.Join(tmpDir, ".vespa", "t1.a1.i1", "data-plane-public-cert.pem")
- privateKey := filepath.Join(tmpDir, ".vespa", "t1.a1.i1", "data-plane-private-key.pem")
+ app, err := vespa.ApplicationFromString("t1.a1.i1")
+ assert.Nil(t, err)
+ pkgCertificate := filepath.Join(homeDir, "security", "clients.pem")
+ certificate := filepath.Join(homeDir, ".vespa", app.String(), "data-plane-public-cert.pem")
+ privateKey := filepath.Join(homeDir, ".vespa", app.String(), "data-plane-private-key.pem")
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", tmpDir}, configDir: tmpDir}, t, nil)
+ out = execute(command{args: []string{"cert", "-a", "t1.a1.i1", homeDir}, homeDir: homeDir}, t, nil)
assert.True(t, strings.HasPrefix(out, "Error: Certificate or private key"))
}
diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go
index be752f03d53..095a1af7ac3 100644
--- a/client/go/cmd/command_tester.go
+++ b/client/go/cmd/command_tester.go
@@ -11,6 +11,7 @@ import (
"log"
"net/http"
"os"
+ "path/filepath"
"strconv"
"testing"
"time"
@@ -23,9 +24,9 @@ import (
)
type command struct {
- configDir string
- args []string
- moreArgs []string
+ homeDir string
+ args []string
+ moreArgs []string
}
func execute(cmd command, t *testing.T, client *mockHttpClient) string {
@@ -37,11 +38,11 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) string {
color = aurora.NewAurora(false)
// Set config dir. Use a separate one per test if none is specified
- if cmd.configDir == "" {
- cmd.configDir = t.TempDir()
+ if cmd.homeDir == "" {
+ cmd.homeDir = t.TempDir()
viper.Reset()
}
- os.Setenv("VESPA_CLI_HOME", cmd.configDir)
+ os.Setenv("VESPA_CLI_HOME", filepath.Join(cmd.homeDir, ".vespa"))
// Reset flags to their default value - persistent flags in Cobra persists over tests
rootCmd.Flags().VisitAll(func(f *pflag.Flag) {
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index bb1662d0b07..262f5d584b4 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -6,6 +6,7 @@ package cmd
import (
"fmt"
+ "io/ioutil"
"log"
"os"
"path/filepath"
@@ -49,10 +50,17 @@ var setConfigCmd = &cobra.Command{
DisableAutoGenTag: true,
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
- if err := setOption(args[0], args[1]); err != nil {
- log.Print(err)
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
+ if err := cfg.Set(args[0], args[1]); err != nil {
+ fatalErr(err)
} else {
- writeConfig()
+ if err := cfg.Write(); err != nil {
+ fatalErr(err)
+ }
}
},
}
@@ -64,69 +72,121 @@ var getConfigCmd = &cobra.Command{
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) {
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
+
if len(args) == 0 { // Print all values
- printOption(targetFlag)
- printOption(applicationFlag)
+ printOption(cfg, targetFlag)
+ printOption(cfg, applicationFlag)
} else {
- printOption(args[0])
+ printOption(cfg, args[0])
}
},
}
-func printOption(option string) {
- value, err := getOption(option)
- if err != nil {
- value = color.Faint("<unset>").String()
- } else {
- value = color.Cyan(value).String()
- }
- log.Printf("%s = %s", option, value)
+type Config struct {
+ Home string
+ createDirs bool
}
-func configDir(application string) string {
+func LoadConfig() (*Config, error) {
home := os.Getenv("VESPA_CLI_HOME")
if home == "" {
var err error
home, err = os.UserHomeDir()
if err != nil {
- fatalErr(err, "Could not determine configuration directory")
- return ""
+ return nil, err
}
+ home = filepath.Join(home, ".vespa")
}
- configDir := filepath.Join(home, ".vespa", application)
- if err := os.MkdirAll(configDir, 0755); err != nil {
- fatalErr(err, "Could not create config directory")
- return ""
+ if err := os.MkdirAll(home, 0700); err != nil {
+ return nil, err
}
- return configDir
+ c := &Config{Home: home, createDirs: true}
+ if err := c.load(); err != nil {
+ return nil, err
+ }
+ return c, nil
}
-func bindFlagToConfig(option string, command *cobra.Command) {
- flagToConfigBindings[option] = command
+func (c *Config) Write() error {
+ if err := os.MkdirAll(c.Home, 0700); err != nil {
+ return err
+ }
+ configFile := filepath.Join(c.Home, configName+"."+configType)
+ if !util.PathExists(configFile) {
+ if _, err := os.Create(configFile); err != nil {
+ return err
+ }
+ }
+ return viper.WriteConfig()
+}
+
+func (c *Config) CertificatePath(app vespa.ApplicationID) (string, error) {
+ return c.applicationFilePath(app, "data-plane-public-cert.pem")
+}
+
+func (c *Config) PrivateKeyPath(app vespa.ApplicationID) (string, error) {
+ return c.applicationFilePath(app, "data-plane-private-key.pem")
+}
+
+func (c *Config) APIKeyPath(tenantName string) string {
+ return filepath.Join(c.Home, tenantName+".api-key.pem")
+}
+
+func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) {
+ return ioutil.ReadFile(c.APIKeyPath(tenantName))
}
-func readConfig() {
- configDir := configDir("")
- if configDir == "" {
- return
+func (c *Config) ReadSessionID(app vespa.ApplicationID) (int64, error) {
+ sessionPath, err := c.applicationFilePath(app, "session_id")
+ if err != nil {
+ return 0, err
+ }
+ b, err := ioutil.ReadFile(sessionPath)
+ if err != nil {
+ return 0, err
}
+ return strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
+}
+
+func (c *Config) WriteSessionID(app vespa.ApplicationID, sessionID int64) error {
+ sessionPath, err := c.applicationFilePath(app, "session_id")
+ if err != nil {
+ return err
+ }
+ return ioutil.WriteFile(sessionPath, []byte(fmt.Sprintf("%d\n", sessionID)), 0600)
+}
+
+func (c *Config) applicationFilePath(app vespa.ApplicationID, name string) (string, error) {
+ appDir := filepath.Join(c.Home, app.String())
+ if c.createDirs {
+ if err := os.MkdirAll(appDir, 0700); err != nil {
+ return "", err
+ }
+ }
+ return filepath.Join(appDir, name), nil
+}
+
+func (c *Config) load() error {
viper.SetConfigName(configName)
viper.SetConfigType(configType)
- viper.AddConfigPath(configDir)
+ viper.AddConfigPath(c.Home)
viper.AutomaticEnv()
for option, command := range flagToConfigBindings {
viper.BindPFlag(option, command.PersistentFlags().Lookup(option))
}
err := viper.ReadInConfig()
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
- return // Fine
- }
- if err != nil {
- fatalErr(err, "Could not read configuration")
+ return nil
}
+ return err
}
-func getOption(option string) (string, error) {
+func (c *Config) Get(option string) (string, error) {
value := viper.GetString(option)
if value == "" {
return "", fmt.Errorf("no such option: %q", option)
@@ -134,7 +194,7 @@ func getOption(option string) (string, error) {
return value, nil
}
-func setOption(option, value string) error {
+func (c *Config) Set(option, value string) error {
switch option {
case targetFlag:
switch value {
@@ -162,29 +222,16 @@ func setOption(option, value string) error {
return fmt.Errorf("invalid option or value: %q: %q", option, value)
}
-func writeConfig() {
- configDir := configDir("")
- if configDir == "" {
- return
- }
-
- if !util.PathExists(configDir) {
- if err := os.MkdirAll(configDir, 0700); err != nil {
- fatalErr(err, "Could not create ", color.Cyan(configDir))
- return
- }
- }
-
- configFile := filepath.Join(configDir, configName+"."+configType)
- if !util.PathExists(configFile) {
- if _, err := os.Create(configFile); err != nil {
- fatalErr(err, "Could not create ", color.Cyan(configFile))
- return
- }
+func printOption(cfg *Config, option string) {
+ value, err := cfg.Get(option)
+ if err != nil {
+ value = color.Faint("<unset>").String()
+ } else {
+ value = color.Cyan(value).String()
}
+ log.Printf("%s = %s", option, value)
+}
- if err := viper.WriteConfig(); err != nil {
- fatalErr(err, "Could not write config")
- return
- }
+func bindFlagToConfig(option string, command *cobra.Command) {
+ flagToConfigBindings[option] = command
}
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index dee63bcb58f..07d165d58e0 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -7,24 +7,24 @@ import (
)
func TestConfig(t *testing.T) {
- configDir := t.TempDir()
- assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "foo", "bar"}}, t, nil))
- assert.Equal(t, "foo = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "foo"}}, t, nil))
- assert.Equal(t, "target = local\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
- assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "cloud"}}, t, nil))
- assert.Equal(t, "target = cloud\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
- assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "http://127.0.0.1:8080"}}, t, nil))
- assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "https://127.0.0.1"}}, t, nil))
- assert.Equal(t, "target = https://127.0.0.1\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil))
+ 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))
- assert.Equal(t, "invalid application: \"foo\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "application", "foo"}}, t, nil))
- assert.Equal(t, "application = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil))
- assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, nil))
- assert.Equal(t, "application = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil))
+ 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))
- assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get"}}, t, nil))
+ assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", execute(command{homeDir: homeDir, args: []string{"config", "get"}}, t, nil))
- assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "wait", "60"}}, t, nil))
- assert.Equal(t, "wait option must be an integer >= 0, got \"foo\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "wait", "foo"}}, t, nil))
- assert.Equal(t, "wait = 60\n", execute(command{configDir: configDir, args: []string{"config", "get", "wait"}}, t, nil))
+ 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))
}
diff --git a/client/go/cmd/curl.go b/client/go/cmd/curl.go
new file mode 100644
index 00000000000..4d949b51e8f
--- /dev/null
+++ b/client/go/cmd/curl.go
@@ -0,0 +1,143 @@
+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/vespa"
+)
+
+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")
+}
+
+var curlCmd = &cobra.Command{
+ Use: "curl [curl-options] path",
+ Short: "Query Vespa using curl",
+ Long: `Query Vespa using curl.
+
+Execute curl with the appropriate URL, certificate and private key for your application.`,
+ Example: `$ vespa curl /search/?yql=query
+$ vespa curl -- -v --data-urlencode "yql=select * from sources * where title contains 'foo';" /search/
+$ vespa curl -t local -- -v /search/?yql=query
+`,
+ DisableAutoGenTag: true,
+ Args: cobra.MinimumNArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
+ app, err := vespa.ApplicationFromString(getApplication())
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+ privateKeyFile, err := cfg.PrivateKeyPath(app)
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+ certificateFile, err := cfg.CertificatePath(app)
+ if err != nil {
+ fatalErr(err)
+ return
+ }
+ service := getService("query", 0)
+ c := &curl{privateKeyPath: privateKeyFile, certificatePath: 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...))
+ } else {
+ if err := c.run(service.BaseURL, args...); err != nil {
+ fatalErr(err, "Failed to run curl")
+ return
+ }
+ }
+ },
+}
+
+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)
+ }
+
+ 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
+}
diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go
new file mode 100644
index 00000000000..c3163e731ce
--- /dev/null
+++ b/client/go/cmd/curl_test.go
@@ -0,0 +1,53 @@
+package cmd
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+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)
+
+ expected := fmt.Sprintf("/usr/bin/curl -v --data-urlencode 'arg=with space' --key %s --cert %s 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.go b/client/go/cmd/deploy.go
index 19fa08ebaa4..d2836a6bcd1 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -6,12 +6,7 @@ package cmd
import (
"fmt"
- "io/ioutil"
"log"
- "os"
- "path/filepath"
- "strconv"
- "strings"
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/vespa"
@@ -51,6 +46,11 @@ If application directory is not specified, it defaults to working directory.`,
fatalErr(nil, err.Error())
return
}
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
target := getTarget()
opts := vespa.DeploymentOpts{ApplicationPackage: pkg, Target: target}
if opts.IsCloud() {
@@ -58,7 +58,11 @@ If application directory is not specified, it defaults to working directory.`,
if !opts.ApplicationPackage.HasCertificate() {
fatalErrHint(fmt.Errorf("Missing certificate in application package"), "Applications in Vespa Cloud require a certificate", "Try 'vespa cert'")
}
- opts.APIKey = readAPIKey(deployment.Application.Tenant)
+ opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
+ if err != nil {
+ fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ return
+ }
opts.Deployment = deployment
}
if sessionOrRunID, err := vespa.Deploy(opts); err == nil {
@@ -93,8 +97,9 @@ var prepareCmd = &cobra.Command{
fatalErr(err, "Could not find application package")
return
}
- configDir := configDir("default")
- if configDir == "" {
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
return
}
target := getTarget()
@@ -103,7 +108,10 @@ var prepareCmd = &cobra.Command{
Target: target,
})
if err == nil {
- writeSessionID(configDir, sessionID)
+ if err := cfg.WriteSessionID(vespa.DefaultApplication, sessionID); err != nil {
+ fatalErr(err, "Could not write session ID")
+ return
+ }
printSuccess("Prepared ", color.Cyan(pkg.Path), " with session ", sessionID)
} else {
fatalErr(nil, err.Error())
@@ -122,8 +130,16 @@ var activateCmd = &cobra.Command{
fatalErr(err, "Could not find application package")
return
}
- configDir := configDir("default")
- sessionID := readSessionID(configDir)
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return
+ }
+ sessionID, err := cfg.ReadSessionID(vespa.DefaultApplication)
+ if err != nil {
+ fatalErr(err, "Could not read session ID")
+ return
+ }
target := getTarget()
err = vespa.Activate(sessionID, vespa.DeploymentOpts{
ApplicationPackage: pkg,
@@ -144,26 +160,3 @@ func waitForQueryService(sessionOrRunID int64) {
waitForService("query", sessionOrRunID)
}
}
-
-func writeSessionID(appConfigDir string, sessionID int64) {
- if err := os.MkdirAll(appConfigDir, 0755); err != nil {
- fatalErr(err, "Could not create directory for session ID")
- }
- if err := ioutil.WriteFile(sessionIDFile(appConfigDir), []byte(fmt.Sprintf("%d\n", sessionID)), 0600); err != nil {
- fatalErr(err, "Could not write session ID")
- }
-}
-
-func readSessionID(appConfigDir string) int64 {
- b, err := ioutil.ReadFile(sessionIDFile(appConfigDir))
- if err != nil {
- fatalErr(err, "Could not read session ID")
- }
- id, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
- if err != nil {
- fatalErr(err, "Invalid session ID")
- }
- return id
-}
-
-func sessionIDFile(appConfigDir string) string { return filepath.Join(appConfigDir, "session_id") }
diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go
index ff85cd3d835..f24ba0829f9 100644
--- a/client/go/cmd/deploy_test.go
+++ b/client/go/cmd/deploy_test.go
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
func TestPrepareZip(t *testing.T) {
@@ -124,12 +125,14 @@ func assertPrepare(applicationPackage string, arguments []string, t *testing.T)
func assertActivate(applicationPackage string, arguments []string, t *testing.T) {
client := &mockHttpClient{}
- configDir := t.TempDir()
- appConfigDir := filepath.Join(configDir, ".vespa", "default")
- writeSessionID(appConfigDir, 42)
+ homeDir := t.TempDir()
+ cfg := Config{Home: filepath.Join(homeDir, ".vespa"), createDirs: true}
+ if err := cfg.WriteSessionID(vespa.DefaultApplication, 42); err != nil {
+ t.Fatal(err)
+ }
assert.Equal(t,
"Success: Activated "+applicationPackage+" with session 42\n",
- execute(command{args: arguments, configDir: configDir}, t, client))
+ execute(command{args: arguments, homeDir: homeDir}, t, client))
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/helpers.go b/client/go/cmd/helpers.go
index b672419cae6..14699abf40e 100644
--- a/client/go/cmd/helpers.go
+++ b/client/go/cmd/helpers.go
@@ -10,7 +10,6 @@ import (
"io/ioutil"
"log"
"os"
- "path/filepath"
"strings"
"time"
@@ -51,16 +50,6 @@ func printSuccess(msg ...interface{}) {
log.Print(color.Green("Success: "), fmt.Sprint(msg...))
}
-func readAPIKey(tenant string) []byte {
- configDir := configDir("")
- apiKeyPath := filepath.Join(configDir, tenant+".api-key.pem")
- key, err := ioutil.ReadFile(apiKeyPath)
- if err != nil {
- fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
- }
- return key
-}
-
func deploymentFromArgs() vespa.Deployment {
zone, err := vespa.ZoneFromString(zoneArg)
if err != nil {
@@ -81,7 +70,12 @@ func applicationSource(args []string) string {
}
func getApplication() string {
- app, err := getOption(applicationFlag)
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return ""
+ }
+ app, err := cfg.Get(applicationFlag)
if err != nil {
fatalErr(err, "A valid application must be specified")
}
@@ -89,7 +83,12 @@ func getApplication() string {
}
func getTargetType() string {
- target, err := getOption(targetFlag)
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return ""
+ }
+ target, err := cfg.Get(targetFlag)
if err != nil {
fatalErr(err, "A valid target must be specified")
}
@@ -122,10 +121,25 @@ func getTarget() vespa.Target {
return vespa.LocalTarget()
case "cloud":
deployment := deploymentFromArgs()
- apiKey := readAPIKey(deployment.Application.Tenant)
- configDir := configDir(deployment.Application.String())
- privateKeyFile := filepath.Join(configDir, "data-plane-private-key.pem")
- certificateFile := filepath.Join(configDir, "data-plane-public-cert.pem")
+ cfg, err := LoadConfig()
+ if err != nil {
+ fatalErr(err, "Could not load config")
+ return nil
+ }
+ apiKey, err := ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant))
+ if err != nil {
+ fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ }
+ privateKeyFile, err := cfg.PrivateKeyPath(deployment.Application)
+ if err != nil {
+ fatalErr(err)
+ return nil
+ }
+ certificateFile, err := cfg.CertificatePath(deployment.Application)
+ if err != nil {
+ fatalErr(err)
+ return nil
+ }
kp, err := tls.LoadX509KeyPair(certificateFile, privateKeyFile)
if err != nil {
fatalErr(err, "Could not read key pair")
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go
index fde7d6edb5a..f8bf87b508c 100644
--- a/client/go/cmd/root.go
+++ b/client/go/cmd/root.go
@@ -49,7 +49,6 @@ func configureLogger() {
func init() {
configureLogger()
- cobra.OnInitialize(readConfig)
rootCmd.PersistentFlags().StringVarP(&targetArg, targetFlag, "t", "local", "The name or URL of the recipient of this command")
rootCmd.PersistentFlags().StringVarP(&applicationArg, applicationFlag, "a", "", "The application to manage")
rootCmd.PersistentFlags().IntVarP(&waitSecsArg, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready")
diff --git a/client/go/go.mod b/client/go/go.mod
index 893add7218b..509eb273c6c 100644
--- a/client/go/go.mod
+++ b/client/go/go.mod
@@ -3,6 +3,7 @@ module github.com/vespa-engine/vespa/client/go
go 1.15
require (
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-isatty v0.0.3
diff --git a/client/go/go.sum b/client/go/go.sum
index 826f137d5e2..97328690ee5 100644
--- a/client/go/go.sum
+++ b/client/go/go.sum
@@ -170,6 +170,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go
index 081e9fc17d2..22ab5380c23 100644
--- a/client/go/vespa/deploy.go
+++ b/client/go/vespa/deploy.go
@@ -23,6 +23,8 @@ import (
"github.com/vespa-engine/vespa/client/go/util"
)
+var DefaultApplication = ApplicationID{Tenant: "default", Application: "application", Instance: "default"}
+
type ApplicationID struct {
Tenant string
Application string