summaryrefslogtreecommitdiffstats
path: root/client/go/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'client/go/cmd')
-rw-r--r--client/go/cmd/api_key.go69
-rw-r--r--client/go/cmd/api_key_test.go2
-rw-r--r--client/go/cmd/auth.go17
-rw-r--r--client/go/cmd/cert.go92
-rw-r--r--client/go/cmd/cert_test.go4
-rw-r--r--client/go/cmd/clone.go115
-rw-r--r--client/go/cmd/clone_test.go7
-rw-r--r--client/go/cmd/command_tester.go5
-rw-r--r--client/go/cmd/config.go80
-rw-r--r--client/go/cmd/config_test.go6
-rw-r--r--client/go/cmd/curl.go89
-rw-r--r--client/go/cmd/curl_test.go4
-rw-r--r--client/go/cmd/deploy.go115
-rw-r--r--client/go/cmd/deploy_test.go4
-rw-r--r--client/go/cmd/document.go60
-rw-r--r--client/go/cmd/document_test.go18
-rw-r--r--client/go/cmd/helpers.go182
-rw-r--r--client/go/cmd/log.go30
-rw-r--r--client/go/cmd/log_test.go2
-rw-r--r--client/go/cmd/login.go3
-rw-r--r--client/go/cmd/man.go9
-rw-r--r--client/go/cmd/prod.go152
-rw-r--r--client/go/cmd/prod_test.go6
-rw-r--r--client/go/cmd/query.go38
-rw-r--r--client/go/cmd/query_test.go25
-rw-r--r--client/go/cmd/root.go47
-rw-r--r--client/go/cmd/status.go20
-rw-r--r--client/go/cmd/status_test.go2
-rw-r--r--client/go/cmd/test.go71
-rw-r--r--client/go/cmd/test_test.go11
-rw-r--r--client/go/cmd/testdata/sample-apps-master.zipbin4253469 -> 4653209 bytes
-rw-r--r--client/go/cmd/version.go8
-rw-r--r--client/go/cmd/vespa/main.go9
33 files changed, 785 insertions, 517 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go
index 032d98c96fe..faab67781d1 100644
--- a/client/go/cmd/api_key.go
+++ b/client/go/cmd/api_key.go
@@ -15,13 +15,31 @@ import (
var overwriteKey bool
+const apiKeyLongDoc = `Create a new user API key for authentication with Vespa Cloud.
+
+The API key will be stored in the Vespa CLI home directory
+(see 'vespa help config'). Other commands will then automatically load the API
+key as necessary.
+
+It's possible to override the API key used through environment variables. This
+can be useful in continuous integration systems.
+
+* VESPA_CLI_API_KEY containing the key directly:
+
+ export VESPA_CLI_API_KEY="my api key"
+
+* VESPA_CLI_API_KEY_FILE containing path to the key:
+
+ export VESPA_CLI_API_KEY_FILE=/path/to/api-key
+
+Note that when overriding API key through environment variables, that key will
+always be used. It's not possible to specify a tenant-specific key.`
+
func init() {
apiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key")
apiKeyCmd.MarkPersistentFlagRequired(applicationFlag)
}
-var example string
-
func apiKeyExample() string {
if vespa.Auth0AccessTokenEnabled() {
return "$ vespa auth api-key -a my-tenant.my-app.my-instance"
@@ -33,40 +51,46 @@ func apiKeyExample() string {
var apiKeyCmd = &cobra.Command{
Use: "api-key",
Short: "Create a new user API key for authentication with Vespa Cloud",
+ Long: apiKeyLongDoc,
Example: apiKeyExample(),
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
- Run: doApiKey,
+ RunE: doApiKey,
}
var deprecatedApiKeyCmd = &cobra.Command{
Use: "api-key",
Short: "Create a new user API key for authentication with Vespa Cloud",
+ Long: apiKeyLongDoc,
Example: apiKeyExample(),
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
Hidden: true,
Deprecated: "use 'vespa auth api-key' instead",
- Run: doApiKey,
+ RunE: doApiKey,
}
-func doApiKey(_ *cobra.Command, _ []string) {
+func doApiKey(_ *cobra.Command, _ []string) error {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return fmt.Errorf("could not load config: %w", err)
+ }
+ app, err := getApplication()
+ if err != nil {
+ return err
}
- app := getApplication()
apiKeyFile := cfg.APIKeyPath(app.Tenant)
if util.PathExists(apiKeyFile) && !overwriteKey {
- printErrHint(fmt.Errorf("File %s already exists", apiKeyFile), "Use -f to overwrite it")
+ err := fmt.Errorf("refusing to overwrite %s", apiKeyFile)
+ printErrHint(err, "Use -f to overwrite it")
printPublicKey(apiKeyFile, app.Tenant)
- return
+ return ErrCLI{error: err, quiet: true}
}
apiKey, err := vespa.CreateAPIKey()
if err != nil {
- fatalErr(err, "Could not create API key")
- return
+ return fmt.Errorf("could not create api key: %w", err)
}
if err := ioutil.WriteFile(apiKeyFile, apiKey, 0600); err == nil {
printSuccess("API private key written to ", apiKeyFile)
@@ -74,41 +98,40 @@ func doApiKey(_ *cobra.Command, _ []string) {
if vespa.Auth0AccessTokenEnabled() {
if err == nil {
if err := cfg.Set(cloudAuthFlag, "api-key"); err != nil {
- fatalErr(err, "Could not write config")
+ return fmt.Errorf("could not write config: %w", err)
}
if err := cfg.Write(); err != nil {
- fatalErr(err)
+ return err
}
}
}
+ return nil
} else {
- fatalErr(err, "Failed to write ", apiKeyFile)
+ return fmt.Errorf("failed to write: %s: %w", apiKeyFile, err)
}
}
-func printPublicKey(apiKeyFile, tenant string) {
+func printPublicKey(apiKeyFile, tenant string) error {
pemKeyData, err := ioutil.ReadFile(apiKeyFile)
if err != nil {
- fatalErr(err, "Failed to read ", apiKeyFile)
- return
+ return fmt.Errorf("failed to read: %s: %w", apiKeyFile, err)
}
key, err := vespa.ECPrivateKeyFrom(pemKeyData)
if err != nil {
- fatalErr(err, "Failed to load key")
- return
+ return fmt.Errorf("failed to load key: %w", err)
}
pemPublicKey, err := vespa.PEMPublicKeyFrom(key)
if err != nil {
- fatalErr(err, "Failed to extract public key")
- return
+ return fmt.Errorf("failed to extract public key: %w", err)
}
fingerprint, err := vespa.FingerprintMD5(pemPublicKey)
if err != nil {
- fatalErr(err, "Failed to extract fingerprint")
+ return fmt.Errorf("failed to extract fingerprint: %w", err)
}
log.Printf("\nThis is your public key:\n%s", color.Green(pemPublicKey))
log.Printf("Its fingerprint is:\n%s\n", color.Cyan(fingerprint))
log.Print("\nTo use this key in Vespa Cloud click 'Add custom key' at")
log.Printf(color.Cyan("%s/tenant/%s/keys").String(), getConsoleURL(), tenant)
log.Print("and paste the entire public key including the BEGIN and END lines.")
+ return nil
}
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go
index b08758ae21d..df3cf144180 100644
--- a/client/go/cmd/api_key_test.go
+++ b/client/go/cmd/api_key_test.go
@@ -18,6 +18,6 @@ func TestAPIKey(t *testing.T) {
assert.Contains(t, out, "Success: API private key written to "+keyFile+"\n")
out, outErr := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n")
+ assert.Contains(t, outErr, "Error: refusing to overwrite "+keyFile+"\nHint: Use -f to overwrite it\n")
assert.Contains(t, out, "This is your public key")
}
diff --git a/client/go/cmd/auth.go b/client/go/cmd/auth.go
index 9322f8d0808..68d7fa00fdf 100644
--- a/client/go/cmd/auth.go
+++ b/client/go/cmd/auth.go
@@ -1,6 +1,8 @@
package cmd
import (
+ "fmt"
+
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/vespa"
)
@@ -21,14 +23,13 @@ func init() {
}
var authCmd = &cobra.Command{
- Use: "auth",
- Short: "Manage Vespa Cloud credentials",
- Long: `Manage Vespa Cloud credentials.`,
-
+ Use: "auth",
+ Short: "Manage Vespa Cloud credentials",
+ Long: `Manage Vespa Cloud credentials.`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
- // Root command does nothing
- cmd.Help()
- exitFunc(1)
+ SilenceUsage: false,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
},
}
diff --git a/client/go/cmd/cert.go b/client/go/cmd/cert.go
index e79a45d3af8..f338a1ba81a 100644
--- a/client/go/cmd/cert.go
+++ b/client/go/cmd/cert.go
@@ -5,15 +5,41 @@ package cmd
import (
"fmt"
+ "os"
+ "path/filepath"
+
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/vespa"
- "os"
- "path/filepath"
)
var overwriteCertificate bool
+const longDoc = `Create a new private key and self-signed certificate for Vespa Cloud deployment.
+
+The private key and certificate will be stored in the Vespa CLI home directory
+(see 'vespa help config'). Other commands will then automatically load the
+certificate as necessary.
+
+It's possible to override the private key and certificate used through
+environment variables. This can be useful in continuous integration systems.
+
+* VESPA_CLI_DATA_PLANE_CERT and VESPA_CLI_DATA_PLANE_KEY containing the
+ certificate and private key directly:
+
+ export VESPA_CLI_DATA_PLANE_CERT="my cert"
+ export VESPA_CLI_DATA_PLANE_KEY="my private key"
+
+* VESPA_CLI_DATA_PLANE_CERT_FILE and VESPA_CLI_DATA_PLANE_KEY_FILE containing
+ paths to the certificate and private key:
+
+ export VESPA_CLI_DATA_PLANE_CERT_FILE=/path/to/cert
+ export VESPA_CLI_DATA_PLANE_KEY_FILE=/path/to/key
+
+Note that when overriding key pair through environment variables, that key pair
+will always be used for all applications. It's not possible to specify an
+application-specific key.`
+
func init() {
certCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key")
certCmd.MarkPersistentFlagRequired(applicationFlag)
@@ -30,96 +56,90 @@ func certExample() string {
var certCmd = &cobra.Command{
Use: "cert",
Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment",
+ Long: longDoc,
Example: certExample(),
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
- Run: doCert,
+ RunE: doCert,
}
var deprecatedCertCmd = &cobra.Command{
Use: "cert",
Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment",
+ Long: longDoc,
Example: "$ vespa cert -a my-tenant.my-app.my-instance",
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
Deprecated: "use 'vespa auth cert' instead",
Hidden: true,
- Run: doCert,
+ RunE: doCert,
}
-func doCert(_ *cobra.Command, args []string) {
- app := getApplication()
+func doCert(_ *cobra.Command, args []string) error {
+ app, err := getApplication()
+ if err != nil {
+ return err
+ }
pkg, err := vespa.FindApplicationPackage(applicationSource(args), false)
if err != nil {
- fatalErr(err)
- return
+ return err
}
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err)
- return
+ return err
}
privateKeyFile, err := cfg.PrivateKeyPath(app)
if err != nil {
- fatalErr(err)
- return
+ return err
}
certificateFile, err := cfg.CertificatePath(app)
if err != nil {
- fatalErr(err)
- return
+ return err
}
if !overwriteCertificate {
hint := "Use -f flag to force overwriting"
if pkg.HasCertificate() {
- fatalErrHint(fmt.Errorf("Application package %s already contains a certificate", pkg.Path), hint)
- return
+ return errHint(fmt.Errorf("application package %s already contains a certificate", pkg.Path), hint)
}
if util.PathExists(privateKeyFile) {
- fatalErrHint(fmt.Errorf("Private key %s already exists", color.Cyan(privateKeyFile)), hint)
- return
+ return errHint(fmt.Errorf("private key %s already exists", color.Cyan(privateKeyFile)), hint)
}
if util.PathExists(certificateFile) {
- fatalErrHint(fmt.Errorf("Certificate %s already exists", color.Cyan(certificateFile)), hint)
- return
+ return errHint(fmt.Errorf("certificate %s already exists", color.Cyan(certificateFile)), hint)
}
}
if pkg.IsZip() {
- var msg string
+ var hint string
if vespa.Auth0AccessTokenEnabled() {
- msg = "Try running 'mvn clean' before 'vespa auth cert', and then 'mvn package'"
+ hint = "Try running 'mvn clean' before 'vespa auth cert', and then 'mvn package'"
} else {
- msg = "Try running 'mvn clean' before 'vespa cert', and then 'mvn package'"
+ hint = "Try running 'mvn clean' before 'vespa cert', and then 'mvn package'"
}
- fatalErrHint(fmt.Errorf("Cannot add certificate to compressed application package %s", pkg.Path),
- msg)
- return
+ return errHint(fmt.Errorf("cannot add certificate to compressed application package %s", pkg.Path), hint)
}
keyPair, err := vespa.CreateKeyPair()
if err != nil {
- fatalErr(err, "Could not create key pair")
- return
+ return err
}
pkgCertificateFile := filepath.Join(pkg.Path, "security", "clients.pem")
if err := os.MkdirAll(filepath.Dir(pkgCertificateFile), 0755); err != nil {
- fatalErr(err, "Could not create security directory")
- return
+ return fmt.Errorf("could not create security directory: %w", err)
}
if err := keyPair.WriteCertificateFile(pkgCertificateFile, overwriteCertificate); err != nil {
- fatalErr(err, "Could not write certificate")
- return
+ return fmt.Errorf("could not write certificate to application package: %w", err)
}
if err := keyPair.WriteCertificateFile(certificateFile, overwriteCertificate); err != nil {
- fatalErr(err, "Could not write certificate")
- return
+ return fmt.Errorf("could not write certificate: %w", err)
}
if err := keyPair.WritePrivateKeyFile(privateKeyFile, overwriteCertificate); err != nil {
- fatalErr(err, "Could not write private key")
- return
+ return fmt.Errorf("could not write private key: %w", err)
}
printSuccess("Certificate written to ", color.Cyan(pkgCertificateFile))
printSuccess("Certificate written to ", color.Cyan(certificateFile))
printSuccess("Private key written to ", color.Cyan(privateKeyFile))
+ return nil
}
diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go
index 96b626b5c98..aae40db43a6 100644
--- a/client/go/cmd/cert_test.go
+++ b/client/go/cmd/cert_test.go
@@ -29,7 +29,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)
_, outErr := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, fmt.Sprintf("Error: Application package %s already contains a certificate", appDir))
+ assert.Contains(t, outErr, fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
}
func TestCertCompressedPackage(t *testing.T) {
@@ -42,7 +42,7 @@ func TestCertCompressedPackage(t *testing.T) {
assert.Nil(t, err)
_, outErr := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, "Error: Cannot add certificate to compressed application package")
+ assert.Contains(t, outErr, "Error: cannot add certificate to compressed application package")
err = os.Remove(zipFile)
assert.Nil(t, err)
diff --git a/client/go/cmd/clone.go b/client/go/cmd/clone.go
index 6fe3c0d5a29..7a25613947a 100644
--- a/client/go/cmd/clone.go
+++ b/client/go/cmd/clone.go
@@ -45,62 +45,61 @@ directory can be overriden by setting the VESPA_CLI_CACHE_DIR environment
variable.`,
Example: "$ vespa clone vespa-cloud/album-recommendation my-app",
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
if listApps {
apps, err := listSampleApps()
if err != nil {
- fatalErr(err, "Could not list sample applications")
- return
+ return fmt.Errorf("could not list sample applications: %w", err)
}
for _, app := range apps {
log.Print(app)
}
- } else {
- if len(args) != 2 {
- fatalErr(nil, "Expected exactly 2 arguments")
- return
- }
- cloneApplication(args[0], args[1])
+ return nil
+ }
+ if len(args) != 2 {
+ return fmt.Errorf("expected exactly 2 arguments, got %d", len(args))
}
+ return cloneApplication(args[0], args[1])
},
}
-func cloneApplication(source string, name string) {
- zipFile := getSampleAppsZip()
+func cloneApplication(applicationName string, applicationDir string) error {
+ zipFile, err := getSampleAppsZip()
+ if err != nil {
+ return err
+ }
defer zipFile.Close()
- zipReader, zipOpenError := zip.OpenReader(zipFile.Name())
- if zipOpenError != nil {
- fatalErr(zipOpenError, "Could not open sample apps zip '", color.Cyan(zipFile.Name()), "'")
- return
+ r, err := zip.OpenReader(zipFile.Name())
+ if err != nil {
+ return fmt.Errorf("could not open sample apps zip '%s': %w", color.Cyan(zipFile.Name()), err)
}
- defer zipReader.Close()
+ defer r.Close()
found := false
- for _, f := range zipReader.File {
- zipEntryPrefix := "sample-apps-master/" + source + "/"
- if strings.HasPrefix(f.Name, zipEntryPrefix) {
+ for _, f := range r.File {
+ dirPrefix := "sample-apps-master/" + applicationName + "/"
+ if strings.HasPrefix(f.Name, dirPrefix) {
if !found { // Create destination directory lazily when source is found
- createErr := os.Mkdir(name, 0755)
+ createErr := os.Mkdir(applicationDir, 0755)
if createErr != nil {
- fatalErr(createErr, "Could not create directory '", color.Cyan(name), "'")
- return
+ return fmt.Errorf("could not create directory '%s': %w", color.Cyan(applicationDir), createErr)
}
}
found = true
- copyError := copy(f, name, zipEntryPrefix)
- if copyError != nil {
- fatalErr(copyError, "Could not copy zip entry '", color.Cyan(f.Name), "' to ", color.Cyan(name))
- return
+ if err := copy(f, applicationDir, dirPrefix); err != nil {
+ return fmt.Errorf("could not copy zip entry '%s': %w", color.Cyan(f.Name), err)
}
}
}
if !found {
- fatalErrHint(fmt.Errorf("Could not find source application '%s'", color.Cyan(source)), "Use -f to ignore the cache")
+ return errHint(fmt.Errorf("could not find source application '%s'", color.Cyan(applicationName)), "Use -f to ignore the cache")
} else {
- log.Print("Created ", color.Cyan(name))
+ log.Print("Created ", color.Cyan(applicationDir))
}
+ return nil
}
func openOutputFile() (*os.File, error) {
@@ -126,72 +125,66 @@ func useCache(cacheFile *os.File) (bool, error) {
return stat.Size() > 0 && time.Now().Before(expiry), nil
}
-func getSampleAppsZip() *os.File {
+func getSampleAppsZip() (*os.File, error) {
f, err := openOutputFile()
if err != nil {
- fatalErr(err, "Could not determine location of cache file")
- return nil
+ return nil, fmt.Errorf("could not determine location of cache file: %w", err)
}
useCache, err := useCache(f)
if err != nil {
- fatalErr(err, "Could not determine cache status", "Try ignoring the cache with the -f flag")
- return nil
+ return nil, errHint(fmt.Errorf("could not determine cache status: %w", err), "Try ignoring the cache with the -f flag")
}
if useCache {
log.Print(color.Yellow("Using cached sample apps ..."))
- return f
+ return f, nil
}
-
err = util.Spinner(color.Yellow("Downloading sample apps ...").String(), func() error {
request, err := http.NewRequest("GET", "https://github.com/vespa-engine/sample-apps/archive/refs/heads/master.zip", nil)
if err != nil {
- fatalErr(err, "Invalid URL")
- return nil
+ return fmt.Errorf("invalid url: %w", err)
}
response, err := util.HttpDo(request, time.Minute*60, "GitHub")
if err != nil {
- fatalErr(err, "Could not download sample apps from GitHub")
- return nil
+ return fmt.Errorf("could not download sample apps: %w", err)
}
defer response.Body.Close()
if response.StatusCode != 200 {
- fatalErr(nil, "Could not download sample apps from GitHub: ", response.StatusCode)
- return nil
+ return fmt.Errorf("could not download sample apps: github returned status %d", response.StatusCode)
+ }
+ if err := f.Truncate(0); err != nil {
+ return fmt.Errorf("could not truncate sample apps file: %s: %w", f.Name(), err)
}
if _, err := io.Copy(f, response.Body); err != nil {
- fatalErr(err, "Could not write sample apps to file: ", f.Name())
- return nil
+ return fmt.Errorf("could not write sample apps to file: %s: %w", f.Name(), err)
}
- return err
+ return nil
})
-
- return f
+ return f, err
}
func copy(f *zip.File, destinationDir string, zipEntryPrefix string) error {
destinationPath := filepath.Join(destinationDir, filepath.FromSlash(strings.TrimPrefix(f.Name, zipEntryPrefix)))
if strings.HasSuffix(f.Name, "/") {
if f.Name != zipEntryPrefix { // root is already created
- createError := os.Mkdir(destinationPath, 0755)
- if createError != nil {
- return createError
+ if err := os.Mkdir(destinationPath, 0755); err != nil {
+ return err
}
}
} else {
- zipEntry, zipEntryOpenError := f.Open()
- if zipEntryOpenError != nil {
- return zipEntryOpenError
+ r, err := f.Open()
+ if err != nil {
+ return err
}
- defer zipEntry.Close()
-
- destination, createError := os.Create(destinationPath)
- if createError != nil {
- return createError
+ defer r.Close()
+ destination, err := os.Create(destinationPath)
+ if err != nil {
+ return err
}
-
- _, copyError := io.Copy(destination, zipEntry)
- if copyError != nil {
- return copyError
+ if _, err := io.Copy(destination, r); err != nil {
+ return err
+ }
+ if err := os.Chmod(destinationPath, f.Mode()); err != nil {
+ return err
}
}
return nil
diff --git a/client/go/cmd/clone_test.go b/client/go/cmd/clone_test.go
index 054dc7b21fb..af8b686b111 100644
--- a/client/go/cmd/clone_test.go
+++ b/client/go/cmd/clone_test.go
@@ -15,7 +15,7 @@ import (
)
func TestClone(t *testing.T) {
- assertCreated("album-recommendation-selfhosted", "mytestapp", t)
+ assertCreated("text-search", "mytestapp", t)
}
func assertCreated(sampleAppName string, app string, t *testing.T) {
@@ -32,6 +32,9 @@ func assertCreated(sampleAppName string, app string, t *testing.T) {
assert.True(t, util.IsDirectory(filepath.Join(app, "src", "main", "application")))
servicesStat, _ := os.Stat(filepath.Join(app, "src", "main", "application", "services.xml"))
- servicesSize := int64(2474)
+ servicesSize := int64(1772)
assert.Equal(t, servicesSize, servicesStat.Size())
+
+ scriptStat, _ := os.Stat(filepath.Join(app, "bin", "convert-msmarco.sh"))
+ assert.Equal(t, os.FileMode(0755), scriptStat.Mode())
}
diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go
index eb55021b536..135dda895d4 100644
--- a/client/go/cmd/command_tester.go
+++ b/client/go/cmd/command_tester.go
@@ -63,9 +63,6 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) (string, string)
rootCmd.Flags().VisitAll(resetFlag)
documentCmd.Flags().VisitAll(resetFlag)
- // Do not exit in tests
- exitFunc = func(code int) {}
-
// Capture stdout and execute command
var capturedOut bytes.Buffer
var capturedErr bytes.Buffer
@@ -79,7 +76,7 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) (string, string)
// Execute command and return output
rootCmd.SetArgs(append(cmd.args, cmd.moreArgs...))
- rootCmd.Execute()
+ Execute()
return capturedOut.String(), capturedErr.String()
}
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index 3a6e43e7ffe..a195a3e1975 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -5,6 +5,7 @@
package cmd
import (
+ "crypto/tls"
"fmt"
"io/ioutil"
"log"
@@ -45,10 +46,10 @@ instead.
Configuration is written to $HOME/.vespa by default. This path can be
overridden by setting the VESPA_CLI_HOME environment variable.`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
- // Root command does nothing
- cmd.Help()
- exitFunc(1)
+ SilenceUsage: false,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
},
}
@@ -57,36 +58,33 @@ var setConfigCmd = &cobra.Command{
Short: "Set a configuration option.",
Example: "$ vespa config set target cloud",
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(2),
- Run: func(cmd *cobra.Command, args []string) {
+ RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
}
if err := cfg.Set(args[0], args[1]); err != nil {
- fatalErr(err)
- } else {
- if err := cfg.Write(); err != nil {
- fatalErr(err)
- }
+ return err
}
+ return cfg.Write()
},
}
var getConfigCmd = &cobra.Command{
- Use: "get option-name",
- Short: "Get a configuration option",
- Example: "$ vespa config get target",
+ Use: "get [option-name]",
+ Short: "Show given configuration option, or all configuration options",
+ Example: `$ vespa config get
+$ vespa config get target`,
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
}
-
if len(args) == 0 { // Print all values
var flags []string
for flag := range flagToConfigBindings {
@@ -99,6 +97,7 @@ var getConfigCmd = &cobra.Command{
} else {
printOption(cfg, args[0])
}
+ return nil
},
}
@@ -107,14 +106,20 @@ type Config struct {
createDirs bool
}
+type KeyPair struct {
+ KeyPair tls.Certificate
+ CertificateFile string
+ PrivateKeyFile string
+}
+
func LoadConfig() (*Config, error) {
home, err := vespaCliHome()
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not detect config directory: %w", err)
}
c := &Config{Home: home, createDirs: true}
if err := c.load(); err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not load config: %w", err)
}
return c, nil
}
@@ -146,11 +151,44 @@ func (c *Config) PrivateKeyPath(app vespa.ApplicationID) (string, error) {
return c.applicationFilePath(app, "data-plane-private-key.pem")
}
+func (c *Config) X509KeyPair(app vespa.ApplicationID) (KeyPair, error) {
+ cert, certOk := os.LookupEnv("VESPA_CLI_DATA_PLANE_CERT")
+ key, keyOk := os.LookupEnv("VESPA_CLI_DATA_PLANE_KEY")
+ if certOk && keyOk {
+ // Use key pair from environment
+ kp, err := tls.X509KeyPair([]byte(cert), []byte(key))
+ return KeyPair{KeyPair: kp}, err
+ }
+ privateKeyFile, err := c.PrivateKeyPath(app)
+ if err != nil {
+ return KeyPair{}, err
+ }
+ certificateFile, err := c.CertificatePath(app)
+ if err != nil {
+ return KeyPair{}, err
+ }
+ kp, err := tls.LoadX509KeyPair(certificateFile, privateKeyFile)
+ if err != nil {
+ return KeyPair{}, err
+ }
+ return KeyPair{
+ KeyPair: kp,
+ CertificateFile: certificateFile,
+ PrivateKeyFile: privateKeyFile,
+ }, nil
+}
+
func (c *Config) APIKeyPath(tenantName string) string {
+ if override, ok := os.LookupEnv("VESPA_CLI_API_KEY_FILE"); ok {
+ return override
+ }
return filepath.Join(c.Home, tenantName+".api-key.pem")
}
func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) {
+ if override, ok := os.LookupEnv("VESPA_CLI_API_KEY"); ok {
+ return []byte(override), nil
+ }
return ioutil.ReadFile(c.APIKeyPath(tenantName))
}
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index 0e74e53c5e5..ba0df781715 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -10,7 +10,7 @@ import (
func TestConfig(t *testing.T) {
homeDir := filepath.Join(t.TempDir(), ".vespa")
- assertConfigCommandErr(t, "invalid option or value: \"foo\": \"bar\"\n", homeDir, "config", "set", "foo", "bar")
+ assertConfigCommandErr(t, "Error: 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")
@@ -19,7 +19,7 @@ func TestConfig(t *testing.T) {
assertConfigCommand(t, "", homeDir, "config", "set", "target", "https://127.0.0.1")
assertConfigCommand(t, "target = https://127.0.0.1\n", homeDir, "config", "get", "target")
- assertConfigCommandErr(t, "invalid application: \"foo\"\n", homeDir, "config", "set", "application", "foo")
+ assertConfigCommandErr(t, "Error: 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")
@@ -27,7 +27,7 @@ func TestConfig(t *testing.T) {
assertConfigCommand(t, "application = t1.a1.i1\ncolor = auto\nquiet = false\ntarget = https://127.0.0.1\nwait = 0\n", homeDir, "config", "get")
assertConfigCommand(t, "", homeDir, "config", "set", "wait", "60")
- assertConfigCommandErr(t, "wait option must be an integer >= 0, got \"foo\"\n", homeDir, "config", "set", "wait", "foo")
+ assertConfigCommandErr(t, "Error: wait option must be an integer >= 0, got \"foo\"\n", homeDir, "config", "set", "wait", "foo")
assertConfigCommand(t, "wait = 60\n", homeDir, "config", "get", "wait")
}
diff --git a/client/go/cmd/curl.go b/client/go/cmd/curl.go
index 2496ddc3abc..47d0dcde95b 100644
--- a/client/go/cmd/curl.go
+++ b/client/go/cmd/curl.go
@@ -2,72 +2,115 @@
package cmd
import (
+ "errors"
+ "fmt"
"log"
"os"
"strings"
"github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/auth0"
"github.com/vespa-engine/vespa/client/go/curl"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
var curlDryRun bool
+var curlService string
func init() {
rootCmd.AddCommand(curlCmd)
curlCmd.Flags().BoolVarP(&curlDryRun, "dry-run", "n", false, "Print the curl command that would be executed")
+ curlCmd.Flags().StringVarP(&curlService, "service", "s", "query", "Which service to query. Must be \"deploy\", \"document\" or \"query\"")
}
var curlCmd = &cobra.Command{
Use: "curl [curl-options] path",
- Short: "Query Vespa using curl",
- Long: `Query Vespa using curl.
+ Short: "Access Vespa directly using curl",
+ Long: `Access Vespa directly 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
+Execute curl with the appropriate URL, certificate and private key for your application.
+
+For a more high-level interface to query and feeding, see the 'query' and 'document' commands.
`,
+ Example: `$ vespa curl /ApplicationStatus
+$ vespa curl -- -X POST -H "Content-Type:application/json" --data-binary @src/test/resources/A-Head-Full-of-Dreams.json /document/v1/namespace/music/docid/1
+$ vespa curl -- -v --data-urlencode "yql=select * from music where album contains 'head';" /search/\?hits=5`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MinimumNArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
+ RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
}
- app := getApplication()
- privateKeyFile, err := cfg.PrivateKeyPath(app)
+ app, err := getApplication()
if err != nil {
- fatalErr(err)
- return
+ return err
}
- certificateFile, err := cfg.CertificatePath(app)
+ service, err := getService(curlService, 0, "")
if err != nil {
- fatalErr(err)
- return
+ return err
}
- service := getService("query", 0, "")
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
+ return err
+ }
+ switch curlService {
+ case "deploy":
+ t, err := getTarget()
+ if err != nil {
+ return err
+ }
+ if t.Type() == "cloud" {
+ if !vespa.Auth0AccessTokenEnabled() {
+ return errors.New("accessing control plane using curl subcommand is only supported for Auth0 device flow")
+ }
+ if err := addCloudAuth0Authentication(cfg, c); err != nil {
+ return err
+ }
+ }
+ case "document", "query":
+ privateKeyFile, err := cfg.PrivateKeyPath(app)
+ if err != nil {
+ return err
+ }
+ certificateFile, err := cfg.CertificatePath(app)
+ if err != nil {
+ return err
+ }
+ c.PrivateKey = privateKeyFile
+ c.Certificate = certificateFile
+ default:
+ return fmt.Errorf("service not found: %s", curlService)
}
- c.PrivateKey = privateKeyFile
- c.Certificate = certificateFile
if curlDryRun {
log.Print(c.String())
} else {
if err := c.Run(os.Stdout, os.Stderr); err != nil {
- fatalErr(err, "Failed to run curl")
- return
+ return fmt.Errorf("failed to execute curl: %w", err)
}
}
+ return nil
},
}
+func addCloudAuth0Authentication(cfg *Config, c *curl.Command) error {
+ a, err := auth0.GetAuth0(cfg.AuthConfigPath(), getSystemName(), getApiURL())
+ if err != nil {
+ return err
+ }
+
+ system, err := a.PrepareSystem(auth0.ContextWithCancel())
+ if err != nil {
+ return err
+ }
+ c.Header("Authorization", "Bearer "+system.AccessToken)
+ return nil
+}
+
func joinURL(baseURL, path string) string {
baseURL = strings.TrimSuffix(baseURL, "/")
path = strings.TrimPrefix(path, "/")
diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go
index d5021e19cf2..e593f48a390 100644
--- a/client/go/cmd/curl_test.go
+++ b/client/go/cmd/curl_test.go
@@ -18,4 +18,8 @@ func TestCurl(t *testing.T) {
filepath.Join(homeDir, "t1.a1.i1", "data-plane-private-key.pem"),
filepath.Join(homeDir, "t1.a1.i1", "data-plane-public-cert.pem"))
assert.Equal(t, expected, out)
+
+ out, _ = execute(command{homeDir: homeDir, args: []string{"curl", "-s", "deploy", "-n", "/application/v4/tenant/foo"}}, t, httpClient)
+ expected = "curl https://127.0.0.1:19071/application/v4/tenant/foo\n"
+ assert.Equal(t, expected, out)
}
diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go
index ae39afc3773..e35188933e1 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -18,9 +18,8 @@ const (
)
var (
- zoneArg string
- logLevelArg string
- sessionOrRunID int64
+ zoneArg string
+ logLevelArg string
)
func init() {
@@ -51,39 +50,45 @@ $ vespa deploy -t cloud -z dev.aws-us-east-1c # -z can be omitted here as this
$ vespa deploy -t cloud -z perf.aws-us-east-1c`,
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
if err != nil {
- fatalErr(nil, err.Error())
- return
+ return err
}
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
+ }
+ target, err := getTarget()
+ if err != nil {
+ return err
+ }
+ opts, err := getDeploymentOpts(cfg, pkg, target)
+ if err != nil {
+ return err
+ }
+ sessionOrRunID, err := vespa.Deploy(opts)
+ if err != nil {
+ return err
}
- target := getTarget()
- opts := getDeploymentOpts(cfg, pkg, target)
- if sessionOrRunID, err := vespa.Deploy(opts); err == nil {
- fmt.Print("\n")
- if opts.IsCloud() {
- printSuccess("Triggered deployment of ", color.Cyan(pkg.Path), " with run ID ", color.Cyan(sessionOrRunID))
- } else {
- printSuccess("Deployed ", color.Cyan(pkg.Path))
- }
- if opts.IsCloud() {
- log.Printf("\nUse %s for deployment status, or follow this deployment at", color.Cyan("vespa status"))
- log.Print(color.Cyan(fmt.Sprintf("%s/tenant/%s/application/%s/dev/instance/%s/job/%s-%s/run/%d",
- getConsoleURL(),
- opts.Deployment.Application.Tenant, opts.Deployment.Application.Application, opts.Deployment.Application.Instance,
- opts.Deployment.Zone.Environment, opts.Deployment.Zone.Region,
- sessionOrRunID)))
- }
- waitForQueryService(sessionOrRunID)
+ fmt.Print("\n")
+ if opts.IsCloud() {
+ printSuccess("Triggered deployment of ", color.Cyan(pkg.Path), " with run ID ", color.Cyan(sessionOrRunID))
} else {
- fatalErr(nil, err.Error())
+ printSuccess("Deployed ", color.Cyan(pkg.Path))
}
+ if opts.IsCloud() {
+ log.Printf("\nUse %s for deployment status, or follow this deployment at", color.Cyan("vespa status"))
+ log.Print(color.Cyan(fmt.Sprintf("%s/tenant/%s/application/%s/dev/instance/%s/job/%s-%s/run/%d",
+ getConsoleURL(),
+ opts.Deployment.Application.Tenant, opts.Deployment.Application.Application, opts.Deployment.Application.Instance,
+ opts.Deployment.Zone.Environment, opts.Deployment.Zone.Region,
+ sessionOrRunID)))
+ }
+ waitForQueryService(sessionOrRunID)
+ return nil
},
}
@@ -92,31 +97,32 @@ var prepareCmd = &cobra.Command{
Short: "Prepare an application package for activation",
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
if err != nil {
- fatalErr(err, "Could not find application package")
- return
+ return fmt.Errorf("could not find application package: %w", err)
}
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
+ }
+ target, err := getTarget()
+ if err != nil {
+ return err
}
- target := getTarget()
sessionID, err := vespa.Prepare(vespa.DeploymentOpts{
ApplicationPackage: pkg,
Target: target,
})
- if err == nil {
- 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())
+ if err != nil {
+ return err
+ }
+ if err := cfg.WriteSessionID(vespa.DefaultApplication, sessionID); err != nil {
+ return fmt.Errorf("could not write session id: %w", err)
}
+ printSuccess("Prepared ", color.Cyan(pkg.Path), " with session ", sessionID)
+ return nil
},
}
@@ -125,33 +131,34 @@ var activateCmd = &cobra.Command{
Short: "Activate (deploy) a previously prepared application package",
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
if err != nil {
- fatalErr(err, "Could not find application package")
- return
+ return fmt.Errorf("could not find application package: %w", err)
}
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
}
sessionID, err := cfg.ReadSessionID(vespa.DefaultApplication)
if err != nil {
- fatalErr(err, "Could not read session ID")
- return
+ return fmt.Errorf("could not read session id: %w", err)
+ }
+ target, err := getTarget()
+ if err != nil {
+ return err
}
- target := getTarget()
err = vespa.Activate(sessionID, vespa.DeploymentOpts{
ApplicationPackage: pkg,
Target: target,
})
- if err == nil {
- printSuccess("Activated ", color.Cyan(pkg.Path), " with session ", sessionID)
- waitForQueryService(sessionID)
- } else {
- fatalErr(nil, err.Error())
+ if err != nil {
+ return err
}
+ printSuccess("Activated ", color.Cyan(pkg.Path), " with session ", sessionID)
+ waitForQueryService(sessionID)
+ return nil
},
}
diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go
index 5bb45e70fad..a37a433397f 100644
--- a/client/go/cmd/deploy_test.go
+++ b/client/go/cmd/deploy_test.go
@@ -164,7 +164,7 @@ func assertApplicationPackageError(t *testing.T, cmd string, status int, expecte
client.NextResponse(status, returnBody)
_, outErr := execute(command{args: []string{cmd, "testdata/applications/withTarget/target/application.zip"}}, t, client)
assert.Equal(t,
- "Error: Invalid application package (Status "+strconv.Itoa(status)+")\n\n"+expectedMessage+"\n",
+ "Error: invalid application package (Status "+strconv.Itoa(status)+")\n"+expectedMessage+"\n",
outErr)
}
@@ -173,6 +173,6 @@ func assertDeployServerError(t *testing.T, status int, errorMessage string) {
client.NextResponse(status, errorMessage)
_, outErr := execute(command{args: []string{"deploy", "testdata/applications/withTarget/target/application.zip"}}, t, client)
assert.Equal(t,
- "Error: Error from deploy service at 127.0.0.1:19071 (Status "+strconv.Itoa(status)+"):\n"+errorMessage+"\n",
+ "Error: error from deploy service at 127.0.0.1:19071 (Status "+strconv.Itoa(status)+"):\n"+errorMessage+"\n",
outErr)
}
diff --git a/client/go/cmd/document.go b/client/go/cmd/document.go
index 84c384e701e..8dab813ec68 100644
--- a/client/go/cmd/document.go
+++ b/client/go/cmd/document.go
@@ -46,9 +46,14 @@ To feed with high throughput, https://docs.vespa.ai/en/vespa-feed-client.html
should be used instead of this.`,
Example: `$ vespa document src/test/resources/A-Head-Full-of-Dreams.json`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
- printResult(vespa.Send(args[0], documentService(), operationOptions()), false)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService()
+ if err != nil {
+ return err
+ }
+ return printResult(vespa.Send(args[0], service, operationOptions()), false)
},
}
@@ -62,11 +67,16 @@ If the document id is specified both as an argument and in the file the argument
Example: `$ vespa document put src/test/resources/A-Head-Full-of-Dreams.json
$ vespa document put id:mynamespace:music::a-head-full-of-dreams src/test/resources/A-Head-Full-of-Dreams.json`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService()
+ if err != nil {
+ return err
+ }
if len(args) == 1 {
- printResult(vespa.Put("", args[0], documentService(), operationOptions()), false)
+ return printResult(vespa.Put("", args[0], service, operationOptions()), false)
} else {
- printResult(vespa.Put(args[0], args[1], documentService(), operationOptions()), false)
+ return printResult(vespa.Put(args[0], args[1], service, operationOptions()), false)
}
},
}
@@ -80,11 +90,16 @@ If the document id is specified both as an argument and in the file the argument
Example: `$ vespa document update src/test/resources/A-Head-Full-of-Dreams-Update.json
$ vespa document update id:mynamespace:music::a-head-full-of-dreams src/test/resources/A-Head-Full-of-Dreams.json`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService()
+ if err != nil {
+ return err
+ }
if len(args) == 1 {
- printResult(vespa.Update("", args[0], documentService(), operationOptions()), false)
+ return printResult(vespa.Update("", args[0], service, operationOptions()), false)
} else {
- printResult(vespa.Update(args[0], args[1], documentService(), operationOptions()), false)
+ return printResult(vespa.Update(args[0], args[1], service, operationOptions()), false)
}
},
}
@@ -98,11 +113,16 @@ If the document id is specified both as an argument and in the file the argument
Example: `$ vespa document remove src/test/resources/A-Head-Full-of-Dreams-Remove.json
$ vespa document remove id:mynamespace:music::a-head-full-of-dreams`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService()
+ if err != nil {
+ return err
+ }
if strings.HasPrefix(args[0], "id:") {
- printResult(vespa.RemoveId(args[0], documentService(), operationOptions()), false)
+ return printResult(vespa.RemoveId(args[0], service, operationOptions()), false)
} else {
- printResult(vespa.RemoveOperation(args[0], documentService(), operationOptions()), false)
+ return printResult(vespa.RemoveOperation(args[0], service, operationOptions()), false)
}
},
}
@@ -112,13 +132,18 @@ var documentGetCmd = &cobra.Command{
Short: "Gets a document",
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
+ SilenceUsage: 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(), operationOptions()), true)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService()
+ if err != nil {
+ return err
+ }
+ return printResult(vespa.Get(args[0], service, operationOptions()), true)
},
}
-func documentService() *vespa.Service { return getService("document", 0, "") }
+func documentService() (*vespa.Service, error) { return getService("document", 0, "") }
func operationOptions() vespa.OperationOptions {
return vespa.OperationOptions{
@@ -134,7 +159,7 @@ func curlOutput() io.Writer {
return ioutil.Discard
}
-func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) {
+func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) error {
out := stdout
if !result.Success {
out = stderr
@@ -158,6 +183,9 @@ func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) {
}
if !result.Success {
- exitFunc(1)
+ err := errHint(fmt.Errorf("document operation failed"))
+ err.quiet = true
+ return err
}
+ return nil
}
diff --git a/client/go/cmd/document_test.go b/client/go/cmd/document_test.go
index f3a5fbe9543..2b596e9893b 100644
--- a/client/go/cmd/document_test.go
+++ b/client/go/cmd/document_test.go
@@ -97,7 +97,10 @@ 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)
+ documentURL, err := documentServiceURL(client)
+ if err != nil {
+ t.Fatal(err)
+ }
expectedPath, _ := vespa.IdToURLPath(expectedDocumentId)
expectedURL := documentURL + "/document/v1/" + expectedPath
out, errOut := execute(command{args: arguments}, t, client)
@@ -123,7 +126,10 @@ func assertDocumentSend(arguments []string, expectedOperation string, expectedMe
func assertDocumentGet(arguments []string, documentId string, t *testing.T) {
client := &mockHttpClient{}
- documentURL := documentServiceURL(client)
+ documentURL, err := documentServiceURL(client)
+ if err != nil {
+ t.Fatal(err)
+ }
client.NextResponse(200, "{\"fields\":{\"foo\":\"bar\"}}")
assert.Equal(t,
`{
@@ -160,6 +166,10 @@ func assertDocumentServerError(t *testing.T, status int, errorMessage string) {
outErr)
}
-func documentServiceURL(client *mockHttpClient) string {
- return getService("document", 0, "").BaseURL
+func documentServiceURL(client *mockHttpClient) (string, error) {
+ service, err := getService("document", 0, "")
+ if err != nil {
+ return "", err
+ }
+ return service.BaseURL, nil
}
diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go
index f065ae0c680..03cdaecbfce 100644
--- a/client/go/cmd/helpers.go
+++ b/client/go/cmd/helpers.go
@@ -5,10 +5,8 @@
package cmd
import (
- "crypto/tls"
"encoding/json"
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -18,34 +16,15 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var exitFunc = os.Exit // To allow overriding Exit in tests
-
-func fatalErrHint(err error, hints ...string) {
- printErrHint(err, hints...)
- exitFunc(1)
-}
-
-func fatalErr(err error, msg ...interface{}) {
- printErr(err, msg...)
- exitFunc(1)
-}
-
func printErrHint(err error, hints ...string) {
- if err != nil {
- printErr(nil, err.Error())
- }
+ printErr(err)
for _, hint := range hints {
fmt.Fprintln(stderr, color.Cyan("Hint:"), hint)
}
}
-func printErr(err error, msg ...interface{}) {
- if len(msg) > 0 {
- fmt.Fprintln(stderr, color.Red("Error:"), fmt.Sprint(msg...))
- }
- if err != nil {
- fmt.Fprintln(stderr, color.Yellow(err))
- }
+func printErr(err error) {
+ fmt.Fprintln(stderr, color.Red("Error:"), err)
}
func printSuccess(msg ...interface{}) {
@@ -82,13 +61,16 @@ func vespaCliCacheDir() (string, error) {
return cacheDir, nil
}
-func deploymentFromArgs() vespa.Deployment {
+func deploymentFromArgs() (vespa.Deployment, error) {
zone, err := vespa.ZoneFromString(zoneArg)
if err != nil {
- fatalErrHint(err, "Zone format is <env>.<region>")
+ return vespa.Deployment{}, err
+ }
+ app, err := getApplication()
+ if err != nil {
+ return vespa.Deployment{}, err
}
- app := getApplication()
- return vespa.Deployment{Application: app, Zone: zone}
+ return vespa.Deployment{Application: app, Zone: zone}, nil
}
func applicationSource(args []string) string {
@@ -98,49 +80,48 @@ func applicationSource(args []string) string {
return "."
}
-func getApplication() vespa.ApplicationID {
+func getApplication() (vespa.ApplicationID, error) {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return vespa.ApplicationID{}
+ return vespa.ApplicationID{}, err
}
app, err := cfg.Get(applicationFlag)
if err != nil {
- fatalErrHint(err, "No application specified. Try the --"+applicationFlag+" flag")
- return vespa.ApplicationID{}
+ return vespa.ApplicationID{}, errHint(fmt.Errorf("no application specified: %w", err), "Try the --"+applicationFlag+" flag")
}
application, err := vespa.ApplicationFromString(app)
if err != nil {
- fatalErrHint(err, "Application format is <tenant>.<app>.<instance>")
- return vespa.ApplicationID{}
+ return vespa.ApplicationID{}, errHint(err, "application format is <tenant>.<app>.<instance>")
}
- return application
+ return application, nil
}
-func getTargetType() string {
+func getTargetType() (string, error) {
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return ""
+ return "", err
}
target, err := cfg.Get(targetFlag)
if err != nil {
- fatalErr(err, "A valid target must be specified")
+ return "", fmt.Errorf("invalid target: %w", err)
}
- return target
+ return target, nil
}
-func getService(service string, sessionOrRunID int64, cluster string) *vespa.Service {
- t := getTarget()
+func getService(service string, sessionOrRunID int64, cluster string) (*vespa.Service, error) {
+ t, err := getTarget()
+ if err != nil {
+ return nil, err
+ }
timeout := time.Duration(waitSecsArg) * time.Second
if timeout > 0 {
- log.Printf("Waiting up to %d %s for service to become available ...", color.Cyan(waitSecsArg), color.Cyan("seconds"))
+ log.Printf("Waiting up to %d %s for %s service to become available ...", color.Cyan(waitSecsArg), color.Cyan("seconds"), color.Cyan(service))
}
s, err := t.Service(service, timeout, sessionOrRunID, cluster)
if err != nil {
- fatalErr(err, "Invalid service: ", service)
+ return nil, fmt.Errorf("service %s not found: %w", service, err)
}
- return s
+ return s, nil
}
func getEndpointsOverride() string { return os.Getenv("VESPA_CLI_ENDPOINTS") }
@@ -169,49 +150,47 @@ func getApiURL() string {
return "https://api.vespa-external.aws.oath.cloud:4443"
}
-func getTarget() vespa.Target {
- targetType := getTargetType()
+func getTarget() (vespa.Target, error) {
+ targetType, err := getTargetType()
+ if err != nil {
+ return nil, err
+ }
if strings.HasPrefix(targetType, "http") {
- return vespa.CustomTarget(targetType)
+ return vespa.CustomTarget(targetType), nil
}
switch targetType {
case "local":
- return vespa.LocalTarget()
+ return vespa.LocalTarget(), nil
case "cloud":
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return nil
+ return nil, err
+ }
+ deployment, err := deploymentFromArgs()
+ if err != nil {
+ return nil, err
+ }
+ endpoints, err := getEndpointsFromEnv()
+ if err != nil {
+ return nil, err
}
- deployment := deploymentFromArgs()
- endpoints := getEndpointsFromEnv()
var apiKey []byte = nil
- apiKey, err = ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant))
+ apiKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
if !vespa.Auth0AccessTokenEnabled() && endpoints == nil {
if err != nil {
- fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ return nil, errHint(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)
+ kp, err := cfg.X509KeyPair(deployment.Application)
if err != nil {
- fatalErr(err)
- return nil
- }
- kp, err := tls.LoadX509KeyPair(certificateFile, privateKeyFile)
- if err != nil {
- var msg string
+ var hint string
if vespa.Auth0AccessTokenEnabled() {
- msg = "Deployment to cloud requires a certificate. Try 'vespa auth cert'"
+ hint = "Deployment to cloud requires a certificate. Try 'vespa auth cert'"
} else {
- msg = "Deployment to cloud requires a certificate. Try 'vespa cert'"
+ hint = "Deployment to cloud requires a certificate. Try 'vespa cert'"
}
- fatalErrHint(err, msg)
+ return nil, errHint(err, hint)
}
var cloudAuth string
if vespa.Auth0AccessTokenEnabled() {
@@ -227,11 +206,14 @@ func getTarget() vespa.Target {
cloudAuth = ""
}
- return vespa.CloudTarget(getApiURL(), deployment, apiKey,
+ return vespa.CloudTarget(
+ getApiURL(),
+ deployment,
+ apiKey,
vespa.TLSOptions{
- KeyPair: kp,
- CertificateFile: certificateFile,
- PrivateKeyFile: privateKeyFile,
+ KeyPair: kp.KeyPair,
+ CertificateFile: kp.CertificateFile,
+ PrivateKeyFile: kp.PrivateKeyFile,
},
vespa.LogOptions{
Writer: stdout,
@@ -240,14 +222,17 @@ func getTarget() vespa.Target {
cfg.AuthConfigPath(),
getSystemName(),
cloudAuth,
- endpoints)
+ endpoints,
+ ), nil
}
- fatalErrHint(fmt.Errorf("Invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL")
- return nil
+ return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL")
}
-func waitForService(service string, sessionOrRunID int64) {
- s := getService(service, sessionOrRunID, "")
+func waitForService(service string, sessionOrRunID int64) error {
+ s, err := getService(service, sessionOrRunID, "")
+ if err != nil {
+ return err
+ }
timeout := time.Duration(waitSecsArg) * time.Second
if timeout > 0 {
log.Printf("Waiting up to %d %s for service to become ready ...", color.Cyan(waitSecsArg), color.Cyan("seconds"))
@@ -257,57 +242,58 @@ func waitForService(service string, sessionOrRunID int64) {
log.Print(s.Description(), " at ", color.Cyan(s.BaseURL), " is ", color.Green("ready"))
} else {
if err == nil {
- err = fmt.Errorf("Status %d", status)
+ err = fmt.Errorf("status %d", status)
}
- fatalErr(err, s.Description(), " at ", color.Cyan(s.BaseURL), " is ", color.Red("not ready"))
+ return fmt.Errorf("%s at %s is %s: %w", s.Description(), color.Cyan(s.BaseURL), color.Red("not ready"), err)
}
+ return nil
}
-func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.Target) vespa.DeploymentOpts {
+func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.Target) (vespa.DeploymentOpts, error) {
opts := vespa.DeploymentOpts{ApplicationPackage: pkg, Target: target}
if opts.IsCloud() {
- deployment := deploymentFromArgs()
+ deployment, err := deploymentFromArgs()
+ if err != nil {
+ return vespa.DeploymentOpts{}, err
+ }
if !opts.ApplicationPackage.HasCertificate() {
- var msg string
+ var hint string
if vespa.Auth0AccessTokenEnabled() {
- msg = "Try 'vespa auth cert'"
+ hint = "Try 'vespa auth cert'"
} else {
- msg = "Try 'vespa cert'"
+ hint = "Try 'vespa cert'"
}
- fatalErrHint(fmt.Errorf("Missing certificate in application package"), "Applications in Vespa Cloud require a certificate", msg)
- return opts
+ return vespa.DeploymentOpts{}, errHint(fmt.Errorf("missing certificate in application package"), "Applications in Vespa Cloud require a certificate", hint)
}
- var err error
opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
if !vespa.Auth0AccessTokenEnabled() {
if err != nil {
- fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
- return opts
+ return vespa.DeploymentOpts{}, errHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
}
}
opts.Deployment = deployment
}
- return opts
+ return opts, nil
}
-func getEndpointsFromEnv() map[string]string {
+func getEndpointsFromEnv() (map[string]string, error) {
endpointsString := getEndpointsOverride()
if endpointsString == "" {
- return nil
+ return nil, nil
}
var endpoints endpoints
urlsByCluster := make(map[string]string)
if err := json.Unmarshal([]byte(endpointsString), &endpoints); err != nil {
- fatalErrHint(err, "Endpoints must be valid JSON")
+ return nil, fmt.Errorf("endpoints must be valid json: %w", err)
}
if len(endpoints.Endpoints) == 0 {
- fatalErr(fmt.Errorf("endpoints must be non-empty"))
+ return nil, fmt.Errorf("endpoints must be non-empty")
}
for _, endpoint := range endpoints.Endpoints {
urlsByCluster[endpoint.Cluster] = endpoint.URL
}
- return urlsByCluster
+ return urlsByCluster, nil
}
type endpoints struct {
diff --git a/client/go/cmd/log.go b/client/go/cmd/log.go
index 4577e890959..d61eaecf35b 100644
--- a/client/go/cmd/log.go
+++ b/client/go/cmd/log.go
@@ -32,15 +32,21 @@ var logCmd = &cobra.Command{
Long: `Show the Vespa log.
The logs shown can be limited to a relative or fixed period. All timestamps are shown in UTC.
+
+Logs for the past hour are shown if no arguments are given.
`,
Example: `$ vespa log 1h
$ vespa log --nldequote=false 10m
$ vespa log --from 2021-08-25T15:00:00Z --to 2021-08-26T02:00:00Z
$ vespa log --follow`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
- target := getTarget()
+ RunE: func(cmd *cobra.Command, args []string) error {
+ target, err := getTarget()
+ if err != nil {
+ return err
+ }
options := vespa.LogOptions{
Level: vespa.LogLevel(levelArg),
Follow: followArg,
@@ -49,30 +55,32 @@ $ vespa log --follow`,
}
if options.Follow {
if fromArg != "" || toArg != "" || len(args) > 0 {
- fatalErr(fmt.Errorf("cannot combine --from/--to or relative time with --follow"), "Could not follow logs")
+ return fmt.Errorf("cannot combine --from/--to or relative time with --follow")
}
options.From = time.Now().Add(-5 * time.Minute)
} else {
from, to, err := parsePeriod(args)
if err != nil {
- fatalErr(err, "Invalid period")
- return
+ return fmt.Errorf("invalid period: %w", err)
}
options.From = from
options.To = to
}
if err := target.PrintLog(options); err != nil {
- fatalErr(err, "Could not retrieve logs")
+ return fmt.Errorf("could not retrieve logs: %w", err)
}
+ return nil
},
}
func parsePeriod(args []string) (time.Time, time.Time, error) {
- if len(args) == 1 {
- if fromArg != "" || toArg != "" {
- return time.Time{}, time.Time{}, fmt.Errorf("cannot combine --from/--to with relative value: %s", args[0])
+ relativePeriod := fromArg == "" || toArg == ""
+ if relativePeriod {
+ period := "1h"
+ if len(args) > 0 {
+ period = args[0]
}
- d, err := time.ParseDuration(args[0])
+ d, err := time.ParseDuration(period)
if err != nil {
return time.Time{}, time.Time{}, err
}
@@ -82,6 +90,8 @@ func parsePeriod(args []string) (time.Time, time.Time, error) {
to := time.Now()
from := to.Add(d)
return from, to, nil
+ } else if len(args) > 0 {
+ return time.Time{}, time.Time{}, fmt.Errorf("cannot combine --from/--to with relative value: %s", args[0])
}
from, err := time.Parse(time.RFC3339, fromArg)
if err != nil {
diff --git a/client/go/cmd/log_test.go b/client/go/cmd/log_test.go
index f239bebc488..4b32927ca17 100644
--- a/client/go/cmd/log_test.go
+++ b/client/go/cmd/log_test.go
@@ -24,5 +24,5 @@ func TestLog(t *testing.T) {
assert.Equal(t, expected, out)
_, errOut := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T13:12:49Z", "--to", "2021-09-27T13:15:00", "1h"}}, t, httpClient)
- assert.Equal(t, "Error: Invalid period\ncannot combine --from/--to with relative value: 1h\n", errOut)
+ assert.Equal(t, "Error: invalid period: cannot combine --from/--to with relative value: 1h\n", errOut)
}
diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go
index 5011b290b9f..f4438cdbb24 100644
--- a/client/go/cmd/login.go
+++ b/client/go/cmd/login.go
@@ -12,6 +12,7 @@ var loginCmd = &cobra.Command{
Short: "Authenticate the Vespa CLI",
Example: "$ vespa auth login",
DisableAutoGenTag: true,
+ SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg, err := LoadConfig()
@@ -29,7 +30,7 @@ var loginCmd = &cobra.Command{
return err
}
if err := cfg.Write(); err != nil {
- fatalErr(err)
+ return err
}
}
}
diff --git a/client/go/cmd/man.go b/client/go/cmd/man.go
index d90898117de..01fffd38a32 100644
--- a/client/go/cmd/man.go
+++ b/client/go/cmd/man.go
@@ -2,6 +2,8 @@
package cmd
import (
+ "fmt"
+
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
@@ -16,13 +18,14 @@ var manCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
Hidden: true, // Not intended to be called by users
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
dir := args[0]
err := doc.GenManTree(rootCmd, nil, dir)
if err != nil {
- fatalErr(err, "Failed to write man pages")
- return
+ return fmt.Errorf("failed to write man pages: %w", err)
}
printSuccess("Man pages written to ", dir)
+ return nil
},
}
diff --git a/client/go/cmd/prod.go b/client/go/cmd/prod.go
index c686f1d29ad..89f401a356e 100644
--- a/client/go/cmd/prod.go
+++ b/client/go/cmd/prod.go
@@ -33,10 +33,10 @@ Configure and deploy your application package to production in Vespa Cloud.`,
Example: `$ vespa prod init
$ vespa prod submit`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
- // Root command does nothing
- cmd.Help()
- exitFunc(1)
+ SilenceUsage: false,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
},
}
@@ -53,47 +53,50 @@ Reference:
https://cloud.vespa.ai/en/reference/services
https://cloud.vespa.ai/en/reference/deployment`,
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
appSource := applicationSource(args)
pkg, err := vespa.FindApplicationPackage(appSource, false)
if err != nil {
- fatalErr(err)
- return
+ return err
}
if pkg.IsZip() {
- fatalErrHint(fmt.Errorf("Cannot modify compressed application package %s", pkg.Path),
+ return errHint(fmt.Errorf("cannot modify compressed application package %s", pkg.Path),
"Try running 'mvn clean' and run this command again")
- return
}
deploymentXML, err := readDeploymentXML(pkg)
if err != nil {
- fatalErr(err, "Could not read deployment.xml")
- return
+ return fmt.Errorf("could not read deployment.xml: %w", err)
}
servicesXML, err := readServicesXML(pkg)
if err != nil {
- fatalErr(err, "A services.xml declaring your cluster(s) must exist")
- return
+ return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err)
}
fmt.Fprint(stdout, "This will modify any existing ", color.Yellow("deployment.xml"), " and ", color.Yellow("services.xml"),
"!\nBefore modification a backup of the original file will be created.\n\n")
fmt.Fprint(stdout, "A default value is suggested (shown inside brackets) based on\nthe files' existing contents. Press enter to use it.\n\n")
fmt.Fprint(stdout, "Abort the configuration at any time by pressing Ctrl-C. The\nfiles will remain untouched.\n\n")
+ fmt.Fprint(stdout, "See this guide for sizing a Vespa deployment:\n", color.Green("https://docs.vespa.ai/en/performance/sizing-search.html\n\n"))
r := bufio.NewReader(stdin)
- deploymentXML = updateRegions(r, deploymentXML)
- servicesXML = updateNodes(r, servicesXML)
+ deploymentXML, err = updateRegions(r, deploymentXML)
+ if err != nil {
+ return err
+ }
+ servicesXML, err = updateNodes(r, servicesXML)
+ if err != nil {
+ return err
+ }
fmt.Fprintln(stdout)
if err := writeWithBackup(pkg, "deployment.xml", deploymentXML.String()); err != nil {
- fatalErr(err)
- return
+ return err
}
if err := writeWithBackup(pkg, "services.xml", servicesXML.String()); err != nil {
- fatalErr(err)
- return
+ return err
}
+ return nil
},
}
@@ -116,49 +119,55 @@ For more information about production deployments in Vespa Cloud see:
https://cloud.vespa.ai/en/getting-to-production
https://cloud.vespa.ai/en/automated-deployments`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Example: `$ mvn package # when adding custom Java components
$ vespa prod submit`,
- Run: func(cmd *cobra.Command, args []string) {
- target := getTarget()
+ RunE: func(cmd *cobra.Command, args []string) error {
+ target, err := getTarget()
+ if err != nil {
+ return err
+ }
if target.Type() != "cloud" {
- fatalErr(fmt.Errorf("%s target cannot deploy to Vespa Cloud", target.Type()))
- return
+ return fmt.Errorf("%s target cannot deploy to Vespa Cloud", target.Type())
}
appSource := applicationSource(args)
pkg, err := vespa.FindApplicationPackage(appSource, true)
if err != nil {
- fatalErr(err)
- return
+ return err
}
cfg, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
- return
+ return err
}
if !pkg.HasDeployment() {
- fatalErrHint(fmt.Errorf("No deployment.xml found"), "Try creating one with vespa prod init")
- return
+ return errHint(fmt.Errorf("no deployment.xml found"), "Try creating one with vespa prod init")
}
if pkg.TestPath == "" {
- fatalErrHint(fmt.Errorf("No tests found"),
+ return errHint(fmt.Errorf("no tests found"),
"The application must be a Java maven project, or include basic HTTP tests under src/test/application/",
"See https://cloud.vespa.ai/en/getting-to-production")
- return
}
- verifyTests(pkg.TestPath, target)
+ // TODO: Always verify tests. Do it before packaging, when running Maven from this CLI.
+ if !pkg.IsZip() {
+ verifyTests(pkg.TestPath, target)
+ }
isCI := os.Getenv("CI") != ""
if !isCI {
fmt.Fprintln(stderr, color.Yellow("Warning:"), "We recommend doing this only from a CD job")
printErrHint(nil, "See https://cloud.vespa.ai/en/getting-to-production")
}
- opts := getDeploymentOpts(cfg, pkg, target)
+ opts, err := getDeploymentOpts(cfg, pkg, target)
+ if err != nil {
+ return err
+ }
if err := vespa.Submit(opts); err != nil {
- fatalErr(err, "Could not submit application for deployment")
+ return fmt.Errorf("could not submit application for deployment: %w", err)
} else {
printSuccess("Submitted ", color.Cyan(pkg.Path), " for deployment")
log.Printf("See %s for deployment progress\n", color.Cyan(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment",
getConsoleURL(), opts.Deployment.Application.Tenant, opts.Deployment.Application.Application)))
}
+ return nil
},
}
@@ -193,24 +202,27 @@ func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) er
return ioutil.WriteFile(dst, []byte(contents), 0644)
}
-func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment) xml.Deployment {
- regions := promptRegions(r, deploymentXML)
+func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment) (xml.Deployment, error) {
+ regions, err := promptRegions(r, deploymentXML)
+ if err != nil {
+ return xml.Deployment{}, err
+ }
parts := strings.Split(regions, ",")
regionElements := xml.Regions(parts...)
if err := deploymentXML.Replace("prod", "region", regionElements); err != nil {
- fatalErr(err, "Could not update region elements in deployment.xml")
+ return xml.Deployment{}, fmt.Errorf("could not update region elements in deployment.xml: %w", err)
}
// TODO: Some sample apps come with production <test> elements, but not necessarily working production tests, we
// therefore remove <test> elements here.
// This can be improved by supporting <test> elements in xml package and allow specifying testing as part of
// region prompt, e.g. region1;test,region2
if err := deploymentXML.Replace("prod", "test", nil); err != nil {
- fatalErr(err, "Could not remove test elements in deployment.xml")
+ return xml.Deployment{}, fmt.Errorf("could not remove test elements in deployment.xml: %w", err)
}
- return deploymentXML
+ return deploymentXML, nil
}
-func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment) string {
+func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment) (string, error) {
fmt.Fprintln(stdout, color.Cyan("> Deployment regions"))
fmt.Fprintf(stdout, "Documentation: %s\n", color.Green("https://cloud.vespa.ai/en/reference/zones"))
fmt.Fprintf(stdout, "Example: %s\n\n", color.Yellow("aws-us-east-1c,aws-us-west-2a"))
@@ -235,47 +247,56 @@ func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment) string {
return prompt(r, "Which regions do you wish to deploy in?", strings.Join(currentRegions, ","), validator)
}
-func updateNodes(r *bufio.Reader, servicesXML xml.Services) xml.Services {
+func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error) {
for _, c := range servicesXML.Container {
- nodes := promptNodes(r, c.ID, c.Nodes)
+ nodes, err := promptNodes(r, c.ID, c.Nodes)
+ if err != nil {
+ return xml.Services{}, err
+ }
if err := servicesXML.Replace("container#"+c.ID, "nodes", nodes); err != nil {
- fatalErr(err)
- return xml.Services{}
+ return xml.Services{}, err
}
}
for _, c := range servicesXML.Content {
- nodes := promptNodes(r, c.ID, c.Nodes)
+ nodes, err := promptNodes(r, c.ID, c.Nodes)
+ if err != nil {
+ return xml.Services{}, err
+ }
if err := servicesXML.Replace("content#"+c.ID, "nodes", nodes); err != nil {
- fatalErr(err)
- return xml.Services{}
+ return xml.Services{}, err
}
}
- return servicesXML
+ return servicesXML, nil
}
-func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) xml.Nodes {
- count := promptNodeCount(r, clusterID, defaultValue.Count)
+func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml.Nodes, error) {
+ count, err := promptNodeCount(r, clusterID, defaultValue.Count)
+ if err != nil {
+ return xml.Nodes{}, err
+ }
const autoSpec = "auto"
defaultSpec := autoSpec
resources := defaultValue.Resources
if resources != nil {
defaultSpec = defaultValue.Resources.String()
}
- spec := promptResources(r, clusterID, defaultSpec)
+ spec, err := promptResources(r, clusterID, defaultSpec)
+ if err != nil {
+ return xml.Nodes{}, err
+ }
if spec == autoSpec {
resources = nil
} else {
r, err := xml.ParseResources(spec)
if err != nil {
- fatalErr(err) // Should not happen as resources have already been validated
- return xml.Nodes{}
+ return xml.Nodes{}, err // Should not happen as resources have already been validated
}
resources = &r
}
- return xml.Nodes{Count: count, Resources: resources}
+ return xml.Nodes{Count: count, Resources: resources}, nil
}
-func promptNodeCount(r *bufio.Reader, clusterID string, nodeCount string) string {
+func promptNodeCount(r *bufio.Reader, clusterID string, nodeCount string) (string, error) {
fmt.Fprintln(stdout, color.Cyan("\n> Node count: "+clusterID+" cluster"))
fmt.Fprintf(stdout, "Documentation: %s\n", color.Green("https://cloud.vespa.ai/en/reference/services"))
fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.Yellow("4"), color.Yellow("[2,8]"))
@@ -286,7 +307,7 @@ func promptNodeCount(r *bufio.Reader, clusterID string, nodeCount string) string
return prompt(r, fmt.Sprintf("How many nodes should the %s cluster have?", color.Cyan(clusterID)), nodeCount, validator)
}
-func promptResources(r *bufio.Reader, clusterID string, resources string) string {
+func promptResources(r *bufio.Reader, clusterID string, resources string) (string, error) {
fmt.Fprintln(stdout, color.Cyan("\n> Node resources: "+clusterID+" cluster"))
fmt.Fprintf(stdout, "Documentation: %s\n", color.Green("https://cloud.vespa.ai/en/reference/services"))
fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.Yellow("auto"), color.Yellow("vcpu=4,memory=8Gb,disk=100Gb"))
@@ -321,7 +342,7 @@ func readServicesXML(pkg vespa.ApplicationPackage) (xml.Services, error) {
return xml.ReadServices(f)
}
-func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(input string) error) string {
+func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(input string) error) (string, error) {
var input string
for input == "" {
fmt.Fprint(stdout, question)
@@ -333,8 +354,7 @@ func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(inpu
var err error
input, err = r.ReadString('\n')
if err != nil {
- fatalErr(err)
- return ""
+ return "", err
}
input = strings.TrimSpace(input)
if input == "" {
@@ -347,7 +367,7 @@ func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(inpu
input = ""
}
}
- return input
+ return input, nil
}
func verifyTests(testsParent string, target vespa.Target) {
@@ -357,20 +377,20 @@ func verifyTests(testsParent string, target vespa.Target) {
verifyTest(testsParent, "production-test", target, false)
}
-func verifyTest(testsParent string, suite string, target vespa.Target, required bool) {
+func verifyTest(testsParent string, suite string, target vespa.Target, required bool) error {
testDirectory := filepath.Join(testsParent, "tests", suite)
_, err := os.Stat(testDirectory)
if err != nil {
if required {
if errors.Is(err, os.ErrNotExist) {
- fatalErrHint(fmt.Errorf("No %s tests found", suite),
+ return errHint(fmt.Errorf("no %s tests found: %w", suite, err),
fmt.Sprintf("No such directory: %s", testDirectory),
"See https://cloud.vespa.ai/en/reference/testing")
}
- fatalErrHint(err, "See https://cloud.vespa.ai/en/reference/testing")
+ return errHint(err, "See https://cloud.vespa.ai/en/reference/testing")
}
- return
+ return nil
}
-
- runTests(testDirectory, true)
+ _, _, err = runTests(testDirectory, true)
+ return err
}
diff --git a/client/go/cmd/prod_test.go b/client/go/cmd/prod_test.go
index a4f3ebd6b56..4e635f87a75 100644
--- a/client/go/cmd/prod_test.go
+++ b/client/go/cmd/prod_test.go
@@ -46,8 +46,8 @@ func TestProdInit(t *testing.T) {
// Verify contents
deploymentPath := filepath.Join(pkgDir, "src", "main", "application", "deployment.xml")
deploymentXML := readFileString(t, deploymentPath)
- assert.Contains(t, deploymentXML, `<region active="true">aws-us-west-2a</region>`)
- assert.Contains(t, deploymentXML, `<region active="true">aws-eu-west-1a</region>`)
+ assert.Contains(t, deploymentXML, `<region>aws-us-west-2a</region>`)
+ assert.Contains(t, deploymentXML, `<region>aws-eu-west-1a</region>`)
servicesPath := filepath.Join(pkgDir, "src", "main", "application", "services.xml")
servicesXML := readFileString(t, servicesPath)
@@ -90,7 +90,7 @@ func createApplication(t *testing.T, pkgDir string, java bool) {
deploymentXML := `<deployment version="1.0">
<prod>
- <region active="true">aws-us-east-1c</region>
+ <region>aws-us-east-1c</region>
</prod>
</deployment>`
if err := ioutil.WriteFile(filepath.Join(appDir, "deployment.xml"), []byte(deploymentXML), 0644); err != nil {
diff --git a/client/go/cmd/query.go b/client/go/cmd/query.go
index 6638c275330..8ee022d7061 100644
--- a/client/go/cmd/query.go
+++ b/client/go/cmd/query.go
@@ -5,6 +5,7 @@
package cmd
import (
+ "fmt"
"log"
"net/http"
"net/url"
@@ -19,49 +20,62 @@ var queryTimeoutSecs int
func init() {
rootCmd.AddCommand(queryCmd)
- queryCmd.Flags().IntVarP(&queryTimeoutSecs, "timeout", "T", 10, "Timeout for the query request in seconds")
+ queryCmd.Flags().IntVarP(&queryTimeoutSecs, "timeout", "T", 10, "Timeout for the query in seconds")
}
var queryCmd = &cobra.Command{
Use: "query query-parameters",
Short: "Issue a query to Vespa",
- Example: `$ vespa query "yql=select * from sources * where title contains 'foo';" hits=5`,
+ Example: `$ vespa query "yql=select * from music where album contains 'head';" hits=5`,
Long: `Issue a query to Vespa.
Any parameter from https://docs.vespa.ai/en/reference/query-api-reference.html
can be set by the syntax [parameter-name]=[value].`,
// TODO: Support referencing a query json file
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MinimumNArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
- query(args)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return query(args)
},
}
-func query(arguments []string) {
- service := getService("query", 0, "")
+func query(arguments []string) error {
+ service, err := getService("query", 0, "")
+ if err != nil {
+ return err
+ }
url, _ := url.Parse(service.BaseURL + "/search/")
urlQuery := url.Query()
for i := 0; i < len(arguments); i++ {
key, value := splitArg(arguments[i])
urlQuery.Set(key, value)
}
+ queryTimeout := urlQuery.Get("timeout")
+ if queryTimeout == "" {
+ // No timeout set by user, use the timeout option
+ queryTimeout = fmt.Sprintf("%ds", queryTimeoutSecs)
+ urlQuery.Set("timeout", queryTimeout)
+ }
url.RawQuery = urlQuery.Encode()
-
- response, err := service.Do(&http.Request{URL: url}, time.Second*time.Duration(queryTimeoutSecs))
+ deadline, err := time.ParseDuration(queryTimeout)
+ if err != nil {
+ return fmt.Errorf("invalid query timeout: %w", err)
+ }
+ response, err := service.Do(&http.Request{URL: url}, deadline+time.Second) // Slightly longer than query timeout
if err != nil {
- fatalErr(nil, "Request failed: ", err)
- return
+ return fmt.Errorf("request failed: %w", err)
}
defer response.Body.Close()
if response.StatusCode == 200 {
log.Print(util.ReaderToJSON(response.Body))
} else if response.StatusCode/100 == 4 {
- fatalErr(nil, "Invalid query: ", response.Status, "\n", util.ReaderToJSON(response.Body))
+ return fmt.Errorf("invalid query: %s\n%s", response.Status, util.ReaderToJSON(response.Body))
} else {
- fatalErr(nil, response.Status, " from container at ", color.Cyan(url.Host), "\n", util.ReaderToJSON(response.Body))
+ return fmt.Errorf("%s from container at %s\n%s", response.Status, color.Cyan(url.Host), util.ReaderToJSON(response.Body))
}
+ return nil
}
func splitArg(argument string) (string, string) {
diff --git a/client/go/cmd/query_test.go b/client/go/cmd/query_test.go
index 55046ae49ba..09b1afb2b84 100644
--- a/client/go/cmd/query_test.go
+++ b/client/go/cmd/query_test.go
@@ -13,25 +13,25 @@ import (
func TestQuery(t *testing.T) {
assertQuery(t,
- "?yql=select+from+sources+%2A+where+title+contains+%27foo%27",
+ "?timeout=10s&yql=select+from+sources+%2A+where+title+contains+%27foo%27",
"select from sources * where title contains 'foo'")
}
func TestQueryNonJsonResult(t *testing.T) {
assertQuery(t,
- "?yql=select+from+sources+%2A+where+title+contains+%27foo%27",
+ "?timeout=10s&yql=select+from+sources+%2A+where+title+contains+%27foo%27",
"select from sources * where title contains 'foo'")
}
func TestQueryWithMultipleParameters(t *testing.T) {
assertQuery(t,
- "?hits=5&yql=select+from+sources+%2A+where+title+contains+%27foo%27",
- "select from sources * where title contains 'foo'", "hits=5")
+ "?hits=5&timeout=20s&yql=select+from+sources+%2A+where+title+contains+%27foo%27",
+ "select from sources * where title contains 'foo'", "hits=5", "timeout=20s")
}
func TestQueryWithExplicitYqlParameter(t *testing.T) {
assertQuery(t,
- "?yql=select+from+sources+%2A+where+title+contains+%27foo%27",
+ "?timeout=10s&yql=select+from+sources+%2A+where+title+contains+%27foo%27",
"yql=select from sources * where title contains 'foo'")
}
@@ -50,7 +50,10 @@ func assertQuery(t *testing.T, expectedQuery string, query ...string) {
"{\n \"query\": \"result\"\n}\n",
executeCommand(t, client, []string{"query"}, query),
"query output")
- queryURL := queryServiceURL(client)
+ queryURL, err := queryServiceURL(client)
+ if err != nil {
+ t.Fatal(err)
+ }
assert.Equal(t, queryURL+"/search/"+expectedQuery, client.lastRequest.URL.String())
}
@@ -59,7 +62,7 @@ func assertQueryError(t *testing.T, status int, errorMessage string) {
client.NextResponse(status, errorMessage)
_, outErr := execute(command{args: []string{"query", "yql=select from sources * where title contains 'foo'"}}, t, client)
assert.Equal(t,
- "Error: Invalid query: Status "+strconv.Itoa(status)+"\n"+errorMessage+"\n",
+ "Error: invalid query: Status "+strconv.Itoa(status)+"\n"+errorMessage+"\n",
outErr,
"error output")
}
@@ -74,6 +77,10 @@ func assertQueryServiceError(t *testing.T, status int, errorMessage string) {
"error output")
}
-func queryServiceURL(client *mockHttpClient) string {
- return getService("query", 0, "").BaseURL
+func queryServiceURL(client *mockHttpClient) (string, error) {
+ service, err := getService("query", 0, "")
+ if err != nil {
+ return "", err
+ }
+ return service.BaseURL, nil
}
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go
index 087be7c352d..e25c5cb2d0d 100644
--- a/client/go/cmd/root.go
+++ b/client/go/cmd/root.go
@@ -17,6 +17,15 @@ import (
"github.com/spf13/cobra"
)
+// ErrCLI is an error returned to the user. It wraps an exit status, a regular error and optional hints for resolving
+// the error.
+type ErrCLI struct {
+ Status int
+ quiet bool
+ hints []string
+ error
+}
+
var (
rootCmd = &cobra.Command{
Use: "vespa command-name",
@@ -28,8 +37,14 @@ Prefer web service API's to this in production.
Vespa documentation: https://docs.vespa.ai`,
DisableAutoGenTag: true,
- PersistentPreRun: func(cmd *cobra.Command, args []string) {
- configureOutput()
+ SilenceErrors: true, // We have our own error printing
+ SilenceUsage: false,
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+ return configureOutput()
+ },
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
},
}
@@ -64,7 +79,7 @@ func isTerminal() bool {
return false
}
-func configureOutput() {
+func configureOutput() error {
if quietArg {
stdout = ioutil.Discard
}
@@ -73,11 +88,11 @@ func configureOutput() {
config, err := LoadConfig()
if err != nil {
- fatalErr(err, "Could not load config")
+ return err
}
colorValue, err := config.Get(colorFlag)
if err != nil {
- fatalErr(err)
+ return err
}
colorize := false
@@ -88,9 +103,10 @@ func configureOutput() {
colorize = true
case "never":
default:
- fatalErrHint(fmt.Errorf("Invalid value for %s option", colorFlag), "Must be \"auto\", \"never\" or \"always\"")
+ return errHint(fmt.Errorf("invalid value for %s option", colorFlag), "Must be \"auto\", \"never\" or \"always\"")
}
color = aurora.NewAurora(colorize)
+ return nil
}
func init() {
@@ -106,5 +122,20 @@ func init() {
bindFlagToConfig(quietFlag, rootCmd)
}
-// Execute executes the root command.
-func Execute() error { return rootCmd.Execute() }
+// errHint creates a new CLI error, with optional hints that will be printed after the error
+func errHint(err error, hints ...string) ErrCLI { return ErrCLI{Status: 1, hints: hints, error: err} }
+
+// Execute executes command and prints any errors.
+func Execute() error {
+ err := rootCmd.Execute()
+ if err != nil {
+ if cliErr, ok := err.(ErrCLI); ok {
+ if !cliErr.quiet {
+ printErrHint(cliErr, cliErr.hints...)
+ }
+ } else {
+ printErr(err)
+ }
+ }
+ return err
+}
diff --git a/client/go/cmd/status.go b/client/go/cmd/status.go
index c72df481547..711dba4aa9d 100644
--- a/client/go/cmd/status.go
+++ b/client/go/cmd/status.go
@@ -20,9 +20,10 @@ var statusCmd = &cobra.Command{
Short: "Verify that a service is ready to use (query by default)",
Example: `$ vespa status query`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
- waitForService("query", 0)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return waitForService("query", 0)
},
}
@@ -31,9 +32,10 @@ var statusQueryCmd = &cobra.Command{
Short: "Verify that the query service is ready to use (default)",
Example: `$ vespa status query`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
- Run: func(cmd *cobra.Command, args []string) {
- waitForService("query", 0)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return waitForService("query", 0)
},
}
@@ -42,9 +44,10 @@ var statusDocumentCmd = &cobra.Command{
Short: "Verify that the document service is ready to use",
Example: `$ vespa status document`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
- Run: func(cmd *cobra.Command, args []string) {
- waitForService("document", 0)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return waitForService("document", 0)
},
}
@@ -53,8 +56,9 @@ var statusDeployCmd = &cobra.Command{
Short: "Verify that the deploy service is ready to use",
Example: `$ vespa status deploy`,
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
- Run: func(cmd *cobra.Command, args []string) {
- waitForService("deploy", 0)
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return waitForService("deploy", 0)
},
}
diff --git a/client/go/cmd/status_test.go b/client/go/cmd/status_test.go
index 757ef5f3b06..631aa511459 100644
--- a/client/go/cmd/status_test.go
+++ b/client/go/cmd/status_test.go
@@ -82,7 +82,7 @@ func assertQueryStatusError(target string, args []string, t *testing.T) {
cmd = append(cmd, args...)
_, outErr := execute(command{args: cmd}, t, client)
assert.Equal(t,
- "Error: Container (query API) at "+target+" is not ready\nStatus 500\n",
+ "Error: Container (query API) at "+target+" is not ready: status 500\n",
outErr,
"vespa status container")
}
diff --git a/client/go/cmd/test.go b/client/go/cmd/test.go
index 262b57eff33..179a8043a2a 100644
--- a/client/go/cmd/test.go
+++ b/client/go/cmd/test.go
@@ -9,9 +9,6 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
- "github.com/spf13/cobra"
- "github.com/vespa-engine/vespa/client/go/util"
- "github.com/vespa-engine/vespa/client/go/vespa"
"io/ioutil"
"math"
"net/http"
@@ -20,6 +17,10 @@ import (
"path/filepath"
"strings"
"time"
+
+ "github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/util"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
func init() {
@@ -39,8 +40,13 @@ See https://cloud.vespa.ai/en/reference/testing.html for details.`,
$ vespa test src/test/application/tests/system-test/feed-and-query.json`,
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
- Run: func(cmd *cobra.Command, args []string) {
- if count, failed := runTests(args[0], false); len(failed) != 0 {
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ count, failed, err := runTests(args[0], false)
+ if err != nil {
+ return err
+ }
+ if len(failed) != 0 {
plural := "s"
if count == 1 {
plural = ""
@@ -49,26 +55,27 @@ $ vespa test src/test/application/tests/system-test/feed-and-query.json`,
for _, test := range failed {
fmt.Fprintln(stdout, test)
}
- exitFunc(3)
+ return ErrCLI{Status: 3, error: fmt.Errorf("tests failed"), quiet: true}
} else {
plural := "s"
if count == 1 {
plural = ""
}
fmt.Fprintf(stdout, "\n%s %d test%s OK\n", color.Green("Success:"), count, plural)
+ return nil
}
},
}
-func runTests(rootPath string, dryRun bool) (int, []string) {
+func runTests(rootPath string, dryRun bool) (int, []string, error) {
count := 0
failed := make([]string, 0)
if stat, err := os.Stat(rootPath); err != nil {
- fatalErrHint(err, "See https://cloud.vespa.ai/en/reference/testing")
+ return 0, nil, errHint(err, "See https://cloud.vespa.ai/en/reference/testing")
} else if stat.IsDir() {
tests, err := ioutil.ReadDir(rootPath) // TODO: Use os.ReadDir when >= 1.16 is required.
if err != nil {
- fatalErrHint(err, "See https://cloud.vespa.ai/en/reference/testing")
+ return 0, nil, errHint(err, "See https://cloud.vespa.ai/en/reference/testing")
}
context := testContext{testsPath: rootPath, dryRun: dryRun}
previousFailed := false
@@ -79,7 +86,10 @@ func runTests(rootPath string, dryRun bool) (int, []string) {
fmt.Fprintln(stdout, "")
previousFailed = false
}
- failure := runTest(testPath, context)
+ failure, err := runTest(testPath, context)
+ if err != nil {
+ return 0, nil, err
+ }
if failure != "" {
failed = append(failed, failure)
previousFailed = true
@@ -88,27 +98,30 @@ func runTests(rootPath string, dryRun bool) (int, []string) {
}
}
} else if strings.HasSuffix(stat.Name(), ".json") {
- failure := runTest(rootPath, testContext{testsPath: filepath.Dir(rootPath), dryRun: dryRun})
+ failure, err := runTest(rootPath, testContext{testsPath: filepath.Dir(rootPath), dryRun: dryRun})
+ if err != nil {
+ return 0, nil, err
+ }
if failure != "" {
failed = append(failed, failure)
}
count++
}
if count == 0 {
- fatalErrHint(fmt.Errorf("Failed to find any tests at %s", rootPath), "See https://cloud.vespa.ai/en/reference/testing")
+ return 0, nil, errHint(fmt.Errorf("failed to find any tests at %s", rootPath), "See https://cloud.vespa.ai/en/reference/testing")
}
- return count, failed
+ return count, failed, nil
}
// Runs the test at the given path, and returns the specified test name if the test fails
-func runTest(testPath string, context testContext) string {
+func runTest(testPath string, context testContext) (string, error) {
var test test
testBytes, err := ioutil.ReadFile(testPath)
if err != nil {
- fatalErrHint(err, "See https://cloud.vespa.ai/en/reference/testing")
+ return "", errHint(err, "See https://cloud.vespa.ai/en/reference/testing")
}
if err = json.Unmarshal(testBytes, &test); err != nil {
- fatalErrHint(err, fmt.Sprintf("Failed parsing test at %s", testPath), "See https://cloud.vespa.ai/en/reference/testing")
+ return "", errHint(fmt.Errorf("failed parsing test at %s: %w", testPath, err), "See https://cloud.vespa.ai/en/reference/testing")
}
testName := test.Name
@@ -122,12 +135,12 @@ func runTest(testPath string, context testContext) string {
defaultParameters, err := getParameters(test.Defaults.ParametersRaw, filepath.Dir(testPath))
if err != nil {
fmt.Fprintln(stderr)
- fatalErrHint(err, fmt.Sprintf("Invalid default parameters for %s", testName), "See https://cloud.vespa.ai/en/reference/testing")
+ return "", errHint(fmt.Errorf("invalid default parameters for %s: %w", testName, err), "See https://cloud.vespa.ai/en/reference/testing")
}
if len(test.Steps) == 0 {
fmt.Fprintln(stderr)
- fatalErrHint(fmt.Errorf("a test must have at least one step, but none were found in %s", testPath), "See https://cloud.vespa.ai/en/reference/testing")
+ return "", errHint(fmt.Errorf("a test must have at least one step, but none were found in %s", testPath), "See https://cloud.vespa.ai/en/reference/testing")
}
for i, step := range test.Steps {
stepName := fmt.Sprintf("Step %d", i+1)
@@ -137,12 +150,12 @@ func runTest(testPath string, context testContext) string {
failure, longFailure, err := verify(step, test.Defaults.Cluster, defaultParameters, context)
if err != nil {
fmt.Fprintln(stderr)
- fatalErrHint(err, fmt.Sprintf("Error in %s", stepName), "See https://cloud.vespa.ai/en/reference/testing")
+ return "", errHint(fmt.Errorf("error in %s: %w", stepName, err), "See https://cloud.vespa.ai/en/reference/testing")
}
if !context.dryRun {
if failure != "" {
fmt.Fprintf(stdout, " %s\n%s:\n%s\n", color.Red("failed"), stepName, longFailure)
- return fmt.Sprintf("%s: %s: %s", testName, stepName, failure)
+ return fmt.Sprintf("%s: %s: %s", testName, stepName, failure), nil
}
if i == 0 {
fmt.Fprintf(stdout, " ")
@@ -153,7 +166,7 @@ func runTest(testPath string, context testContext) string {
if !context.dryRun {
fmt.Fprintln(stdout, color.Green(" OK"))
}
- return ""
+ return "", nil
}
// Asserts specified response is obtained for request, or returns a failure message, or an error if this fails
@@ -194,7 +207,11 @@ func verify(step step, defaultCluster string, defaultParameters map[string]strin
}
externalEndpoint := requestUrl.IsAbs()
if !externalEndpoint && !context.dryRun {
- service, err = context.target().Service("query", 0, 0, cluster)
+ target, err := context.target()
+ if err != nil {
+ return "", "", err
+ }
+ service, err = target.Service("query", 0, 0, cluster)
if err != nil {
return "", "", err
}
@@ -453,9 +470,13 @@ type testContext struct {
dryRun bool
}
-func (t *testContext) target() vespa.Target {
+func (t *testContext) target() (vespa.Target, error) {
if t.lazyTarget == nil {
- t.lazyTarget = getTarget()
+ target, err := getTarget()
+ if err != nil {
+ return nil, err
+ }
+ t.lazyTarget = target
}
- return t.lazyTarget
+ return t.lazyTarget, nil
}
diff --git a/client/go/cmd/test_test.go b/client/go/cmd/test_test.go
index 6649353df77..2c59bbbc030 100644
--- a/client/go/cmd/test_test.go
+++ b/client/go/cmd/test_test.go
@@ -5,9 +5,6 @@
package cmd
import (
- "fmt"
- "github.com/vespa-engine/vespa/client/go/util"
- "github.com/vespa-engine/vespa/client/go/vespa"
"io/ioutil"
"net/http"
"net/url"
@@ -16,6 +13,9 @@ import (
"strings"
"testing"
+ "github.com/vespa-engine/vespa/client/go/util"
+ "github.com/vespa-engine/vespa/client/go/vespa"
+
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,6 @@ func TestSuite(t *testing.T) {
requests = append(requests, createSearchRequest(baseUrl+"/search/"))
}
assertRequests(requests, client, t)
- fmt.Println(outBytes)
assert.Equal(t, string(expectedBytes), outBytes)
assert.Equal(t, "", errBytes)
}
@@ -51,7 +50,7 @@ func TestIllegalFileReference(t *testing.T) {
client.NextStatus(200)
_, errBytes := execute(command{args: []string{"test", "testdata/tests/production-test/illegal-reference.json"}}, t, client)
assertRequests([]*http.Request{createRequest("GET", "http://127.0.0.1:8080/search/", "{}")}, client, t)
- assert.Equal(t, "\nError: path may not point outside src/test/application, but 'foo/../../../../this-is-not-ok.json' does\nHint: Error in Step 2\nHint: See https://cloud.vespa.ai/en/reference/testing\n", errBytes)
+ assert.Equal(t, "\nError: error in Step 2: path may not point outside src/test/application, but 'foo/../../../../this-is-not-ok.json' does\nHint: See https://cloud.vespa.ai/en/reference/testing\n", errBytes)
}
func TestProductionTest(t *testing.T) {
@@ -72,7 +71,7 @@ func TestTestWithoutAssertions(t *testing.T) {
func TestSuiteWithoutTests(t *testing.T) {
client := &mockHttpClient{}
_, errBytes := execute(command{args: []string{"test", "testdata/tests/staging-test"}}, t, client)
- assert.Equal(t, "Error: Failed to find any tests at testdata/tests/staging-test\nHint: See https://cloud.vespa.ai/en/reference/testing\n", errBytes)
+ assert.Equal(t, "Error: failed to find any tests at testdata/tests/staging-test\nHint: See https://cloud.vespa.ai/en/reference/testing\n", errBytes)
}
func TestSingleTest(t *testing.T) {
diff --git a/client/go/cmd/testdata/sample-apps-master.zip b/client/go/cmd/testdata/sample-apps-master.zip
index 6ad49361072..c8fb40af713 100644
--- a/client/go/cmd/testdata/sample-apps-master.zip
+++ b/client/go/cmd/testdata/sample-apps-master.zip
Binary files differ
diff --git a/client/go/cmd/version.go b/client/go/cmd/version.go
index d2760402851..2660dfe7d61 100644
--- a/client/go/cmd/version.go
+++ b/client/go/cmd/version.go
@@ -46,14 +46,14 @@ var versionCmd = &cobra.Command{
Use: "version",
Short: "Show current version and check for updates",
DisableAutoGenTag: true,
+ SilenceUsage: true,
Args: cobra.ExactArgs(0),
- Run: func(cmd *cobra.Command, args []string) {
+ RunE: func(cmd *cobra.Command, args []string) error {
log.Printf("vespa version %s compiled with %v on %v/%v", build.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
if !skipVersionCheck && sp.isTerminal() {
- if err := checkVersion(); err != nil {
- fatalErr(err)
- }
+ return checkVersion()
}
+ return nil
},
}
diff --git a/client/go/cmd/vespa/main.go b/client/go/cmd/vespa/main.go
index 32828b15aa4..f7ce064f3a5 100644
--- a/client/go/cmd/vespa/main.go
+++ b/client/go/cmd/vespa/main.go
@@ -5,12 +5,17 @@
package main
import (
- "github.com/vespa-engine/vespa/client/go/cmd"
"os"
+
+ "github.com/vespa-engine/vespa/client/go/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
- os.Exit(1)
+ if cliErr, ok := err.(cmd.ErrCLI); ok {
+ os.Exit(cliErr.Status)
+ } else {
+ os.Exit(1)
+ }
}
}