diff options
author | Jon Bratseth <bratseth@oath.com> | 2021-09-13 09:07:42 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-13 09:07:42 +0200 |
commit | 9420cc0c5873d92fd20c9ab99bc54a23001d606a (patch) | |
tree | 798b52b641bcb1b872db48c52c9680213e28b34e | |
parent | d83a5c6e35b106ccbfb7ea0a41cf1b8749bd28ac (diff) | |
parent | edb94f27688e030d61ac3286797bbbb163624ae3 (diff) |
Merge pull request #19068 from vespa-engine/mpolden/vespa-curl
vespa curl
-rw-r--r-- | client/go/cmd/api_key.go | 12 | ||||
-rw-r--r-- | client/go/cmd/api_key_test.go | 8 | ||||
-rw-r--r-- | client/go/cmd/cert.go | 24 | ||||
-rw-r--r-- | client/go/cmd/cert_test.go | 17 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 13 | ||||
-rw-r--r-- | client/go/cmd/config.go | 163 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 34 | ||||
-rw-r--r-- | client/go/cmd/curl.go | 143 | ||||
-rw-r--r-- | client/go/cmd/curl_test.go | 53 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 61 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 11 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 48 | ||||
-rw-r--r-- | client/go/cmd/root.go | 1 | ||||
-rw-r--r-- | client/go/go.mod | 1 | ||||
-rw-r--r-- | client/go/go.sum | 2 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 2 |
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 |