summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorEirik Nygaard <eirik.nygaard@yahooinc.com>2022-03-07 08:41:46 +0100
committerGitHub <noreply@github.com>2022-03-07 08:41:46 +0100
commit2e05df2de19c2d5b87befa2ee6c4fd182dcb5630 (patch)
treef104afcd7a6d17aed3434473e327da049baa363f /client
parent77fa0177506fc1633d4097c354cbdf6e79cb1209 (diff)
parent657b728d3a3df39ca724b853514b1f244b5f7d5e (diff)
Merge pull request #21555 from vespa-engine/mpolden/refactor
Refactor cmd package to eliminate package-level state
Diffstat (limited to 'client')
-rw-r--r--client/go/.dir-locals.el3
-rw-r--r--client/go/auth/auth0/auth0.go (renamed from client/go/auth0/auth0.go)8
-rw-r--r--client/go/auth/zts/zts.go (renamed from client/go/zts/zts.go)4
-rw-r--r--client/go/auth/zts/zts_test.go (renamed from client/go/zts/zts_test.go)0
-rw-r--r--client/go/cmd/api_key.go78
-rw-r--r--client/go/cmd/api_key_test.go23
-rw-r--r--client/go/cmd/auth.go32
-rw-r--r--client/go/cmd/cert.go127
-rw-r--r--client/go/cmd/cert_test.go95
-rw-r--r--client/go/cmd/clone.go91
-rw-r--r--client/go/cmd/clone_list.go14
-rw-r--r--client/go/cmd/clone_list_test.go4
-rw-r--r--client/go/cmd/clone_test.go25
-rw-r--r--client/go/cmd/command_tester_test.go124
-rw-r--r--client/go/cmd/config.go313
-rw-r--r--client/go/cmd/config_test.go123
-rw-r--r--client/go/cmd/curl.go110
-rw-r--r--client/go/cmd/curl_test.go40
-rw-r--r--client/go/cmd/deploy.go271
-rw-r--r--client/go/cmd/deploy_test.go44
-rw-r--r--client/go/cmd/document.go255
-rw-r--r--client/go/cmd/document_test.go49
-rw-r--r--client/go/cmd/helpers.go368
-rw-r--r--client/go/cmd/log.go114
-rw-r--r--client/go/cmd/log_test.go52
-rw-r--r--client/go/cmd/login.go54
-rw-r--r--client/go/cmd/logout.go52
-rw-r--r--client/go/cmd/man.go38
-rw-r--r--client/go/cmd/man_test.go5
-rw-r--r--client/go/cmd/prod.go307
-rw-r--r--client/go/cmd/prod_test.go51
-rw-r--r--client/go/cmd/query.go63
-rw-r--r--client/go/cmd/query_test.go36
-rw-r--r--client/go/cmd/root.go501
-rw-r--r--client/go/cmd/status.go122
-rw-r--r--client/go/cmd/status_test.go30
-rw-r--r--client/go/cmd/test.go106
-rw-r--r--client/go/cmd/test_test.go93
-rw-r--r--client/go/cmd/testutil_test.go49
-rw-r--r--client/go/cmd/version.go72
-rw-r--r--client/go/cmd/version_test.go52
-rw-r--r--client/go/cmd/vespa/main.go18
-rw-r--r--client/go/mock/http.go (renamed from client/go/mock/mock.go)0
-rw-r--r--client/go/mock/process.go19
-rw-r--r--client/go/util/http.go36
-rw-r--r--client/go/util/http_test.go55
-rw-r--r--client/go/version/version.go9
-rw-r--r--client/go/vespa/deploy.go7
-rw-r--r--client/go/vespa/target.go17
-rw-r--r--client/go/vespa/target_cloud.go34
-rw-r--r--client/go/vespa/target_custom.go14
-rw-r--r--client/go/vespa/target_test.go25
52 files changed, 2097 insertions, 2135 deletions
diff --git a/client/go/.dir-locals.el b/client/go/.dir-locals.el
new file mode 100644
index 00000000000..1c7c82921be
--- /dev/null
+++ b/client/go/.dir-locals.el
@@ -0,0 +1,3 @@
+((go-mode
+ . ((eglot-workspace-configuration
+ . ((:gopls . ((staticcheck . t))))))))
diff --git a/client/go/auth0/auth0.go b/client/go/auth/auth0/auth0.go
index 2f7040b3d37..52ba3f085a4 100644
--- a/client/go/auth0/auth0.go
+++ b/client/go/auth/auth0/auth0.go
@@ -166,13 +166,7 @@ func (a *Auth0) PrepareSystem(ctx context.Context) (*System, error) {
res, err := tr.Refresh(ctx, a.system)
if err != nil {
- // ask and guide the user through the login process:
- fmt.Println(fmt.Errorf("failed to renew access token, %s", err))
- fmt.Print("\n")
- s, err = RunLogin(ctx, a, true)
- if err != nil {
- return nil, err
- }
+ return nil, fmt.Errorf("failed to renew access token: %w: %s", err, "re-authenticate with 'vespa auth login'")
} else {
// persist the updated system with renewed access token
s.AccessToken = res.AccessToken
diff --git a/client/go/zts/zts.go b/client/go/auth/zts/zts.go
index 538971ebdd8..d288c2050d9 100644
--- a/client/go/zts/zts.go
+++ b/client/go/auth/zts/zts.go
@@ -16,12 +16,12 @@ const DefaultURL = "https://zts.athenz.ouroath.com:4443"
// Client is a client for Athenz ZTS, an authentication token service.
type Client struct {
- client util.HttpClient
+ client util.HTTPClient
tokenURL *url.URL
}
// NewClient creates a new client for an Athenz ZTS service located at serviceURL.
-func NewClient(serviceURL string, client util.HttpClient) (*Client, error) {
+func NewClient(serviceURL string, client util.HTTPClient) (*Client, error) {
tokenURL, err := url.Parse(serviceURL)
if err != nil {
return nil, err
diff --git a/client/go/zts/zts_test.go b/client/go/auth/zts/zts_test.go
index 0eec085aadb..0eec085aadb 100644
--- a/client/go/zts/zts_test.go
+++ b/client/go/auth/zts/zts_test.go
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go
index c6b6ae1d1e3..5e23cdc9ebd 100644
--- a/client/go/cmd/api_key.go
+++ b/client/go/cmd/api_key.go
@@ -14,9 +14,12 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var overwriteKey bool
-
-const apiKeyLongDoc = `Create a new user API key for authentication with Vespa Cloud.
+func newAPIKeyCmd(cli *CLI, deprecated bool) *cobra.Command {
+ var overwriteKey bool
+ cmd := &cobra.Command{
+ Use: "api-key",
+ Short: "Create a new user API key for authentication with Vespa Cloud",
+ Long: `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
@@ -34,65 +37,40 @@ can be useful in continuous integration systems.
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)
- deprecatedApiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key")
- deprecatedApiKeyCmd.MarkPersistentFlagRequired(applicationFlag)
-}
-
-func apiKeyExample() string {
- return "$ vespa auth api-key -a my-tenant.my-app.my-instance"
-}
-
-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),
- RunE: doApiKey,
-}
-
-// TODO: Remove this after 2022-06-01
-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",
- RunE: doApiKey,
+always be used. It's not possible to specify a tenant-specific key.`,
+ Example: "$ vespa auth api-key -a my-tenant.my-app.my-instance",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(0),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return doApiKey(cli, overwriteKey, args)
+ },
+ }
+ if deprecated {
+ cmd.Deprecated = "use 'vespa auth api-key' instead"
+ }
+ cmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key")
+ cmd.MarkPersistentFlagRequired(applicationFlag)
+ return cmd
}
-func doApiKey(_ *cobra.Command, _ []string) error {
- cfg, err := LoadConfig()
- if err != nil {
- return fmt.Errorf("could not load config: %w", err)
- }
- app, err := getApplication()
+func doApiKey(cli *CLI, overwriteKey bool, args []string) error {
+ app, err := cli.config.application()
if err != nil {
return err
}
- targetType, err := getTargetType()
+ targetType, err := cli.config.targetType()
if err != nil {
return err
}
- system, err := getSystem(targetType)
+ system, err := cli.system(targetType)
if err != nil {
return err
}
- apiKeyFile := cfg.APIKeyPath(app.Tenant)
+ apiKeyFile := cli.config.apiKeyPath(app.Tenant)
if util.PathExists(apiKeyFile) && !overwriteKey {
err := fmt.Errorf("refusing to overwrite %s", apiKeyFile)
- printErrHint(err, "Use -f to overwrite it")
+ cli.printErrHint(err, "Use -f to overwrite it")
printPublicKey(system, apiKeyFile, app.Tenant)
return ErrCLI{error: err, quiet: true}
}
@@ -101,7 +79,7 @@ func doApiKey(_ *cobra.Command, _ []string) error {
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)
+ cli.printSuccess("API private key written to ", apiKeyFile)
return printPublicKey(system, apiKeyFile, app.Tenant)
} else {
return fmt.Errorf("failed to write: %s: %w", apiKeyFile, err)
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go
index ba697b69d9f..3e3a7fa0a31 100644
--- a/client/go/cmd/api_key_test.go
+++ b/client/go/cmd/api_key_test.go
@@ -4,7 +4,6 @@
package cmd
import (
- "path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@@ -20,16 +19,20 @@ func TestAPIKey(t *testing.T) {
}
func testAPIKey(t *testing.T, subcommand []string) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
- keyFile := filepath.Join(homeDir, "t1.api-key.pem")
+ cli, stdout, stderr := newTestCLI(t)
- execute(command{args: []string{"config", "set", "target", "cloud"}, homeDir: homeDir}, t, nil)
+ err := cli.Run("config", "set", "target", "cloud")
+ assert.Nil(t, err)
args := append(subcommand, "-a", "t1.a1.i1")
- out, _ := execute(command{args: args, homeDir: homeDir}, t, nil)
- assert.Contains(t, out, "Success: API private key written to "+keyFile+"\n")
-
- out, outErr := execute(command{args: args, homeDir: homeDir}, t, nil)
- 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")
+ err = cli.Run(args...)
+ assert.Nil(t, err)
+ assert.Equal(t, "", stderr.String())
+ assert.Contains(t, stdout.String(), "Success: API private key written to")
+
+ err = cli.Run(subcommand...)
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), "Error: refusing to overwrite")
+ assert.Contains(t, stderr.String(), "Hint: Use -f to overwrite it\n")
+ assert.Contains(t, stdout.String(), "This is your public key")
}
diff --git a/client/go/cmd/auth.go b/client/go/cmd/auth.go
index 592c18c8b22..453d2296b08 100644
--- a/client/go/cmd/auth.go
+++ b/client/go/cmd/auth.go
@@ -6,24 +6,16 @@ import (
"github.com/spf13/cobra"
)
-func init() {
- rootCmd.AddCommand(authCmd)
- rootCmd.AddCommand(deprecatedCertCmd)
- rootCmd.AddCommand(deprecatedApiKeyCmd)
- authCmd.AddCommand(certCmd)
- authCmd.AddCommand(apiKeyCmd)
- authCmd.AddCommand(loginCmd)
- authCmd.AddCommand(logoutCmd)
-}
-
-var authCmd = &cobra.Command{
- Use: "auth",
- Short: "Manage Vespa Cloud credentials",
- Long: `Manage Vespa Cloud credentials.`,
- DisableAutoGenTag: true,
- SilenceUsage: false,
- Args: cobra.MinimumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- return fmt.Errorf("invalid command: %s", args[0])
- },
+func newAuthCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "auth",
+ Short: "Manage Vespa Cloud credentials",
+ Long: `Manage Vespa Cloud credentials.`,
+ DisableAutoGenTag: true,
+ 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 672442b9ad9..caa32b0a963 100644
--- a/client/go/cmd/cert.go
+++ b/client/go/cmd/cert.go
@@ -16,25 +16,15 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var (
- noApplicationPackage bool
- overwriteCertificate bool
-)
-
-func init() {
- certCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key")
- certCmd.Flags().BoolVarP(&noApplicationPackage, "no-add", "N", false, "Do not add certificate to the application package")
- certCmd.MarkPersistentFlagRequired(applicationFlag)
-
- deprecatedCertCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key")
- deprecatedCertCmd.MarkPersistentFlagRequired(applicationFlag)
-
- certCmd.AddCommand(certAddCmd)
- certAddCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate")
- certAddCmd.MarkPersistentFlagRequired(applicationFlag)
-}
-
-var longDoc = `Create a new private key and self-signed certificate for Vespa Cloud deployment.
+func newCertCmd(cli *CLI, deprecated bool) *cobra.Command {
+ var (
+ noApplicationPackage bool
+ overwriteCertificate bool
+ )
+ cmd := &cobra.Command{
+ Use: "cert",
+ Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment",
+ Long: `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
@@ -56,54 +46,55 @@ Example of loading certificate and key from custom paths:
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.`
-
-var certCmd = &cobra.Command{
- Use: "cert",
- Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment",
- Long: longDoc,
- Example: `$ vespa auth cert -a my-tenant.my-app.my-instance
+application-specific key.`,
+ Example: `$ vespa auth cert -a my-tenant.my-app.my-instance
$ vespa auth cert -a my-tenant.my-app.my-instance path/to/application/package`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.MaximumNArgs(1),
- RunE: doCert,
-}
-
-// TODO: Remove this after 2022-06-01
-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,
- RunE: doCert,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return doCert(cli, overwriteCertificate, noApplicationPackage, args)
+ },
+ }
+ cmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key")
+ if deprecated {
+ // TODO: Remove this after 2022-06-01
+ cmd.Deprecated = "use 'vespa auth cert' instead"
+ } else {
+ cmd.Flags().BoolVarP(&noApplicationPackage, "no-add", "N", false, "Do not add certificate to the application package")
+ }
+ cmd.MarkPersistentFlagRequired(applicationFlag)
+ return cmd
}
-var certAddCmd = &cobra.Command{
- Use: "add",
- Short: "Add certificate to application package",
- Long: `Add an existing self-signed certificate for Vespa Cloud deployment to your application package.
+func newCertAddCmd(cli *CLI) *cobra.Command {
+ var overwriteCertificate bool
+ cmd := &cobra.Command{
+ Use: "add",
+ Short: "Add certificate to application package",
+ Long: `Add an existing self-signed certificate for Vespa Cloud deployment to your application package.
The certificate will be loaded from the Vespa CLI home directory (see 'vespa
help config') by default.
The location of the application package can be specified as an argument to this
command (default '.').`,
- Example: `$ vespa auth cert add -a my-tenant.my-app.my-instance
+ Example: `$ vespa auth cert add -a my-tenant.my-app.my-instance
$ vespa auth cert add -a my-tenant.my-app.my-instance path/to/application/package`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.MaximumNArgs(1),
- RunE: doCertAdd,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return doCertAdd(cli, overwriteCertificate, args)
+ },
+ }
+ cmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate")
+ cmd.MarkPersistentFlagRequired(applicationFlag)
+ return cmd
}
-func doCert(_ *cobra.Command, args []string) error {
- app, err := getApplication()
+func doCert(cli *CLI, overwriteCertificate, noApplicationPackage bool, args []string) error {
+ app, err := cli.config.application()
if err != nil {
return err
}
@@ -114,15 +105,11 @@ func doCert(_ *cobra.Command, args []string) error {
return err
}
}
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- privateKeyFile, err := cfg.PrivateKeyPath(app)
+ privateKeyFile, err := cli.config.privateKeyPath(app)
if err != nil {
return err
}
- certificateFile, err := cfg.CertificatePath(app)
+ certificateFile, err := cli.config.certificatePath(app)
if err != nil {
return err
}
@@ -169,15 +156,15 @@ func doCert(_ *cobra.Command, args []string) error {
return fmt.Errorf("could not write private key: %w", err)
}
if !noApplicationPackage {
- printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
+ cli.printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
}
- printSuccess("Certificate written to ", color.CyanString(certificateFile))
- printSuccess("Private key written to ", color.CyanString(privateKeyFile))
+ cli.printSuccess("Certificate written to ", color.CyanString(certificateFile))
+ cli.printSuccess("Private key written to ", color.CyanString(privateKeyFile))
return nil
}
-func doCertAdd(_ *cobra.Command, args []string) error {
- app, err := getApplication()
+func doCertAdd(cli *CLI, overwriteCertificate bool, args []string) error {
+ app, err := cli.config.application()
if err != nil {
return err
}
@@ -185,11 +172,7 @@ func doCertAdd(_ *cobra.Command, args []string) error {
if err != nil {
return err
}
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- certificateFile, err := cfg.CertificatePath(app)
+ certificateFile, err := cli.config.certificatePath(app)
if err != nil {
return err
}
@@ -226,6 +209,6 @@ func doCertAdd(_ *cobra.Command, args []string) error {
return fmt.Errorf("could not copy certificate file to application: %w", err)
}
- printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
+ cli.printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
return nil
}
diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go
index 51e99938d2b..e5837170d15 100644
--- a/client/go/cmd/cert_test.go
+++ b/client/go/cmd/cert_test.go
@@ -25,24 +25,28 @@ func TestCert(t *testing.T) {
}
func testCert(t *testing.T, subcommand []string) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := mockApplicationPackage(t, false)
+
+ cli, stdout, stderr := newTestCLI(t)
args := append(subcommand, "-a", "t1.a1.i1", pkgDir)
- out, _ := execute(command{args: args, homeDir: homeDir}, t, nil)
+ err := cli.Run(args...)
+ assert.Nil(t, err)
app, err := vespa.ApplicationFromString("t1.a1.i1")
assert.Nil(t, err)
appDir := filepath.Join(pkgDir, "src", "main", "application")
pkgCertificate := filepath.Join(appDir, "security", "clients.pem")
+ homeDir := cli.config.homeDir
certificate := filepath.Join(homeDir, app.String(), "data-plane-public-cert.pem")
privateKey := filepath.Join(homeDir, app.String(), "data-plane-private-key.pem")
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Certificate written to %s\nSuccess: Private key written to %s\n", pkgCertificate, certificate, privateKey), out)
+ 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), stdout.String())
args = append(subcommand, "-a", "t1.a1.i1", pkgDir)
- _, outErr := execute(command{args: args, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
+ err = cli.Run(args...)
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
}
func TestCertCompressedPackage(t *testing.T) {
@@ -55,7 +59,6 @@ func TestCertCompressedPackage(t *testing.T) {
}
func testCertCompressedPackage(t *testing.T, subcommand []string) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := mockApplicationPackage(t, true)
zipFile := filepath.Join(pkgDir, "target", "application.zip")
err := os.MkdirAll(filepath.Dir(zipFile), 0755)
@@ -63,71 +66,71 @@ func testCertCompressedPackage(t *testing.T, subcommand []string) {
_, err = os.Create(zipFile)
assert.Nil(t, err)
+ cli, stdout, stderr := newTestCLI(t)
+
args := append(subcommand, "-a", "t1.a1.i1", pkgDir)
- _, outErr := execute(command{args: args, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, "Error: cannot add certificate to compressed application package")
+ err = cli.Run(args...)
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), "Error: cannot add certificate to compressed application package")
err = os.Remove(zipFile)
assert.Nil(t, err)
args = append(subcommand, "-f", "-a", "t1.a1.i1", pkgDir)
- out, _ := execute(command{args: args, homeDir: homeDir}, t, nil)
- assert.Contains(t, out, "Success: Certificate written to")
- assert.Contains(t, out, "Success: Private key written to")
+ err = cli.Run(args...)
+ assert.Nil(t, err)
+ assert.Contains(t, stdout.String(), "Success: Certificate written to")
+ assert.Contains(t, stdout.String(), "Success: Private key written to")
}
func TestCertAdd(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
- execute(command{args: []string{"auth", "cert", "-N", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
+ cli, stdout, stderr := newTestCLI(t)
+ err := cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
+ assert.Nil(t, err)
pkgDir := mockApplicationPackage(t, false)
- out, _ := execute(command{args: []string{"auth", "cert", "add", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil)
+ stdout.Reset()
+ err = cli.Run("auth", "cert", "add", "-a", "t1.a1.i1", pkgDir)
+ assert.Nil(t, err)
appDir := filepath.Join(pkgDir, "src", "main", "application")
pkgCertificate := filepath.Join(appDir, "security", "clients.pem")
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\n", pkgCertificate), out)
+ assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\n", pkgCertificate), stdout.String())
- out, outErr := execute(command{args: []string{"auth", "cert", "add", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", out)
- assert.Contains(t, outErr, fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
- out, _ = execute(command{args: []string{"auth", "cert", "add", "-f", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil)
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\n", pkgCertificate), out)
+ err = cli.Run("auth", "cert", "add", "-a", "t1.a1.i1", pkgDir)
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
+ stdout.Reset()
+ err = cli.Run("auth", "cert", "add", "-f", "-a", "t1.a1.i1", pkgDir)
+ assert.Nil(t, err)
+ assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\n", pkgCertificate), stdout.String())
}
func TestCertNoAdd(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
- out, _ := execute(command{args: []string{"auth", "cert", "-N", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
+ cli, stdout, stderr := newTestCLI(t)
+
+ err := cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
+ assert.Nil(t, err)
+ homeDir := cli.config.homeDir
app, err := vespa.ApplicationFromString("t1.a1.i1")
assert.Nil(t, err)
certificate := filepath.Join(homeDir, app.String(), "data-plane-public-cert.pem")
privateKey := filepath.Join(homeDir, app.String(), "data-plane-private-key.pem")
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Private key written to %s\n", certificate, privateKey), out)
+ assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Private key written to %s\n", certificate, privateKey), stdout.String())
- _, outErr := execute(command{args: []string{"auth", "cert", "-N", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, fmt.Sprintf("Error: private key %s already exists", privateKey))
+ err = cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), fmt.Sprintf("Error: private key %s already exists", privateKey))
require.Nil(t, os.Remove(privateKey))
- _, outErr = execute(command{args: []string{"auth", "cert", "-N", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
- assert.Contains(t, outErr, fmt.Sprintf("Error: certificate %s already exists", certificate))
- out, _ = execute(command{args: []string{"auth", "cert", "-N", "-f", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Private key written to %s\n", certificate, privateKey), out)
-}
+ stderr.Reset()
+ err = cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
+ assert.NotNil(t, err)
+ assert.Contains(t, stderr.String(), fmt.Sprintf("Error: certificate %s already exists", certificate))
-func mockApplicationPackage(t *testing.T, java bool) string {
- dir := t.TempDir()
- appDir := filepath.Join(dir, "src", "main", "application")
- if err := os.MkdirAll(appDir, 0755); err != nil {
- t.Fatal(err)
- }
- servicesXML := filepath.Join(appDir, "services.xml")
- if _, err := os.Create(servicesXML); err != nil {
- t.Fatal(err)
- }
- if java {
- if _, err := os.Create(filepath.Join(dir, "pom.xml")); err != nil {
- t.Fatal(err)
- }
- }
- return dir
+ stdout.Reset()
+ err = cli.Run("auth", "cert", "-N", "-f", "-a", "t1.a1.i1")
+ assert.Nil(t, err)
+ assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Private key written to %s\n", certificate, privateKey), stdout.String())
}
diff --git a/client/go/cmd/clone.go b/client/go/cmd/clone.go
index a1add299712..180ec18debf 100644
--- a/client/go/cmd/clone.go
+++ b/client/go/cmd/clone.go
@@ -22,24 +22,15 @@ import (
"github.com/vespa-engine/vespa/client/go/util"
)
-const (
- sampleAppsCacheTTL = time.Hour * 168 // 1 week
- sampleAppsFilename = "sample-apps-master.zip"
-)
-
-var listApps bool
-var forceClone bool
-
-func init() {
- rootCmd.AddCommand(cloneCmd)
- cloneCmd.Flags().BoolVarP(&listApps, "list", "l", false, "List available sample applications")
- cloneCmd.Flags().BoolVarP(&forceClone, "force", "f", false, "Ignore cache and force downloading the latest sample application from GitHub")
-}
-
-var cloneCmd = &cobra.Command{
- Use: "clone sample-application-path target-directory",
- Short: "Create files and directory structure for a new Vespa application from a sample application",
- Long: `Create files and directory structure for a new Vespa application
+func newCloneCmd(cli *CLI) *cobra.Command {
+ var (
+ listApps bool
+ forceClone bool
+ )
+ cmd := &cobra.Command{
+ Use: "clone sample-application-path target-directory",
+ Short: "Create files and directory structure for a new Vespa application from a sample application",
+ Long: `Create files and directory structure for a new Vespa application
from a sample application.
Sample applications are downloaded from
@@ -48,29 +39,33 @@ https://github.com/vespa-engine/sample-apps.
By default sample applications are cached in the user's cache directory. This
directory can be overriden by setting the VESPA_CLI_CACHE_DIR environment
variable.`,
- Example: "$ vespa clone vespa-cloud/album-recommendation my-app",
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- if listApps {
- apps, err := listSampleApps()
- if err != nil {
- return fmt.Errorf("could not list sample applications: %w", err)
+ Example: "$ vespa clone vespa-cloud/album-recommendation my-app",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if listApps {
+ apps, err := listSampleApps(cli.httpClient)
+ if err != nil {
+ return fmt.Errorf("could not list sample applications: %w", err)
+ }
+ for _, app := range apps {
+ log.Print(app)
+ }
+ return nil
}
- for _, app := range apps {
- log.Print(app)
+ if len(args) != 2 {
+ return fmt.Errorf("expected exactly 2 arguments, got %d", len(args))
}
- return nil
- }
- if len(args) != 2 {
- return fmt.Errorf("expected exactly 2 arguments, got %d", len(args))
- }
- return cloneApplication(args[0], args[1])
- },
+ return cloneApplication(cli, args[0], args[1], forceClone)
+ },
+ }
+ cmd.Flags().BoolVarP(&listApps, "list", "l", false, "List available sample applications")
+ cmd.Flags().BoolVarP(&forceClone, "force", "f", false, "Ignore cache and force downloading the latest sample application from GitHub")
+ return cmd
}
-func cloneApplication(applicationName string, applicationDir string) error {
- zipFile, err := openSampleAppsZip()
+func cloneApplication(cli *CLI, applicationName string, applicationDir string, force bool) error {
+ zipFile, err := openSampleAppsZip(force, cli)
if err != nil {
return err
}
@@ -107,26 +102,26 @@ func cloneApplication(applicationName string, applicationDir string) error {
return nil
}
-func useCache(stat os.FileInfo) (bool, error) {
- if forceClone {
+func useCache(force bool, stat os.FileInfo) (bool, error) {
+ if force {
return false, nil
}
- expiry := stat.ModTime().Add(sampleAppsCacheTTL)
+ expiry := stat.ModTime().Add(time.Hour * 168) // 1 week
return stat.Size() > 0 && time.Now().Before(expiry), nil
}
-func fetchSampleAppsZip(destination string) error {
+func fetchSampleAppsZip(destination string, cli *CLI) error {
f, err := ioutil.TempFile(filepath.Dir(destination), "sample-apps")
if err != nil {
return fmt.Errorf("could not create temporary file: %w", err)
}
defer f.Close()
- return util.Spinner(stderr, color.YellowString("Downloading sample apps ..."), func() error {
+ return util.Spinner(cli.Stderr, color.YellowString("Downloading sample apps ..."), func() error {
request, err := http.NewRequest("GET", "https://github.com/vespa-engine/sample-apps/archive/refs/heads/master.zip", nil)
if err != nil {
return fmt.Errorf("invalid url: %w", err)
}
- response, err := util.HttpDo(request, time.Minute*60, "GitHub")
+ response, err := cli.httpClient.Do(request, time.Minute*60)
if err != nil {
return fmt.Errorf("could not download sample apps: %w", err)
}
@@ -145,12 +140,12 @@ func fetchSampleAppsZip(destination string) error {
})
}
-func openSampleAppsZip() (*os.File, error) {
- cacheDir, err := vespaCliCacheDir()
+func openSampleAppsZip(forceClone bool, cli *CLI) (*os.File, error) {
+ cacheDir, err := vespaCliCacheDir(cli.Environment)
if err != nil {
return nil, err
}
- path := filepath.Join(cacheDir, sampleAppsFilename)
+ path := filepath.Join(cacheDir, "sample-apps-master.zip")
cacheExists := true
stat, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist) {
@@ -159,7 +154,7 @@ func openSampleAppsZip() (*os.File, error) {
return nil, fmt.Errorf("could not stat existing cache file: %w", err)
}
if cacheExists {
- useCache, err := useCache(stat)
+ useCache, err := useCache(forceClone, stat)
if err != nil {
return nil, errHint(fmt.Errorf("could not determine cache status: %w", err), "Try ignoring the cache with the -f flag")
}
@@ -168,7 +163,7 @@ func openSampleAppsZip() (*os.File, error) {
return os.Open(path)
}
}
- if err := fetchSampleAppsZip(path); err != nil {
+ if err := fetchSampleAppsZip(path, cli); err != nil {
return nil, fmt.Errorf("could not fetch sample apps: %w", err)
}
return os.Open(path)
diff --git a/client/go/cmd/clone_list.go b/client/go/cmd/clone_list.go
index 83eb3fdc62f..1d37c0a8617 100644
--- a/client/go/cmd/clone_list.go
+++ b/client/go/cmd/clone_list.go
@@ -10,12 +10,12 @@ import (
"github.com/vespa-engine/vespa/client/go/util"
)
-func listSampleApps() ([]string, error) {
- return listSampleAppsAt("https://api.github.com/repos/vespa-engine/sample-apps/contents/")
+func listSampleApps(client util.HTTPClient) ([]string, error) {
+ return listSampleAppsAt("https://api.github.com/repos/vespa-engine/sample-apps/contents/", client)
}
-func listSampleAppsAt(url string) ([]string, error) {
- rfs, err := getRepositoryFiles(url)
+func listSampleAppsAt(url string, client util.HTTPClient) ([]string, error) {
+ rfs, err := getRepositoryFiles(url, client)
if err != nil {
return nil, err
}
@@ -25,7 +25,7 @@ func listSampleAppsAt(url string) ([]string, error) {
if isApp {
apps = append(apps, rf.Path)
} else if follow {
- apps2, err := listSampleAppsAt(rf.URL)
+ apps2, err := listSampleAppsAt(rf.URL, client)
if err != nil {
return nil, err
}
@@ -36,12 +36,12 @@ func listSampleAppsAt(url string) ([]string, error) {
return apps, nil
}
-func getRepositoryFiles(url string) ([]repositoryFile, error) {
+func getRepositoryFiles(url string, client util.HTTPClient) ([]repositoryFile, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
- response, err := util.HttpDo(req, time.Minute, "GitHub")
+ response, err := client.Do(req, time.Minute)
if err != nil {
return nil, err
}
diff --git a/client/go/cmd/clone_list_test.go b/client/go/cmd/clone_list_test.go
index 2e4fc4004bd..73aa70f12f2 100644
--- a/client/go/cmd/clone_list_test.go
+++ b/client/go/cmd/clone_list_test.go
@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/vespa-engine/vespa/client/go/mock"
- "github.com/vespa-engine/vespa/client/go/util"
)
func TestListSampleApps(t *testing.T) {
@@ -17,9 +16,8 @@ func TestListSampleApps(t *testing.T) {
c.NextResponse(200, readTestData(t, "sample-apps-news.json"))
c.NextResponse(200, readTestData(t, "sample-apps-operations.json"))
c.NextResponse(200, readTestData(t, "sample-apps-vespa-cloud.json"))
- util.ActiveHttpClient = c
- apps, err := listSampleApps()
+ apps, err := listSampleApps(c)
assert.Nil(t, err)
expected := []string{
"album-recommendation-monitoring",
diff --git a/client/go/cmd/clone_test.go b/client/go/cmd/clone_test.go
index 332758a127a..9587a1435d3 100644
--- a/client/go/cmd/clone_test.go
+++ b/client/go/cmd/clone_test.go
@@ -22,23 +22,26 @@ func TestClone(t *testing.T) {
func assertCreated(sampleAppName string, app string, t *testing.T) {
appCached := app + "-cache"
+ defer os.RemoveAll(app)
+ defer os.RemoveAll(appCached)
+
httpClient := &mock.HTTPClient{}
testdata, err := ioutil.ReadFile(filepath.Join("testdata", "sample-apps-master.zip"))
require.Nil(t, err)
httpClient.NextResponseBytes(200, testdata)
- cacheDir := t.TempDir()
- require.Nil(t, err)
- defer func() {
- os.RemoveAll(cacheDir)
- }()
- out, _ := execute(command{failTestOnError: true, cacheDir: cacheDir, args: []string{"clone", sampleAppName, app}}, t, httpClient)
- defer os.RemoveAll(app)
- assert.Equal(t, "Created "+app+"\n", out)
+
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = httpClient
+ err = cli.Run("clone", sampleAppName, app)
+ assert.Nil(t, err)
+
+ assert.Equal(t, "Created "+app+"\n", stdout.String())
assertFiles(t, app)
- outCached, _ := execute(command{failTestOnError: true, cacheDir: cacheDir, args: []string{"clone", sampleAppName, appCached}}, t, nil)
- defer os.RemoveAll(appCached)
- assert.Equal(t, "Using cached sample apps ...\nCreated "+appCached+"\n", outCached)
+ stdout.Reset()
+ err = cli.Run("clone", sampleAppName, appCached)
+ assert.Nil(t, err)
+ assert.Equal(t, "Using cached sample apps ...\nCreated "+appCached+"\n", stdout.String())
assertFiles(t, appCached)
}
diff --git a/client/go/cmd/command_tester_test.go b/client/go/cmd/command_tester_test.go
deleted file mode 100644
index efaca6a7258..00000000000
--- a/client/go/cmd/command_tester_test.go
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-// A helper for testing commands
-// Author: bratseth
-
-package cmd
-
-import (
- "bytes"
- "io"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/spf13/pflag"
- "github.com/spf13/viper"
- "github.com/stretchr/testify/require"
- "github.com/vespa-engine/vespa/client/go/mock"
- "github.com/vespa-engine/vespa/client/go/util"
-)
-
-type command struct {
- homeDir string
- cacheDir string
- stdin io.ReadWriter
- args []string
- moreArgs []string
- env map[string]string
- failTestOnError bool
-}
-
-func resetFlag(f *pflag.Flag) {
- switch v := f.Value.(type) {
- case pflag.SliceValue:
- _ = v.Replace([]string{})
- default:
- switch v.Type() {
- case "bool", "string", "int":
- _ = v.Set(f.DefValue)
- }
- }
-}
-
-func setEnv(env map[string]string) map[string]string {
- originalEnv := map[string]string{}
- for k, v := range env {
- value, ok := os.LookupEnv(k)
- if ok {
- originalEnv[k] = value
- }
- os.Setenv(k, v)
- }
- return originalEnv
-}
-
-func resetEnv(env map[string]string, original map[string]string) {
- for k := range env {
- if v, ok := original[k]; ok {
- os.Setenv(k, v)
- } else {
- os.Unsetenv(k)
- }
- }
-}
-
-func execute(cmd command, t *testing.T, client *mock.HTTPClient) (string, string) {
- if client != nil {
- util.ActiveHttpClient = client
- }
-
- // Set Vespa CLI directories. Use a separate one per test if none is specified
- if cmd.homeDir == "" {
- cmd.homeDir = filepath.Join(t.TempDir(), ".vespa")
- viper.Reset()
- }
- if cmd.cacheDir == "" {
- cmd.cacheDir = filepath.Join(t.TempDir(), ".cache", "vespa")
- }
-
- env := map[string]string{}
- for k, v := range cmd.env {
- env[k] = v
- }
- env["VESPA_CLI_HOME"] = cmd.homeDir
- env["VESPA_CLI_CACHE_DIR"] = cmd.cacheDir
- originalEnv := setEnv(env)
- defer resetEnv(env, originalEnv)
-
- // Reset viper at end of test to ensure vespa config set does not leak between tests
- t.Cleanup(viper.Reset)
-
- // Reset flags to their default value - persistent flags in Cobra persists over tests
- // TODO: Due to the bad design of viper, the only proper fix is to get rid of global state by moving each command to
- // their own sub-package
- rootCmd.Flags().VisitAll(resetFlag)
- queryCmd.Flags().VisitAll(resetFlag)
- documentCmd.Flags().VisitAll(resetFlag)
- logCmd.Flags().VisitAll(resetFlag)
- certCmd.Flags().VisitAll(resetFlag)
- certAddCmd.Flags().VisitAll(resetFlag)
-
- // Capture stdout and execute command
- var capturedOut bytes.Buffer
- var capturedErr bytes.Buffer
- stdout = &capturedOut
- stderr = &capturedErr
- if cmd.stdin != nil {
- stdin = cmd.stdin
- } else {
- stdin = os.Stdin
- }
-
- // Execute command and return output
- rootCmd.SetArgs(append(cmd.args, cmd.moreArgs...))
- err := Execute()
- if cmd.failTestOnError {
- require.Nil(t, err)
- }
- return capturedOut.String(), capturedErr.String()
-}
-
-func executeCommand(t *testing.T, client *mock.HTTPClient, args []string, moreArgs []string) string {
- out, _ := execute(command{args: args, moreArgs: moreArgs}, t, client)
- return out
-}
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index 59739d1c342..75bd9959280 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -18,7 +18,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
- "github.com/vespa-engine/vespa/client/go/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/auth0"
"github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/vespa"
)
@@ -28,87 +28,81 @@ const (
configType = "yaml"
)
-var (
- flagToConfigBindings map[string]*cobra.Command = make(map[string]*cobra.Command)
- envToConfigBindings map[string]string = make(map[string]string)
-)
-
-func init() {
- rootCmd.AddCommand(configCmd)
- configCmd.AddCommand(setConfigCmd)
- configCmd.AddCommand(getConfigCmd)
-}
-
-var configCmd = &cobra.Command{
- Use: "config",
- Short: "Configure persistent values for flags",
- Long: `Configure persistent values for flags.
+func newConfigCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "config",
+ Short: "Configure persistent values for global flags",
+ Long: `Configure persistent values for global flags.
-This command allows setting a persistent value for a given flag. On future
-invocations the flag can then be omitted as it is read from the config file
-instead.
+This command allows setting a persistent value for a given global flag. On
+future invocations the flag can then be omitted as it is read from the config
+file instead.
Configuration is written to $HOME/.vespa by default. This path can be
overridden by setting the VESPA_CLI_HOME environment variable.`,
- DisableAutoGenTag: true,
- SilenceUsage: false,
- Args: cobra.MinimumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- return fmt.Errorf("invalid command: %s", args[0])
- },
+ DisableAutoGenTag: true,
+ SilenceUsage: false,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
+ },
+ }
}
-var setConfigCmd = &cobra.Command{
- Use: "set option-name value",
- Short: "Set a configuration option.",
- Example: "$ vespa config set target cloud",
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.ExactArgs(2),
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- if err := cfg.Set(args[0], args[1]); err != nil {
- return err
- }
- return cfg.Write()
- },
+func newConfigSetCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "set option-name value",
+ Short: "Set a configuration option.",
+ Example: "$ vespa config set target cloud",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(2),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := cli.config.set(args[0], args[1]); err != nil {
+ return err
+ }
+ return cli.config.write()
+ },
+ }
}
-var getConfigCmd = &cobra.Command{
- Use: "get [option-name]",
- Short: "Show given configuration option, or all configuration options",
- Example: `$ vespa config get
+func newConfigGetCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ 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,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- if len(args) == 0 { // Print all values
- var flags []string
- for flag := range flagToConfigBindings {
- flags = append(flags, flag)
- }
- sort.Strings(flags)
- for _, flag := range flags {
- printOption(cfg, flag)
+ Args: cobra.MaximumNArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 { // Print all values
+ var flags []string
+ for flag := range cli.config.bindings.flag {
+ flags = append(flags, flag)
+ }
+ sort.Strings(flags)
+ for _, flag := range flags {
+ cli.config.printOption(flag)
+ }
+ } else {
+ cli.config.printOption(args[0])
}
- } else {
- printOption(cfg, args[0])
- }
- return nil
- },
+ return nil
+ },
+ }
}
type Config struct {
- Home string
- createDirs bool
+ homeDir string
+ environment map[string]string
+ bindings ConfigBindings
+ createDirs bool
+}
+
+type ConfigBindings struct {
+ flag map[string]*cobra.Command
+ environment map[string]string
}
type KeyPair struct {
@@ -117,23 +111,43 @@ type KeyPair struct {
PrivateKeyFile string
}
-func LoadConfig() (*Config, error) {
- home, err := vespaCliHome()
+func NewConfigBindings() ConfigBindings {
+ return ConfigBindings{
+ flag: make(map[string]*cobra.Command),
+ environment: make(map[string]string),
+ }
+}
+
+func (b *ConfigBindings) bindFlag(name string, command *cobra.Command) {
+ b.flag[name] = command
+}
+
+func (b *ConfigBindings) bindEnvironment(flagName string, variable string) {
+ b.environment[flagName] = variable
+}
+
+func loadConfig(environment map[string]string, bindings ConfigBindings) (*Config, error) {
+ home, err := vespaCliHome(environment)
if err != nil {
return nil, fmt.Errorf("could not detect config directory: %w", err)
}
- c := &Config{Home: home, createDirs: true}
+ c := &Config{
+ homeDir: home,
+ environment: environment,
+ bindings: bindings,
+ createDirs: true,
+ }
if err := c.load(); err != nil {
return nil, fmt.Errorf("could not load config: %w", err)
}
return c, nil
}
-func (c *Config) Write() error {
- if err := os.MkdirAll(c.Home, 0700); err != nil {
+func (c *Config) write() error {
+ if err := os.MkdirAll(c.homeDir, 0700); err != nil {
return err
}
- configFile := filepath.Join(c.Home, configName+"."+configType)
+ configFile := filepath.Join(c.homeDir, configName+"."+configType)
if !util.PathExists(configFile) {
if _, err := os.Create(configFile); err != nil {
return err
@@ -142,33 +156,69 @@ func (c *Config) Write() error {
return viper.WriteConfig()
}
-func (c *Config) CertificatePath(app vespa.ApplicationID) (string, error) {
- if override, ok := os.LookupEnv("VESPA_CLI_DATA_PLANE_CERT_FILE"); ok {
+func (c *Config) targetType() (string, error) {
+ targetType, ok := c.get(targetFlag)
+ if !ok {
+ return "", fmt.Errorf("target is unset")
+ }
+ return targetType, nil
+}
+
+func (c *Config) application() (vespa.ApplicationID, error) {
+ app, ok := c.get(applicationFlag)
+ if !ok {
+ return vespa.ApplicationID{}, errHint(fmt.Errorf("no application specified"), "Try the --"+applicationFlag+" flag")
+ }
+ application, err := vespa.ApplicationFromString(app)
+ if err != nil {
+ return vespa.ApplicationID{}, errHint(err, "application format is <tenant>.<app>.<instance>")
+ }
+ return application, nil
+}
+
+func (c *Config) deploymentIn(zoneName string, system vespa.System) (vespa.Deployment, error) {
+ zone := system.DefaultZone
+ var err error
+ if zoneName != "" {
+ zone, err = vespa.ZoneFromString(zoneName)
+ if err != nil {
+ return vespa.Deployment{}, err
+ }
+ }
+ app, err := c.application()
+ if err != nil {
+ return vespa.Deployment{}, err
+ }
+ return vespa.Deployment{System: system, Application: app, Zone: zone}, nil
+}
+
+func (c *Config) certificatePath(app vespa.ApplicationID) (string, error) {
+ if override, ok := c.environment["VESPA_CLI_DATA_PLANE_CERT_FILE"]; ok {
return override, nil
}
return c.applicationFilePath(app, "data-plane-public-cert.pem")
}
-func (c *Config) PrivateKeyPath(app vespa.ApplicationID) (string, error) {
- if override, ok := os.LookupEnv("VESPA_CLI_DATA_PLANE_KEY_FILE"); ok {
+func (c *Config) privateKeyPath(app vespa.ApplicationID) (string, error) {
+ if override, ok := c.environment["VESPA_CLI_DATA_PLANE_KEY_FILE"]; ok {
return override, nil
}
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")
+func (c *Config) x509KeyPair(app vespa.ApplicationID) (KeyPair, error) {
+ cert, certOk := c.environment["VESPA_CLI_DATA_PLANE_CERT"]
+ key, keyOk := c.environment["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)
+ privateKeyFile, err := c.privateKeyPath(app)
if err != nil {
return KeyPair{}, err
}
- certificateFile, err := c.CertificatePath(app)
+ certificateFile, err := c.certificatePath(app)
if err != nil {
return KeyPair{}, err
}
@@ -183,45 +233,45 @@ func (c *Config) X509KeyPair(app vespa.ApplicationID) (KeyPair, error) {
}, nil
}
-func (c *Config) APIKeyPath(tenantName string) string {
- if override, ok := c.Get(apiKeyFileFlag); ok {
+func (c *Config) apiKeyPath(tenantName string) string {
+ if override, ok := c.get(apiKeyFileFlag); ok {
return override
}
- return filepath.Join(c.Home, tenantName+".api-key.pem")
+ return filepath.Join(c.homeDir, tenantName+".api-key.pem")
+}
+
+func (c *Config) authConfigPath() string {
+ return filepath.Join(c.homeDir, "auth.json")
}
-func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) {
- if override, ok := c.Get(apiKeyFlag); ok {
+func (c *Config) readAPIKey(tenantName string) ([]byte, error) {
+ if override, ok := c.get(apiKeyFlag); ok {
return []byte(override), nil
}
- return ioutil.ReadFile(c.APIKeyPath(tenantName))
+ return ioutil.ReadFile(c.apiKeyPath(tenantName))
}
-// UseAPIKey checks if api key should be used be checking if api-key or api-key-file has been set.
-func (c *Config) UseAPIKey(system vespa.System, tenantName string) bool {
- if _, ok := c.Get(apiKeyFlag); ok {
+// useAPIKey returns true if an API key should be used when authenticating with system.
+func (c *Config) useAPIKey(cli *CLI, system vespa.System, tenantName string) bool {
+ if _, ok := c.get(apiKeyFlag); ok {
return true
}
- if _, ok := c.Get(apiKeyFileFlag); ok {
+ if _, ok := c.get(apiKeyFileFlag); ok {
return true
}
// If no Auth0 token is created, fall back to tenant api key, but warn that this functionality is deprecated
// TODO: Remove this when users have had time to migrate over to Auth0 device flow authentication
- if !isCI() {
- a, err := auth0.GetAuth0(c.AuthConfigPath(), system.Name, system.URL)
+ if !cli.isCI() {
+ a, err := auth0.GetAuth0(c.authConfigPath(), system.Name, system.URL)
if err != nil || !a.HasSystem() {
- printWarning("Use of API key is deprecated", "Authenticate with Auth0 instead: 'vespa auth login'")
- return util.PathExists(c.APIKeyPath(tenantName))
+ cli.printWarning("Use of API key is deprecated", "Authenticate with Auth0 instead: 'vespa auth login'")
+ return util.PathExists(c.apiKeyPath(tenantName))
}
}
return false
}
-func (c *Config) AuthConfigPath() string {
- return filepath.Join(c.Home, "auth.json")
-}
-
-func (c *Config) ReadSessionID(app vespa.ApplicationID) (int64, error) {
+func (c *Config) readSessionID(app vespa.ApplicationID) (int64, error) {
sessionPath, err := c.applicationFilePath(app, "session_id")
if err != nil {
return 0, err
@@ -233,7 +283,7 @@ func (c *Config) ReadSessionID(app vespa.ApplicationID) (int64, error) {
return strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64)
}
-func (c *Config) WriteSessionID(app vespa.ApplicationID, sessionID int64) error {
+func (c *Config) writeSessionID(app vespa.ApplicationID, sessionID int64) error {
sessionPath, err := c.applicationFilePath(app, "session_id")
if err != nil {
return err
@@ -242,7 +292,7 @@ func (c *Config) WriteSessionID(app vespa.ApplicationID, sessionID int64) error
}
func (c *Config) applicationFilePath(app vespa.ApplicationID, name string) (string, error) {
- appDir := filepath.Join(c.Home, app.String())
+ appDir := filepath.Join(c.homeDir, app.String())
if c.createDirs {
if err := os.MkdirAll(appDir, 0700); err != nil {
return "", err
@@ -254,14 +304,10 @@ func (c *Config) applicationFilePath(app vespa.ApplicationID, name string) (stri
func (c *Config) load() error {
viper.SetConfigName(configName)
viper.SetConfigType(configType)
- viper.AddConfigPath(c.Home)
- viper.AutomaticEnv()
- for option, command := range flagToConfigBindings {
+ viper.AddConfigPath(c.homeDir)
+ for option, command := range c.bindings.flag {
viper.BindPFlag(option, command.PersistentFlags().Lookup(option))
}
- for option, env := range envToConfigBindings {
- viper.BindEnv(option, env)
- }
err := viper.ReadInConfig()
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
return nil
@@ -269,7 +315,12 @@ func (c *Config) load() error {
return err
}
-func (c *Config) Get(option string) (string, bool) {
+func (c *Config) get(option string) (string, bool) {
+ if envVar, ok := c.bindings.environment[option]; ok {
+ if value, ok := c.environment[envVar]; ok {
+ return value, true
+ }
+ }
value := viper.GetString(option)
if value == "" {
return "", false
@@ -277,7 +328,7 @@ func (c *Config) Get(option string) (string, bool) {
return value, true
}
-func (c *Config) Set(option, value string) error {
+func (c *Config) set(option, value string) error {
switch option {
case targetFlag:
switch value {
@@ -296,7 +347,7 @@ func (c *Config) Set(option, value string) error {
viper.Set(option, value)
return nil
case waitFlag:
- if _, err := strconv.ParseUint(value, 10, 32); err != nil {
+ if n, err := strconv.Atoi(value); err != nil || n < 0 {
return fmt.Errorf("%s option must be an integer >= 0, got %q", option, value)
}
viper.Set(option, value)
@@ -320,8 +371,8 @@ func (c *Config) Set(option, value string) error {
return fmt.Errorf("invalid option or value: %q: %q", option, value)
}
-func printOption(cfg *Config, option string) {
- value, ok := cfg.Get(option)
+func (c *Config) printOption(option string) {
+ value, ok := c.get(option)
if !ok {
faintColor := color.New(color.FgWhite, color.Faint)
value = faintColor.Sprint("<unset>")
@@ -331,10 +382,32 @@ func printOption(cfg *Config, option string) {
log.Printf("%s = %s", option, value)
}
-func bindFlagToConfig(option string, command *cobra.Command) {
- flagToConfigBindings[option] = command
+func vespaCliHome(env map[string]string) (string, error) {
+ home := env["VESPA_CLI_HOME"]
+ if home == "" {
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ home = filepath.Join(userHome, ".vespa")
+ }
+ if err := os.MkdirAll(home, 0700); err != nil {
+ return "", err
+ }
+ return home, nil
}
-func bindEnvToConfig(option string, env string) {
- envToConfigBindings[option] = env
+func vespaCliCacheDir(env map[string]string) (string, error) {
+ cacheDir := env["VESPA_CLI_CACHE_DIR"]
+ if cacheDir == "" {
+ userCacheDir, err := os.UserCacheDir()
+ if err != nil {
+ return "", err
+ }
+ cacheDir = filepath.Join(userCacheDir, "vespa")
+ }
+ if err := os.MkdirAll(cacheDir, 0755); err != nil {
+ return "", err
+ }
+ return cacheDir, nil
}
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index 1ca51652340..1329b356606 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -13,75 +13,64 @@ import (
)
func TestConfig(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
- 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", "hosted")
- assertConfigCommand(t, "target = hosted\n", homeDir, "config", "get", "target")
- assertConfigCommand(t, "", homeDir, "config", "set", "target", "cloud")
- assertConfigCommand(t, "target = cloud\n", homeDir, "config", "get", "target")
- assertConfigCommand(t, "", homeDir, "config", "set", "target", "http://127.0.0.1:8080")
- assertConfigCommand(t, "", homeDir, "config", "set", "target", "https://127.0.0.1")
- assertConfigCommand(t, "target = https://127.0.0.1\n", homeDir, "config", "get", "target")
- assertEnvConfigCommand(t, "api-key-file = /tmp/private.key\n", homeDir, map[string]string{"VESPA_CLI_API_KEY_FILE": "/tmp/private.key"}, "config", "get", "api-key-file")
- assertConfigCommand(t, "", homeDir, "config", "set", "api-key-file", "/tmp/private.key")
- assertConfigCommand(t, "api-key-file = /tmp/private.key\n", homeDir, "config", "get", "api-key-file")
-
- 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")
-
- assertConfigCommand(t, "api-key-file = /tmp/private.key\napplication = 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, "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")
-
- assertConfigCommand(t, "", homeDir, "config", "set", "quiet", "true")
- assertConfigCommand(t, "", homeDir, "config", "set", "quiet", "false")
+ assertConfigCommandErr(t, "Error: invalid option or value: \"foo\": \"bar\"\n", "config", "set", "foo", "bar")
+ assertConfigCommand(t, "foo = <unset>\n", "config", "get", "foo")
+ assertConfigCommand(t, "target = local\n", "config", "get", "target")
+ assertConfigCommand(t, "", "config", "set", "target", "hosted")
+ assertConfigCommand(t, "target = hosted\n", "config", "get", "target")
+ assertConfigCommand(t, "", "config", "set", "target", "cloud")
+ assertConfigCommand(t, "target = cloud\n", "config", "get", "target")
+ assertConfigCommand(t, "", "config", "set", "target", "http://127.0.0.1:8080")
+ assertConfigCommand(t, "", "config", "set", "target", "https://127.0.0.1")
+ assertConfigCommand(t, "target = https://127.0.0.1\n", "config", "get", "target")
+ assertEnvConfigCommand(t, "api-key-file = /tmp/private.key\n", []string{"VESPA_CLI_API_KEY_FILE=/tmp/private.key"}, "config", "get", "api-key-file")
+ assertConfigCommand(t, "", "config", "set", "api-key-file", "/tmp/private.key")
+ assertConfigCommand(t, "api-key-file = /tmp/private.key\n", "config", "get", "api-key-file")
+
+ assertConfigCommandErr(t, "Error: invalid application: \"foo\"\n", "config", "set", "application", "foo")
+ assertConfigCommand(t, "application = <unset>\n", "config", "get", "application")
+ assertConfigCommand(t, "", "config", "set", "application", "t1.a1.i1")
+ assertConfigCommand(t, "application = t1.a1.i1\n", "config", "get", "application")
+
+ assertConfigCommand(t, "api-key-file = /tmp/private.key\napplication = t1.a1.i1\ncolor = auto\nquiet = false\ntarget = https://127.0.0.1\nwait = 0\n", "config", "get")
+
+ assertConfigCommand(t, "", "config", "set", "wait", "60")
+ assertConfigCommandErr(t, "Error: wait option must be an integer >= 0, got \"foo\"\n", "config", "set", "wait", "foo")
+ assertConfigCommand(t, "wait = 60\n", "config", "get", "wait")
+
+ assertConfigCommand(t, "", "config", "set", "quiet", "true")
+ assertConfigCommand(t, "", "config", "set", "quiet", "false")
}
-func assertConfigCommand(t *testing.T, expected, homeDir string, args ...string) {
- assertEnvConfigCommand(t, expected, homeDir, nil, args...)
+func assertConfigCommand(t *testing.T, expected string, args ...string) {
+ assertEnvConfigCommand(t, expected, nil, args...)
}
-func assertEnvConfigCommand(t *testing.T, expected, homeDir string, env map[string]string, args ...string) {
- out, _ := execute(command{homeDir: homeDir, env: env, args: args}, t, nil)
- assert.Equal(t, expected, out)
+func assertEnvConfigCommand(t *testing.T, expected string, env []string, args ...string) {
+ cli, stdout, _ := newTestCLI(t, env...)
+ err := cli.Run(args...)
+ assert.Nil(t, err)
+ assert.Equal(t, expected, stdout.String())
}
-func assertConfigCommandErr(t *testing.T, expected, homeDir string, args ...string) {
- _, outErr := execute(command{homeDir: homeDir, args: args}, t, nil)
- assert.Equal(t, expected, outErr)
-}
-
-func withEnv(key, value string, fn func()) {
- orig, ok := os.LookupEnv(key)
- os.Setenv(key, value)
- fn()
- if ok {
- os.Setenv(key, orig)
- } else {
- os.Unsetenv(key)
- }
+func assertConfigCommandErr(t *testing.T, expected string, args ...string) {
+ cli, _, stderr := newTestCLI(t)
+ err := cli.Run(args...)
+ assert.NotNil(t, err)
+ assert.Equal(t, expected, stderr.String())
}
func TestUseAPIKey(t *testing.T) {
- homeDir := t.TempDir()
- c := Config{Home: homeDir}
+ cli, _, _ := newTestCLI(t)
- assert.False(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
+ assert.False(t, cli.config.useAPIKey(cli, vespa.PublicSystem, "t1"))
- c.Set(apiKeyFileFlag, "/tmp/foo")
- assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
- c.Set(apiKeyFileFlag, "")
+ cli.config.set(apiKeyFileFlag, "/tmp/foo")
+ assert.True(t, cli.config.useAPIKey(cli, vespa.PublicSystem, "t1"))
+ cli.config.set(apiKeyFileFlag, "")
- withEnv("VESPA_CLI_API_KEY", "...", func() {
- require.Nil(t, c.load())
- assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
- })
+ cli, _, _ = newTestCLI(t, "VESPA_CLI_API_KEY=foo")
+ assert.True(t, cli.config.useAPIKey(cli, vespa.PublicSystem, "t1"))
// Test deprecated functionality
authContent := `
@@ -100,18 +89,10 @@ func TestUseAPIKey(t *testing.T) {
}
}
}`
- withEnv("VESPA_CLI_CLOUD_SYSTEM", "public", func() {
- ci, ok := os.LookupEnv("CI")
- if ok {
- os.Unsetenv("CI") // Test depends on unset variable
- }
- _, err := os.Create(filepath.Join(homeDir, "t2.api-key.pem"))
- require.Nil(t, err)
- assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t2"))
- require.Nil(t, ioutil.WriteFile(filepath.Join(homeDir, "auth.json"), []byte(authContent), 0600))
- assert.False(t, c.UseAPIKey(vespa.PublicSystem, "t2"))
- if ok {
- os.Setenv("CI", ci)
- }
- })
+ cli, _, _ = newTestCLI(t, "VESPA_CLI_CLOUD_SYSTEM=public")
+ _, err := os.Create(filepath.Join(cli.config.homeDir, "t2.api-key.pem"))
+ require.Nil(t, err)
+ assert.True(t, cli.config.useAPIKey(cli, vespa.PublicSystem, "t2"))
+ require.Nil(t, ioutil.WriteFile(filepath.Join(cli.config.homeDir, "auth.json"), []byte(authContent), 0600))
+ assert.False(t, cli.config.useAPIKey(cli, vespa.PublicSystem, "t2"))
}
diff --git a/client/go/cmd/curl.go b/client/go/cmd/curl.go
index 289a65465bd..044318583a4 100644
--- a/client/go/cmd/curl.go
+++ b/client/go/cmd/curl.go
@@ -8,81 +8,77 @@ import (
"strings"
"github.com/spf13/cobra"
- "github.com/vespa-engine/vespa/client/go/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/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: "Access Vespa directly using curl",
- Long: `Access Vespa directly using curl.
+func newCurlCmd(cli *CLI) *cobra.Command {
+ var (
+ dryRun bool
+ curlService string
+ )
+ cmd := &cobra.Command{
+ Use: "curl [curl-options] path",
+ 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.
For a more high-level interface to query and feeding, see the 'query' and 'document' commands.
`,
- Example: `$ vespa curl /ApplicationStatus
+ 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),
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- target, err := getTarget()
- if err != nil {
- return err
- }
- service, err := target.Service(curlService, 0, 0, "")
- if err != nil {
- return err
- }
- url := joinURL(service.BaseURL, args[len(args)-1])
- rawArgs := args[:len(args)-1]
- c, err := curl.RawArgs(url, rawArgs...)
- if err != nil {
- return err
- }
- switch curlService {
- case vespa.DeployService:
- if target.Type() == vespa.TargetCloud {
- if err := addCloudAuth0Authentication(target.Deployment().System, cfg, c); err != nil {
- return err
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ target, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
+ service, err := target.Service(curlService, 0, 0, "")
+ if err != nil {
+ return err
+ }
+ url := joinURL(service.BaseURL, args[len(args)-1])
+ rawArgs := args[:len(args)-1]
+ c, err := curl.RawArgs(url, rawArgs...)
+ if err != nil {
+ return err
+ }
+ switch curlService {
+ case vespa.DeployService:
+ if target.Type() == vespa.TargetCloud {
+ if err := addCloudAuth0Authentication(target.Deployment().System, cli.config, c); err != nil {
+ return err
+ }
}
+ case vespa.DocumentService, vespa.QueryService:
+ c.PrivateKey = service.TLSOptions.PrivateKeyFile
+ c.Certificate = service.TLSOptions.CertificateFile
+ default:
+ return fmt.Errorf("service not found: %s", curlService)
}
- case vespa.DocumentService, vespa.QueryService:
- c.PrivateKey = service.TLSOptions.PrivateKeyFile
- c.Certificate = service.TLSOptions.CertificateFile
- default:
- return fmt.Errorf("service not found: %s", curlService)
- }
- if curlDryRun {
- log.Print(c.String())
- } else {
- if err := c.Run(os.Stdout, os.Stderr); err != nil {
- return fmt.Errorf("failed to execute curl: %w", err)
+ if dryRun {
+ log.Print(c.String())
+ } else {
+ if err := c.Run(os.Stdout, os.Stderr); err != nil {
+ return fmt.Errorf("failed to execute curl: %w", err)
+ }
}
- }
- return nil
- },
+ return nil
+ },
+ }
+ cmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "Print the curl command that would be executed")
+ cmd.Flags().StringVarP(&curlService, "service", "s", "query", "Which service to query. Must be \"deploy\", \"document\" or \"query\"")
+ return cmd
}
func addCloudAuth0Authentication(system vespa.System, cfg *Config, c *curl.Command) error {
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
+ a, err := auth0.GetAuth0(cfg.authConfigPath(), system.Name, system.URL)
if err != nil {
return err
}
diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go
index 50b837e0d85..520cf41e308 100644
--- a/client/go/cmd/curl_test.go
+++ b/client/go/cmd/curl_test.go
@@ -3,38 +3,34 @@ package cmd
import (
"fmt"
- "os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
- "github.com/vespa-engine/vespa/client/go/mock"
)
func TestCurl(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
- httpClient := &mock.HTTPClient{}
- _, outErr := execute(command{args: []string{"config", "set", "application", "t1.a1.i1"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", outErr)
- _, outErr = execute(command{args: []string{"config", "set", "target", "cloud"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", outErr)
- _, outErr = execute(command{args: []string{"auth", "api-key"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", outErr)
- _, outErr = execute(command{args: []string{"auth", "cert", "--no-add"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", outErr)
+ cli, stdout, _ := newTestCLI(t)
+ cli.Environment["VESPA_CLI_ENDPOINTS"] = "{\"endpoints\":[{\"cluster\":\"container\",\"url\":\"http://127.0.0.1:8080\"}]}"
+ assert.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ assert.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ assert.Nil(t, cli.Run("auth", "api-key"))
+ assert.Nil(t, cli.Run("auth", "cert", "--no-add"))
- os.Setenv("VESPA_CLI_ENDPOINTS", "{\"endpoints\":[{\"cluster\":\"container\",\"url\":\"http://127.0.0.1:8080\"}]}")
- out, _ := execute(command{homeDir: homeDir, args: []string{"curl", "-n", "--", "-v", "--data-urlencode", "arg=with space", "/search"}}, t, httpClient)
+ stdout.Reset()
+ err := cli.Run("curl", "-n", "--", "-v", "--data-urlencode", "arg=with space", "/search")
+ assert.Nil(t, err)
expected := fmt.Sprintf("curl --key %s --cert %s -v --data-urlencode 'arg=with space' http://127.0.0.1:8080/search\n",
- 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)
+ filepath.Join(cli.config.homeDir, "t1.a1.i1", "data-plane-private-key.pem"),
+ filepath.Join(cli.config.homeDir, "t1.a1.i1", "data-plane-public-cert.pem"))
+ assert.Equal(t, expected, stdout.String())
- _, outErr = execute(command{args: []string{"config", "set", "target", "local"}, homeDir: homeDir}, t, nil)
- assert.Equal(t, "", outErr)
- out, outErr = execute(command{homeDir: homeDir, args: []string{"curl", "-a", "t1.a1.i1", "-s", "deploy", "-n", "/application/v4/tenant/foo"}}, t, httpClient)
- assert.Equal(t, "", outErr)
+ assert.Nil(t, cli.Run("config", "set", "target", "local"))
+
+ stdout.Reset()
+ err = cli.Run("curl", "-a", "t1.a1.i1", "-s", "deploy", "-n", "/application/v4/tenant/foo")
+ assert.Nil(t, err)
expected = "curl http://127.0.0.1:19071/application/v4/tenant/foo\n"
- assert.Equal(t, expected, out)
+ assert.Equal(t, expected, stdout.String())
}
diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go
index 396f42fae67..2260e5aaaa1 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -6,6 +6,7 @@ package cmd
import (
"fmt"
+ "io"
"log"
"strconv"
@@ -15,28 +16,15 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-const (
- zoneFlag = "zone"
- logLevelFlag = "log-level"
-)
-
-var (
- zoneArg string
- logLevelArg string
-)
-
-func init() {
- rootCmd.AddCommand(deployCmd)
- rootCmd.AddCommand(prepareCmd)
- rootCmd.AddCommand(activateCmd)
- deployCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "", "The zone to use for deployment. This defaults to a dev zone")
- deployCmd.PersistentFlags().StringVarP(&logLevelArg, logLevelFlag, "l", "error", `Log level for Vespa logs. Must be "error", "warning", "info" or "debug"`)
-}
-
-var deployCmd = &cobra.Command{
- Use: "deploy [application-directory]",
- Short: "Deploy (prepare and activate) an application package",
- Long: `Deploy (prepare and activate) an application package.
+func newDeployCmd(cli *CLI) *cobra.Command {
+ var (
+ zoneArg string
+ logLevelArg string
+ )
+ cmd := &cobra.Command{
+ Use: "deploy [application-directory]",
+ Short: "Deploy (prepare and activate) an application package",
+ Long: `Deploy (prepare and activate) an application package.
When this returns successfully the application package has been validated
and activated on config servers. The process of applying it on individual nodes
@@ -47,141 +35,140 @@ If application directory is not specified, it defaults to working directory.
When deploying to Vespa Cloud the system can be overridden by setting the
environment variable VESPA_CLI_CLOUD_SYSTEM. This is intended for internal use
only.`,
- Example: `$ vespa deploy .
+ Example: `$ vespa deploy .
$ vespa deploy -t cloud
$ vespa deploy -t cloud -z dev.aws-us-east-1c # -z can be omitted here as this zone is the default
$ vespa deploy -t cloud -z perf.aws-us-east-1c`,
- Args: cobra.MaximumNArgs(1),
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
- if err != nil {
- return err
- }
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- target, err := getTarget()
- if err != nil {
- return err
- }
- opts, err := getDeploymentOptions(cfg, pkg, target)
- if err != nil {
- return err
- }
-
- var result vespa.PrepareResult
- err = util.Spinner(stderr, "Uploading application package ...", func() error {
- result, err = vespa.Deploy(opts)
- return err
- })
- if err != nil {
- return err
- }
-
- log.Println()
- if opts.IsCloud() {
- printSuccess("Triggered deployment of ", color.CyanString(pkg.Path), " with run ID ", color.CyanString(strconv.FormatInt(result.ID, 10)))
- } else {
- printSuccess("Deployed ", color.CyanString(pkg.Path))
- printPrepareLog(result)
- }
- if opts.IsCloud() {
- log.Printf("\nUse %s for deployment status, or follow this deployment at", color.CyanString("vespa status"))
- log.Print(color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/dev/instance/%s/job/%s-%s/run/%d",
- opts.Target.Deployment().System.ConsoleURL,
- opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application, opts.Target.Deployment().Application.Instance,
- opts.Target.Deployment().Zone.Environment, opts.Target.Deployment().Zone.Region,
- result.ID)))
- }
- return waitForQueryService(result.ID)
- },
+ Args: cobra.MaximumNArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ if err != nil {
+ return err
+ }
+ target, err := cli.target(zoneArg, logLevelArg)
+ if err != nil {
+ return err
+ }
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
+
+ var result vespa.PrepareResult
+ err = util.Spinner(cli.Stderr, "Uploading application package ...", func() error {
+ result, err = vespa.Deploy(opts)
+ return err
+ })
+ if err != nil {
+ return err
+ }
+
+ log.Println()
+ if opts.IsCloud() {
+ cli.printSuccess("Triggered deployment of ", color.CyanString(pkg.Path), " with run ID ", color.CyanString(strconv.FormatInt(result.ID, 10)))
+ } else {
+ cli.printSuccess("Deployed ", color.CyanString(pkg.Path))
+ printPrepareLog(cli.Stderr, result)
+ }
+ if opts.IsCloud() {
+ log.Printf("\nUse %s for deployment status, or follow this deployment at", color.CyanString("vespa status"))
+ log.Print(color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/dev/instance/%s/job/%s-%s/run/%d",
+ opts.Target.Deployment().System.ConsoleURL,
+ opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application, opts.Target.Deployment().Application.Instance,
+ opts.Target.Deployment().Zone.Environment, opts.Target.Deployment().Zone.Region,
+ result.ID)))
+ }
+ return waitForQueryService(cli, result.ID)
+ },
+ }
+ cmd.PersistentFlags().StringVarP(&zoneArg, "zone", "z", "", "The zone to use for deployment. This defaults to a dev zone")
+ cmd.PersistentFlags().StringVarP(&logLevelArg, "log-level", "l", "error", `Log level for Vespa logs. Must be "error", "warning", "info" or "debug"`)
+ return cmd
}
-var prepareCmd = &cobra.Command{
- Use: "prepare application-directory",
- Short: "Prepare an application package for activation",
- Args: cobra.MaximumNArgs(1),
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
- if err != nil {
- return fmt.Errorf("could not find application package: %w", err)
- }
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- target, err := getTarget()
- if err != nil {
- return err
- }
- var result vespa.PrepareResult
- err = util.Spinner(stderr, "Uploading application package ...", func() error {
- result, err = vespa.Prepare(vespa.DeploymentOptions{
- ApplicationPackage: pkg,
- Target: target,
+func newPrepareCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "prepare application-directory",
+ Short: "Prepare an application package for activation",
+ Args: cobra.MaximumNArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ if err != nil {
+ return fmt.Errorf("could not find application package: %w", err)
+ }
+ target, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
+ var result vespa.PrepareResult
+ err = util.Spinner(cli.Stderr, "Uploading application package ...", func() error {
+ result, err = vespa.Prepare(opts)
+ return err
})
- return err
- })
- if err != nil {
- return err
- }
- if err := cfg.WriteSessionID(vespa.DefaultApplication, result.ID); err != nil {
- return fmt.Errorf("could not write session id: %w", err)
- }
- printSuccess("Prepared ", color.CyanString(pkg.Path), " with session ", result.ID)
- return nil
- },
+ if err != nil {
+ return err
+ }
+ if err := cli.config.writeSessionID(vespa.DefaultApplication, result.ID); err != nil {
+ return fmt.Errorf("could not write session id: %w", err)
+ }
+ cli.printSuccess("Prepared ", color.CyanString(pkg.Path), " with session ", result.ID)
+ return nil
+ },
+ }
}
-var activateCmd = &cobra.Command{
- Use: "activate",
- Short: "Activate (deploy) a previously prepared application package",
- Args: cobra.MaximumNArgs(1),
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
- if err != nil {
- return fmt.Errorf("could not find application package: %w", err)
- }
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- sessionID, err := cfg.ReadSessionID(vespa.DefaultApplication)
- if err != nil {
- return fmt.Errorf("could not read session id: %w", err)
- }
- target, err := getTarget()
- if err != nil {
- return err
- }
- err = vespa.Activate(sessionID, vespa.DeploymentOptions{
- ApplicationPackage: pkg,
- Target: target,
- })
- if err != nil {
- return err
- }
- printSuccess("Activated ", color.CyanString(pkg.Path), " with session ", sessionID)
- return waitForQueryService(sessionID)
- },
+func newActivateCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "activate",
+ Short: "Activate (deploy) a previously prepared application package",
+ Args: cobra.MaximumNArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ if err != nil {
+ return fmt.Errorf("could not find application package: %w", err)
+ }
+ sessionID, err := cli.config.readSessionID(vespa.DefaultApplication)
+ if err != nil {
+ return fmt.Errorf("could not read session id: %w", err)
+ }
+ target, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
+ err = vespa.Activate(sessionID, opts)
+ if err != nil {
+ return err
+ }
+ cli.printSuccess("Activated ", color.CyanString(pkg.Path), " with session ", sessionID)
+ return waitForQueryService(cli, sessionID)
+ },
+ }
}
-func waitForQueryService(sessionOrRunID int64) error {
- if waitSecsArg > 0 {
+func waitForQueryService(cli *CLI, sessionOrRunID int64) error {
+ if cli.flags.waitSecs > 0 {
log.Println()
- return waitForService(vespa.QueryService, sessionOrRunID)
+ _, err := cli.service(vespa.QueryService, sessionOrRunID, "")
+ return err
}
return nil
}
-func printPrepareLog(result vespa.PrepareResult) {
+func printPrepareLog(stderr io.Writer, result vespa.PrepareResult) {
for _, entry := range result.LogLines {
level := entry.Level
switch level {
diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go
index f5af3751eb8..e2d6da5399d 100644
--- a/client/go/cmd/deploy_test.go
+++ b/client/go/cmd/deploy_test.go
@@ -5,7 +5,6 @@
package cmd
import (
- "path/filepath"
"strconv"
"testing"
@@ -34,9 +33,12 @@ func TestDeployZipWithURLTargetArgument(t *testing.T) {
arguments := []string{"deploy", "testdata/applications/withTarget/target/application.zip", "-t", "http://target:19071"}
client := &mock.HTTPClient{}
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ assert.Nil(t, cli.Run(arguments...))
assert.Equal(t,
"\nSuccess: Deployed "+applicationPackage+"\n",
- executeCommand(t, client, arguments, []string{}))
+ stdout.String())
assertDeployRequestMade("http://target:19071", client, t)
}
@@ -61,11 +63,11 @@ func TestDeployApplicationDirectoryWithPomAndTarget(t *testing.T) {
}
func TestDeployApplicationDirectoryWithPomAndEmptyTarget(t *testing.T) {
- client := &mock.HTTPClient{}
- _, outErr := execute(command{args: []string{"deploy", "testdata/applications/withEmptyTarget"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ assert.NotNil(t, cli.Run("deploy", "testdata/applications/withEmptyTarget"))
assert.Equal(t,
"Error: pom.xml exists but no target/application.zip. Run mvn package first\n",
- outErr)
+ stderr.String())
}
func TestDeployApplicationPackageErrorWithUnexpectedNonJson(t *testing.T) {
@@ -105,19 +107,25 @@ func TestDeployError(t *testing.T) {
}
func assertDeploy(applicationPackage string, arguments []string, t *testing.T) {
+ cli, stdout, _ := newTestCLI(t)
client := &mock.HTTPClient{}
+ cli.httpClient = client
+ assert.Nil(t, cli.Run(arguments...))
assert.Equal(t,
"\nSuccess: Deployed "+applicationPackage+"\n",
- executeCommand(t, client, arguments, []string{}))
+ stdout.String())
assertDeployRequestMade("http://127.0.0.1:19071", client, t)
}
func assertPrepare(applicationPackage string, arguments []string, t *testing.T) {
client := &mock.HTTPClient{}
client.NextResponse(200, `{"session-id":"42"}`)
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ assert.Nil(t, cli.Run(arguments...))
assert.Equal(t,
"Success: Prepared "+applicationPackage+" with session 42\n",
- executeCommand(t, client, arguments, []string{}))
+ stdout.String())
assertPackageUpload(0, "http://127.0.0.1:19071/application/v2/tenant/default/session", client, t)
sessionURL := "http://127.0.0.1:19071/application/v2/tenant/default/session/42/prepared"
@@ -127,15 +135,15 @@ func assertPrepare(applicationPackage string, arguments []string, t *testing.T)
func assertActivate(applicationPackage string, arguments []string, t *testing.T) {
client := &mock.HTTPClient{}
- homeDir := t.TempDir()
- cfg := Config{Home: filepath.Join(homeDir, ".vespa"), createDirs: true}
- if err := cfg.WriteSessionID(vespa.DefaultApplication, 42); err != nil {
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ if err := cli.config.writeSessionID(vespa.DefaultApplication, 42); err != nil {
t.Fatal(err)
}
- out, _ := execute(command{args: arguments, homeDir: cfg.Home}, t, client)
+ assert.Nil(t, cli.Run(arguments...))
assert.Equal(t,
"Success: Activated "+applicationPackage+" with session 42\n",
- out)
+ stdout.String())
url := "http://127.0.0.1:19071/application/v2/tenant/default/session/42/active"
assert.Equal(t, url, client.LastRequest.URL.String())
assert.Equal(t, "PUT", client.LastRequest.Method)
@@ -163,17 +171,21 @@ func assertDeployRequestMade(target string, client *mock.HTTPClient, t *testing.
func assertApplicationPackageError(t *testing.T, cmd string, status int, expectedMessage string, returnBody string) {
client := &mock.HTTPClient{}
client.NextResponse(status, returnBody)
- _, outErr := execute(command{args: []string{cmd, "testdata/applications/withTarget/target/application.zip"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run(cmd, "testdata/applications/withTarget/target/application.zip"))
assert.Equal(t,
"Error: invalid application package (Status "+strconv.Itoa(status)+")\n"+expectedMessage+"\n",
- outErr)
+ stderr.String())
}
func assertDeployServerError(t *testing.T, status int, errorMessage string) {
client := &mock.HTTPClient{}
client.NextResponse(status, errorMessage)
- _, outErr := execute(command{args: []string{"deploy", "testdata/applications/withTarget/target/application.zip"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("deploy", "testdata/applications/withTarget/target/application.zip"))
assert.Equal(t,
"Error: error from deploy api at 127.0.0.1:19071 (Status "+strconv.Itoa(status)+"):\n"+errorMessage+"\n",
- outErr)
+ stderr.String())
}
diff --git a/client/go/cmd/document.go b/client/go/cmd/document.go
index 5e5108d117d..1c155bd6718 100644
--- a/client/go/cmd/document.go
+++ b/client/go/cmd/document.go
@@ -17,25 +17,20 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var (
- docPrintCurl bool
- docTimeoutSecs int
-)
-
-func init() {
- rootCmd.AddCommand(documentCmd)
- documentCmd.AddCommand(documentPutCmd)
- documentCmd.AddCommand(documentUpdateCmd)
- documentCmd.AddCommand(documentRemoveCmd)
- documentCmd.AddCommand(documentGetCmd)
- documentCmd.PersistentFlags().BoolVarP(&docPrintCurl, "verbose", "v", false, "Print the equivalent curl command for the document operation")
- documentCmd.PersistentFlags().IntVarP(&docTimeoutSecs, "timeout", "T", 60, "Timeout for the document request in seconds")
+func addDocumentFlags(cmd *cobra.Command, printCurl *bool, timeoutSecs *int) {
+ cmd.PersistentFlags().BoolVarP(printCurl, "verbose", "v", false, "Print the equivalent curl command for the document operation")
+ cmd.PersistentFlags().IntVarP(timeoutSecs, "timeout", "T", 60, "Timeout for the document request in seconds")
}
-var documentCmd = &cobra.Command{
- Use: "document json-file",
- Short: "Issue a document operation to Vespa",
- Long: `Issue a document operation to Vespa.
+func newDocumentCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ timeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "document json-file",
+ Short: "Issue a document operation to Vespa",
+ Long: `Issue a document operation to Vespa.
The operation must be on the format documented in
https://docs.vespa.ai/en/reference/document-json-format.html#document-operations
@@ -45,125 +40,159 @@ subsequent get or query operation.
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),
- 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)
- },
+ Example: `$ vespa document src/test/resources/A-Head-Full-of-Dreams.json`,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService(cli)
+ if err != nil {
+ return err
+ }
+ return printResult(cli, vespa.Send(args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ },
+ }
+ addDocumentFlags(cmd, &printCurl, &timeoutSecs)
+ return cmd
}
-var documentPutCmd = &cobra.Command{
- Use: "put [id] json-file",
- Short: "Writes a document to Vespa",
- Long: `Writes the document in the given file to Vespa.
+func newDocumentPutCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ timeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "put [id] json-file",
+ Short: "Writes a document to Vespa",
+ Long: `Writes the document in the given file to Vespa.
If the document already exists, all its values will be replaced by this document.
If the document id is specified both as an argument and in the file the argument takes precedence.`,
- Args: cobra.RangeArgs(1, 2),
- Example: `$ vespa document put src/test/resources/A-Head-Full-of-Dreams.json
+ Args: cobra.RangeArgs(1, 2),
+ 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,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- service, err := documentService()
- if err != nil {
- return err
- }
- if len(args) == 1 {
- return printResult(vespa.Put("", args[0], service, operationOptions()), false)
- } else {
- return printResult(vespa.Put(args[0], args[1], service, operationOptions()), false)
- }
- },
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService(cli)
+ if err != nil {
+ return err
+ }
+ if len(args) == 1 {
+ return printResult(cli, vespa.Put("", args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ } else {
+ return printResult(cli, vespa.Put(args[0], args[1], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ }
+ },
+ }
+ addDocumentFlags(cmd, &printCurl, &timeoutSecs)
+ return cmd
}
-var documentUpdateCmd = &cobra.Command{
- Use: "update [id] json-file",
- Short: "Modifies some fields of an existing document",
- Long: `Updates the values of the fields given in a json file as specified in the file.
+func newDocumentUpdateCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ timeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "update [id] json-file",
+ Short: "Modifies some fields of an existing document",
+ Long: `Updates the values of the fields given in a json file as specified in the file.
If the document id is specified both as an argument and in the file the argument takes precedence.`,
- Args: cobra.RangeArgs(1, 2),
- Example: `$ vespa document update src/test/resources/A-Head-Full-of-Dreams-Update.json
+ Args: cobra.RangeArgs(1, 2),
+ 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,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- service, err := documentService()
- if err != nil {
- return err
- }
- if len(args) == 1 {
- return printResult(vespa.Update("", args[0], service, operationOptions()), false)
- } else {
- return printResult(vespa.Update(args[0], args[1], service, operationOptions()), false)
- }
- },
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService(cli)
+ if err != nil {
+ return err
+ }
+ if len(args) == 1 {
+ return printResult(cli, vespa.Update("", args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ } else {
+ return printResult(cli, vespa.Update(args[0], args[1], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ }
+ },
+ }
+ addDocumentFlags(cmd, &printCurl, &timeoutSecs)
+ return cmd
}
-var documentRemoveCmd = &cobra.Command{
- Use: "remove id | json-file",
- Short: "Removes a document from Vespa",
- Long: `Removes the document specified either as a document id or given in the json file.
+func newDocumentRemoveCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ timeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "remove id | json-file",
+ Short: "Removes a document from Vespa",
+ Long: `Removes the document specified either as a document id or given in the json file.
If the document id is specified both as an argument and in the file the argument takes precedence.`,
- Args: cobra.ExactArgs(1),
- Example: `$ vespa document remove src/test/resources/A-Head-Full-of-Dreams-Remove.json
+ Args: cobra.ExactArgs(1),
+ 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,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- service, err := documentService()
- if err != nil {
- return err
- }
- if strings.HasPrefix(args[0], "id:") {
- return printResult(vespa.RemoveId(args[0], service, operationOptions()), false)
- } else {
- return printResult(vespa.RemoveOperation(args[0], service, operationOptions()), false)
- }
- },
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService(cli)
+ if err != nil {
+ return err
+ }
+ if strings.HasPrefix(args[0], "id:") {
+ return printResult(cli, vespa.RemoveId(args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ } else {
+ return printResult(cli, vespa.RemoveOperation(args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), false)
+ }
+ },
+ }
+ addDocumentFlags(cmd, &printCurl, &timeoutSecs)
+ return cmd
}
-var documentGetCmd = &cobra.Command{
- Use: "get id",
- Short: "Gets a document",
- Args: cobra.ExactArgs(1),
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Example: `$ vespa document get id:mynamespace:music::a-head-full-of-dreams`,
- 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 newDocumentGetCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ timeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "get id",
+ Short: "Gets a document",
+ Args: cobra.ExactArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Example: `$ vespa document get id:mynamespace:music::a-head-full-of-dreams`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ service, err := documentService(cli)
+ if err != nil {
+ return err
+ }
+ return printResult(cli, vespa.Get(args[0], service, operationOptions(cli.Stderr, printCurl, timeoutSecs)), true)
+ },
+ }
+ addDocumentFlags(cmd, &printCurl, &timeoutSecs)
+ return cmd
}
-func documentService() (*vespa.Service, error) { return getService(vespa.DocumentService, 0, "") }
-
-func operationOptions() vespa.OperationOptions {
- return vespa.OperationOptions{
- CurlOutput: curlOutput(),
- Timeout: time.Second * time.Duration(docTimeoutSecs),
- }
+func documentService(cli *CLI) (*vespa.Service, error) {
+ return cli.service(vespa.DocumentService, 0, "")
}
-func curlOutput() io.Writer {
- if docPrintCurl {
- return stderr
+func operationOptions(stderr io.Writer, printCurl bool, timeoutSecs int) vespa.OperationOptions {
+ curlOutput := ioutil.Discard
+ if printCurl {
+ curlOutput = stderr
+ }
+ return vespa.OperationOptions{
+ CurlOutput: curlOutput,
+ Timeout: time.Second * time.Duration(timeoutSecs),
}
- return ioutil.Discard
}
-func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) error {
- out := stdout
+func printResult(cli *CLI, result util.OperationResult, payloadOnlyOnSuccess bool) error {
+ out := cli.Stdout
if !result.Success {
- out = stderr
+ out = cli.Stderr
}
if !result.Success {
diff --git a/client/go/cmd/document_test.go b/client/go/cmd/document_test.go
index 1d650f77d08..77b7d68d666 100644
--- a/client/go/cmd/document_test.go
+++ b/client/go/cmd/document_test.go
@@ -66,21 +66,19 @@ func TestDocumentRemoveWithoutIdArg(t *testing.T) {
}
func TestDocumentSendMissingId(t *testing.T) {
- arguments := []string{"document", "put", "testdata/A-Head-Full-of-Dreams-Without-Operation.json"}
- client := &mock.HTTPClient{}
- _, outErr := execute(command{args: arguments}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ assert.NotNil(t, cli.Run("document", "put", "testdata/A-Head-Full-of-Dreams-Without-Operation.json"))
assert.Equal(t,
"Error: No document id given neither as argument or as a 'put' key in the json file\n",
- outErr)
+ stderr.String())
}
func TestDocumentSendWithDisagreeingOperations(t *testing.T) {
- arguments := []string{"document", "update", "testdata/A-Head-Full-of-Dreams-Put.json"}
- client := &mock.HTTPClient{}
- _, outErr := execute(command{args: arguments}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ assert.NotNil(t, cli.Run("document", "update", "testdata/A-Head-Full-of-Dreams-Put.json"))
assert.Equal(t,
"Error: Wanted document operation is update but the JSON file specifies put\n",
- outErr)
+ stderr.String())
}
func TestDocumentPutDocumentError(t *testing.T) {
@@ -98,14 +96,16 @@ func TestDocumentGet(t *testing.T) {
func assertDocumentSend(arguments []string, expectedOperation string, expectedMethod string, expectedDocumentId string, expectedPayloadFile string, t *testing.T) {
client := &mock.HTTPClient{}
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = 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)
+ assert.Nil(t, cli.Run(arguments...))
verbose := false
for _, a := range arguments {
if a == "-v" {
@@ -114,9 +114,9 @@ func assertDocumentSend(arguments []string, expectedOperation string, expectedMe
}
if verbose {
expectedCurl := "curl -X " + expectedMethod + " -H 'Content-Type: application/json' --data-binary @" + expectedPayloadFile + " " + expectedURL + "\n"
- assert.Equal(t, expectedCurl, errOut)
+ assert.Equal(t, expectedCurl, stderr.String())
}
- assert.Equal(t, "Success: "+expectedOperation+" "+expectedDocumentId+"\n", out)
+ assert.Equal(t, "Success: "+expectedOperation+" "+expectedDocumentId+"\n", stdout.String())
assert.Equal(t, expectedURL, client.LastRequest.URL.String())
assert.Equal(t, "application/json", client.LastRequest.Header.Get("Content-Type"))
assert.Equal(t, expectedMethod, client.LastRequest.Method)
@@ -132,6 +132,9 @@ func assertDocumentGet(arguments []string, documentId string, t *testing.T) {
t.Fatal(err)
}
client.NextResponse(200, "{\"fields\":{\"foo\":\"bar\"}}")
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ assert.Nil(t, cli.Run(arguments...))
assert.Equal(t,
`{
"fields": {
@@ -139,7 +142,7 @@ func assertDocumentGet(arguments []string, documentId string, t *testing.T) {
}
}
`,
- executeCommand(t, client, arguments, []string{}))
+ stdout.String())
expectedPath, _ := vespa.IdToURLPath(documentId)
assert.Equal(t, documentURL+"/document/v1/"+expectedPath, client.LastRequest.URL.String())
assert.Equal(t, "GET", client.LastRequest.Method)
@@ -148,29 +151,29 @@ func assertDocumentGet(arguments []string, documentId string, t *testing.T) {
func assertDocumentError(t *testing.T, status int, errorMessage string) {
client := &mock.HTTPClient{}
client.NextResponse(status, errorMessage)
- _, outErr := execute(command{args: []string{"document", "put",
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("document", "put",
"id:mynamespace:music::a-head-full-of-dreams",
- "testdata/A-Head-Full-of-Dreams-Put.json"}}, t, client)
+ "testdata/A-Head-Full-of-Dreams-Put.json"))
assert.Equal(t,
"Error: Invalid document operation: Status "+strconv.Itoa(status)+"\n\n"+errorMessage+"\n",
- outErr)
+ stderr.String())
}
func assertDocumentServerError(t *testing.T, status int, errorMessage string) {
client := &mock.HTTPClient{}
client.NextResponse(status, errorMessage)
- _, outErr := execute(command{args: []string{"document", "put",
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("document", "put",
"id:mynamespace:music::a-head-full-of-dreams",
- "testdata/A-Head-Full-of-Dreams-Put.json"}}, t, client)
+ "testdata/A-Head-Full-of-Dreams-Put.json"))
assert.Equal(t,
"Error: Container (document API) at 127.0.0.1:8080: Status "+strconv.Itoa(status)+"\n\n"+errorMessage+"\n",
- outErr)
+ stderr.String())
}
func documentServiceURL(client *mock.HTTPClient) (string, error) {
- service, err := getService("document", 0, "")
- if err != nil {
- return "", err
- }
- return service.BaseURL, nil
+ return "http://127.0.0.1:8080", nil
}
diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go
deleted file mode 100644
index d45fda58a2f..00000000000
--- a/client/go/cmd/helpers.go
+++ /dev/null
@@ -1,368 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-// Helpers used by multiple sub-commands.
-// Author: mpolden
-
-package cmd
-
-import (
- "crypto/tls"
- "crypto/x509"
- "encoding/json"
- "fmt"
- "log"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "time"
-
- "github.com/fatih/color"
- "github.com/vespa-engine/vespa/client/go/build"
- "github.com/vespa-engine/vespa/client/go/version"
- "github.com/vespa-engine/vespa/client/go/vespa"
-)
-
-func printErrHint(err error, hints ...string) {
- fmt.Fprintln(stderr, color.RedString("Error:"), err)
- for _, hint := range hints {
- fmt.Fprintln(stderr, color.CyanString("Hint:"), hint)
- }
-}
-
-func printSuccess(msg ...interface{}) {
- log.Print(color.GreenString("Success: "), fmt.Sprint(msg...))
-}
-
-func printWarning(msg string, hints ...string) {
- fmt.Fprintln(stderr, color.YellowString("Warning:"), msg)
- for _, hint := range hints {
- fmt.Fprintln(stderr, color.CyanString("Hint:"), hint)
- }
-}
-
-func athenzPath(filename string) (string, error) {
- userHome, err := os.UserHomeDir()
- if err != nil {
- return "", err
- }
- return filepath.Join(userHome, ".athenz", filename), nil
-}
-
-func athenzKeyPair() (KeyPair, error) {
- certFile, err := athenzPath("cert")
- if err != nil {
- return KeyPair{}, err
- }
- keyFile, err := athenzPath("key")
- if err != nil {
- return KeyPair{}, err
- }
- kp, err := tls.LoadX509KeyPair(certFile, keyFile)
- if err != nil {
- return KeyPair{}, err
- }
- cert, err := x509.ParseCertificate(kp.Certificate[0])
- if err != nil {
- return KeyPair{}, err
- }
- now := time.Now()
- expiredAt := cert.NotAfter
- if expiredAt.Before(now) {
- delta := now.Sub(expiredAt).Truncate(time.Second)
- return KeyPair{}, errHint(fmt.Errorf("certificate %s expired at %s (%s ago)", certFile, cert.NotAfter, delta), "Try renewing certificate with 'athenz-user-cert'")
- }
- return KeyPair{KeyPair: kp, CertificateFile: certFile, PrivateKeyFile: keyFile}, nil
-}
-
-func vespaCliHome() (string, error) {
- home := os.Getenv("VESPA_CLI_HOME")
- if home == "" {
- userHome, err := os.UserHomeDir()
- if err != nil {
- return "", err
- }
- home = filepath.Join(userHome, ".vespa")
- }
- if err := os.MkdirAll(home, 0700); err != nil {
- return "", err
- }
- return home, nil
-}
-
-func vespaCliCacheDir() (string, error) {
- cacheDir := os.Getenv("VESPA_CLI_CACHE_DIR")
- if cacheDir == "" {
- userCacheDir, err := os.UserCacheDir()
- if err != nil {
- return "", err
- }
- cacheDir = filepath.Join(userCacheDir, "vespa")
- }
- if err := os.MkdirAll(cacheDir, 0755); err != nil {
- return "", err
- }
- return cacheDir, nil
-}
-
-func deploymentFromArgs(system vespa.System) (vespa.Deployment, error) {
- zone := system.DefaultZone
- var err error
- if zoneArg != "" {
- zone, err = vespa.ZoneFromString(zoneArg)
- if err != nil {
- return vespa.Deployment{}, err
- }
- }
- app, err := getApplication()
- if err != nil {
- return vespa.Deployment{}, err
- }
- return vespa.Deployment{System: system, Application: app, Zone: zone}, nil
-}
-
-func applicationSource(args []string) string {
- if len(args) > 0 {
- return args[0]
- }
- return "."
-}
-
-func getApplication() (vespa.ApplicationID, error) {
- cfg, err := LoadConfig()
- if err != nil {
- return vespa.ApplicationID{}, err
- }
- app, ok := cfg.Get(applicationFlag)
- if !ok {
- return vespa.ApplicationID{}, errHint(fmt.Errorf("no application specified: %w", err), "Try the --"+applicationFlag+" flag")
- }
- application, err := vespa.ApplicationFromString(app)
- if err != nil {
- return vespa.ApplicationID{}, errHint(err, "application format is <tenant>.<app>.<instance>")
- }
- return application, nil
-}
-
-func getTargetType() (string, error) {
- cfg, err := LoadConfig()
- if err != nil {
- return "", err
- }
- target, ok := cfg.Get(targetFlag)
- if !ok {
- return "", fmt.Errorf("target is unset")
- }
- return target, nil
-}
-
-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 %s %s for %s service to become available ...", color.CyanString(strconv.Itoa(waitSecsArg)), color.CyanString("seconds"), color.CyanString(service))
- }
- s, err := t.Service(service, timeout, sessionOrRunID, cluster)
- if err != nil {
- return nil, fmt.Errorf("service '%s' is unavailable: %w", service, err)
- }
- return s, nil
-}
-
-func getEndpointsOverride() string { return os.Getenv("VESPA_CLI_ENDPOINTS") }
-
-func getSystem(targetType string) (vespa.System, error) {
- name := os.Getenv("VESPA_CLI_CLOUD_SYSTEM")
- if name != "" {
- return vespa.GetSystem(name)
- }
- switch targetType {
- case vespa.TargetHosted:
- return vespa.MainSystem, nil
- case vespa.TargetCloud:
- return vespa.PublicSystem, nil
- }
- return vespa.System{}, fmt.Errorf("no default system found for %s target", targetType)
-}
-
-func getTarget() (vespa.Target, error) {
- clientVersion, err := version.Parse(build.Version)
- if err != nil {
- return nil, err
- }
- target, err := createTarget()
- if err != nil {
- return nil, err
- }
- if !isCloudCI() { // Vespa Cloud always runs an up-to-date version
- if err := target.CheckVersion(clientVersion); err != nil {
- printErrHint(err, "This is not a fatal error, but this version may not work as expected", "Try 'vespa version' to check for a new version")
- }
- }
- return target, nil
-}
-
-func createTarget() (vespa.Target, error) {
- targetType, err := getTargetType()
- if err != nil {
- return nil, err
- }
- if strings.HasPrefix(targetType, "http") {
- return vespa.CustomTarget(targetType), nil
- }
- switch targetType {
- case vespa.TargetLocal:
- return vespa.LocalTarget(), nil
- case vespa.TargetCloud, vespa.TargetHosted:
- return createCloudTarget(targetType)
- }
- return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud', 'hosted' or an URL")
-}
-
-func createCloudTarget(targetType string) (vespa.Target, error) {
- cfg, err := LoadConfig()
- if err != nil {
- return nil, err
- }
- system, err := getSystem(targetType)
- if err != nil {
- return nil, err
- }
- deployment, err := deploymentFromArgs(system)
- if err != nil {
- return nil, err
- }
- endpoints, err := getEndpointsFromEnv()
- if err != nil {
- return nil, err
- }
- var (
- apiKey []byte
- authConfigPath string
- apiTLSOptions vespa.TLSOptions
- deploymentTLSOptions vespa.TLSOptions
- )
- if targetType == vespa.TargetCloud {
- if cfg.UseAPIKey(system, deployment.Application.Tenant) {
- apiKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
- if err != nil {
- return nil, err
- }
- }
- authConfigPath = cfg.AuthConfigPath()
- kp, err := cfg.X509KeyPair(deployment.Application)
- if err != nil {
- return nil, errHint(err, "Deployment to cloud requires a certificate. Try 'vespa auth cert'")
- }
- deploymentTLSOptions = vespa.TLSOptions{
- KeyPair: kp.KeyPair,
- CertificateFile: kp.CertificateFile,
- PrivateKeyFile: kp.PrivateKeyFile,
- }
- } else if targetType == vespa.TargetHosted {
- kp, err := athenzKeyPair()
- if err != nil {
- return nil, err
- }
- apiTLSOptions = vespa.TLSOptions{
- KeyPair: kp.KeyPair,
- CertificateFile: kp.CertificateFile,
- PrivateKeyFile: kp.PrivateKeyFile,
- }
- deploymentTLSOptions = apiTLSOptions
- } else {
- return nil, fmt.Errorf("invalid cloud target: %s", targetType)
- }
- apiOptions := vespa.APIOptions{
- System: system,
- TLSOptions: apiTLSOptions,
- APIKey: apiKey,
- AuthConfigPath: authConfigPath,
- }
- deploymentOptions := vespa.CloudDeploymentOptions{
- Deployment: deployment,
- TLSOptions: deploymentTLSOptions,
- ClusterURLs: endpoints,
- }
- logOptions := vespa.LogOptions{
- Writer: stdout,
- Level: vespa.LogLevel(logLevelArg),
- }
- return vespa.CloudTarget(apiOptions, deploymentOptions, logOptions)
-}
-
-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 %s %s for service to become ready ...", color.CyanString(strconv.Itoa(waitSecsArg)), color.CyanString("seconds"))
- }
- status, err := s.Wait(timeout)
- if status/100 == 2 {
- log.Print(s.Description(), " at ", color.CyanString(s.BaseURL), " is ", color.GreenString("ready"))
- } else {
- if err == nil {
- err = fmt.Errorf("status %d", status)
- }
- return fmt.Errorf("%s at %s is %s: %w", s.Description(), color.CyanString(s.BaseURL), color.RedString("not ready"), err)
- }
- return nil
-}
-
-func getDeploymentOptions(cfg *Config, pkg vespa.ApplicationPackage, target vespa.Target) (vespa.DeploymentOptions, error) {
- opts := vespa.DeploymentOptions{ApplicationPackage: pkg, Target: target}
- if opts.IsCloud() {
- if target.Type() == vespa.TargetCloud && !opts.ApplicationPackage.HasCertificate() {
- hint := "Try 'vespa auth cert'"
- return vespa.DeploymentOptions{}, errHint(fmt.Errorf("missing certificate in application package"), "Applications in Vespa Cloud require a certificate", hint)
- }
- }
- opts.Timeout = time.Duration(waitSecsArg) * time.Second
- return opts, nil
-}
-
-func getEndpointsFromEnv() (map[string]string, error) {
- endpointsString := getEndpointsOverride()
- if endpointsString == "" {
- return nil, nil
- }
-
- var endpoints endpoints
- urlsByCluster := make(map[string]string)
- if err := json.Unmarshal([]byte(endpointsString), &endpoints); err != nil {
- return nil, fmt.Errorf("endpoints must be valid json: %w", err)
- }
- if len(endpoints.Endpoints) == 0 {
- return nil, fmt.Errorf("endpoints must be non-empty")
- }
- for _, endpoint := range endpoints.Endpoints {
- urlsByCluster[endpoint.Cluster] = endpoint.URL
- }
- return urlsByCluster, nil
-}
-
-// isCI returns true if running inside a continuous integration environment.
-func isCI() bool {
- _, ok := os.LookupEnv("CI")
- return ok
-}
-
-// isCloudCI returns true if running inside a Vespa Cloud deployment job.
-func isCloudCI() bool {
- _, ok := os.LookupEnv("VESPA_CLI_CLOUD_CI")
- return ok
-}
-
-type endpoints struct {
- Endpoints []endpoint `json:"endpoints"`
-}
-
-type endpoint struct {
- Cluster string `json:"cluster"`
- URL string `json:"url"`
-}
diff --git a/client/go/cmd/log.go b/client/go/cmd/log.go
index d61eaecf35b..829f2c4c1f1 100644
--- a/client/go/cmd/log.go
+++ b/client/go/cmd/log.go
@@ -9,72 +9,70 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var (
- fromArg string
- toArg string
- levelArg string
- followArg bool
- dequoteArg bool
-)
-
-func init() {
- rootCmd.AddCommand(logCmd)
- logCmd.Flags().StringVarP(&fromArg, "from", "F", "", "Include logs since this timestamp (RFC3339 format)")
- logCmd.Flags().StringVarP(&toArg, "to", "T", "", "Include logs until this timestamp (RFC3339 format)")
- logCmd.Flags().StringVarP(&levelArg, "level", "l", "debug", `The maximum log level to show. Must be "error", "warning", "info" or "debug"`)
- logCmd.Flags().BoolVarP(&followArg, "follow", "f", false, "Follow logs")
- logCmd.Flags().BoolVarP(&dequoteArg, "nldequote", "n", true, "Dequote LF and TAB characters in log messages")
-}
-
-var logCmd = &cobra.Command{
- Use: "log [relative-period]",
- Short: "Show the Vespa log",
- Long: `Show the Vespa log.
+func newLogCmd(cli *CLI) *cobra.Command {
+ var (
+ fromArg string
+ toArg string
+ levelArg string
+ followArg bool
+ dequoteArg bool
+ )
+ cmd := &cobra.Command{
+ Use: "log [relative-period]",
+ Short: "Show the Vespa log",
+ 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
+ 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),
- 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,
- Writer: stdout,
- Dequote: dequoteArg,
- }
- if options.Follow {
- if fromArg != "" || toArg != "" || len(args) > 0 {
- 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)
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ target, err := cli.target("", levelArg)
if err != nil {
- return fmt.Errorf("invalid period: %w", err)
+ return err
}
- options.From = from
- options.To = to
- }
- if err := target.PrintLog(options); err != nil {
- return fmt.Errorf("could not retrieve logs: %w", err)
- }
- return nil
- },
+ options := vespa.LogOptions{
+ Level: vespa.LogLevel(levelArg),
+ Follow: followArg,
+ Writer: cli.Stdout,
+ Dequote: dequoteArg,
+ }
+ if options.Follow {
+ if fromArg != "" || toArg != "" || len(args) > 0 {
+ 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(fromArg, toArg, args)
+ if err != nil {
+ return fmt.Errorf("invalid period: %w", err)
+ }
+ options.From = from
+ options.To = to
+ }
+ if err := target.PrintLog(options); err != nil {
+ return fmt.Errorf("could not retrieve logs: %w", err)
+ }
+ return nil
+ },
+ }
+ cmd.Flags().StringVarP(&fromArg, "from", "F", "", "Include logs since this timestamp (RFC3339 format)")
+ cmd.Flags().StringVarP(&toArg, "to", "T", "", "Include logs until this timestamp (RFC3339 format)")
+ cmd.Flags().StringVarP(&levelArg, "level", "l", "debug", `The maximum log level to show. Must be "error", "warning", "info" or "debug"`)
+ cmd.Flags().BoolVarP(&followArg, "follow", "f", false, "Follow logs")
+ cmd.Flags().BoolVarP(&dequoteArg, "nldequote", "n", true, "Dequote LF and TAB characters in log messages")
+ return cmd
}
-func parsePeriod(args []string) (time.Time, time.Time, error) {
- relativePeriod := fromArg == "" || toArg == ""
+func parsePeriod(from, to string, args []string) (time.Time, time.Time, error) {
+ relativePeriod := from == "" || to == ""
if relativePeriod {
period := "1h"
if len(args) > 0 {
@@ -93,16 +91,16 @@ func parsePeriod(args []string) (time.Time, time.Time, error) {
} 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)
+ t1, err := time.Parse(time.RFC3339, from)
if err != nil {
return time.Time{}, time.Time{}, err
}
- to, err := time.Parse(time.RFC3339, toArg)
+ t2, err := time.Parse(time.RFC3339, to)
if err != nil {
return time.Time{}, time.Time{}, err
}
- if !to.After(from) {
+ if !t2.After(t1) {
return time.Time{}, time.Time{}, fmt.Errorf("--to must specify a time after --from")
}
- return from, to, nil
+ return t1, t2, nil
}
diff --git a/client/go/cmd/log_test.go b/client/go/cmd/log_test.go
index 3f6714b0d3c..4a6bd89c12d 100644
--- a/client/go/cmd/log_test.go
+++ b/client/go/cmd/log_test.go
@@ -2,52 +2,50 @@
package cmd
import (
- "path/filepath"
"testing"
"github.com/stretchr/testify/assert"
- "github.com/vespa-engine/vespa/client/go/build"
"github.com/vespa-engine/vespa/client/go/mock"
+ "github.com/vespa-engine/vespa/client/go/version"
)
func TestLog(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := mockApplicationPackage(t, false)
httpClient := &mock.HTTPClient{}
httpClient.NextResponse(200, `1632738690.905535 host1a.dev.aws-us-east-1c 806/53 logserver-container Container.com.yahoo.container.jdisc.ConfiguredApplication info Switching to the latest deployed set of configurations and components. Application config generation: 52532`)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "api-key-file", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = httpClient
- out, outErr := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T10:00:00Z", "--to", "2021-09-27T11:00:00Z"}}, t, httpClient)
- assert.Equal(t, "", outErr)
+ assert.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ assert.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ assert.Nil(t, cli.Run("auth", "api-key"))
+ assert.Nil(t, cli.Run("auth", "cert", pkgDir))
+ stdout.Reset()
+ assert.Nil(t, cli.Run("log", "--from", "2021-09-27T10:00:00Z", "--to", "2021-09-27T11:00:00Z"))
expected := "[2021-09-27 10:31:30.905535] host1a.dev.aws-us-east-1c info logserver-container Container.com.yahoo.container.jdisc.ConfiguredApplication Switching to the latest deployed set of configurations and components. Application config generation: 52532\n"
- assert.Equal(t, expected, out)
+ assert.Equal(t, expected, stdout.String())
- _, 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: cannot combine --from/--to with relative value: 1h\n", errOut)
+ assert.NotNil(t, cli.Run("log", "--from", "2021-09-27T13:12:49Z", "--to", "2021-09-27T13:15:00", "1h"))
+ assert.Contains(t, stderr.String(), "Error: invalid period: cannot combine --from/--to with relative value: 1h\n")
}
func TestLogOldClient(t *testing.T) {
- buildVersion := build.Version
- build.Version = "7.0.0"
- homeDir := filepath.Join(t.TempDir(), ".vespa")
+ cli, _, stderr := newTestCLI(t)
+ cli.version = version.MustParse("7.0.0")
+
pkgDir := mockApplicationPackage(t, false)
httpClient := &mock.HTTPClient{}
httpClient.NextResponse(200, `{"minVersion": "8.0.0"}`)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "api-key-file", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
- out, errOut := execute(command{homeDir: homeDir, args: []string{"log"}}, t, httpClient)
- assert.Equal(t, "", out)
+ httpClient.NextResponse(200, `1632738690.905535 host1a.dev.aws-us-east-1c 806/53 logserver-container Container.com.yahoo.container.jdisc.ConfiguredApplication info Switching to the latest deployed set of configurations and components. Application config generation: 52532`)
+ cli.httpClient = httpClient
+
+ assert.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ assert.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ assert.Nil(t, cli.Run("auth", "api-key"))
+ assert.Nil(t, cli.Run("auth", "cert", pkgDir))
+
+ assert.Nil(t, cli.Run("log"))
expected := "Error: client version 7.0.0 is less than the minimum supported version: 8.0.0\nHint: This is not a fatal error, but this version may not work as expected\nHint: Try 'vespa version' to check for a new version\n"
- assert.Equal(t, expected, errOut)
- build.Version = buildVersion
+ assert.Contains(t, stderr.String(), expected)
}
diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go
index 2ac480d05f5..3750037be88 100644
--- a/client/go/cmd/login.go
+++ b/client/go/cmd/login.go
@@ -2,35 +2,33 @@ package cmd
import (
"github.com/spf13/cobra"
- "github.com/vespa-engine/vespa/client/go/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/auth0"
)
-var loginCmd = &cobra.Command{
- Use: "login",
- Args: cobra.NoArgs,
- 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()
- if err != nil {
+func newLoginCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "login",
+ Args: cobra.NoArgs,
+ Short: "Authenticate the Vespa CLI",
+ Example: "$ vespa auth login",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ targetType, err := cli.config.targetType()
+ if err != nil {
+ return err
+ }
+ system, err := cli.system(targetType)
+ if err != nil {
+ return err
+ }
+ a, err := auth0.GetAuth0(cli.config.authConfigPath(), system.Name, system.URL)
+ if err != nil {
+ return err
+ }
+ _, err = auth0.RunLogin(ctx, a, false)
return err
- }
- targetType, err := getTargetType()
- if err != nil {
- return err
- }
- system, err := getSystem(targetType)
- if err != nil {
- return err
- }
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
- if err != nil {
- return err
- }
- _, err = auth0.RunLogin(ctx, a, false)
- return err
- },
+ },
+ }
}
diff --git a/client/go/cmd/logout.go b/client/go/cmd/logout.go
index b1f2477aba4..6cef5ee371c 100644
--- a/client/go/cmd/logout.go
+++ b/client/go/cmd/logout.go
@@ -2,34 +2,32 @@ package cmd
import (
"github.com/spf13/cobra"
- "github.com/vespa-engine/vespa/client/go/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/auth0"
)
-var logoutCmd = &cobra.Command{
- Use: "logout",
- Args: cobra.NoArgs,
- Short: "Log out of Vespa Cli",
- Example: "$ vespa auth logout",
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- cfg, err := LoadConfig()
- if err != nil {
+func newLogoutCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "logout",
+ Args: cobra.NoArgs,
+ Short: "Log out of Vespa Cli",
+ Example: "$ vespa auth logout",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ targetType, err := cli.config.targetType()
+ if err != nil {
+ return err
+ }
+ system, err := cli.system(targetType)
+ if err != nil {
+ return err
+ }
+ a, err := auth0.GetAuth0(cli.config.authConfigPath(), system.Name, system.URL)
+ if err != nil {
+ return err
+ }
+ err = auth0.RunLogout(a)
return err
- }
- targetType, err := getTargetType()
- if err != nil {
- return err
- }
- system, err := getSystem(targetType)
- if err != nil {
- return err
- }
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
- if err != nil {
- return err
- }
- err = auth0.RunLogout(a)
- return err
- },
+ },
+ }
}
diff --git a/client/go/cmd/man.go b/client/go/cmd/man.go
index 01fffd38a32..4d139adb244 100644
--- a/client/go/cmd/man.go
+++ b/client/go/cmd/man.go
@@ -8,24 +8,22 @@ import (
"github.com/spf13/cobra/doc"
)
-func init() {
- rootCmd.AddCommand(manCmd)
-}
-
-var manCmd = &cobra.Command{
- Use: "man directory",
- Short: "Generate man pages and write them to given directory",
- Args: cobra.ExactArgs(1),
- Hidden: true, // Not intended to be called by users
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- dir := args[0]
- err := doc.GenManTree(rootCmd, nil, dir)
- if err != nil {
- return fmt.Errorf("failed to write man pages: %w", err)
- }
- printSuccess("Man pages written to ", dir)
- return nil
- },
+func newManCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "man directory",
+ Short: "Generate man pages and write them to given directory",
+ Args: cobra.ExactArgs(1),
+ Hidden: true, // Not intended to be called by users
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ dir := args[0]
+ err := doc.GenManTree(cli.cmd, nil, dir)
+ if err != nil {
+ return fmt.Errorf("failed to write man pages: %w", err)
+ }
+ cli.printSuccess("Man pages written to ", dir)
+ return nil
+ },
+ }
}
diff --git a/client/go/cmd/man_test.go b/client/go/cmd/man_test.go
index dfbe04f4c8e..b885d6a8fac 100644
--- a/client/go/cmd/man_test.go
+++ b/client/go/cmd/man_test.go
@@ -12,7 +12,8 @@ import (
func TestMan(t *testing.T) {
tmpDir := t.TempDir()
- out, _ := execute(command{args: []string{"man", tmpDir}}, t, nil)
- assert.Equal(t, fmt.Sprintf("Success: Man pages written to %s\n", tmpDir), out)
+ cli, stdout, _ := newTestCLI(t)
+ assert.Nil(t, cli.Run("man", tmpDir))
+ assert.Equal(t, fmt.Sprintf("Success: Man pages written to %s\n", tmpDir), stdout.String())
assert.True(t, util.PathExists(filepath.Join(tmpDir, "vespa.1")))
}
diff --git a/client/go/cmd/prod.go b/client/go/cmd/prod.go
index 4ce126bebb4..a8480af34e5 100644
--- a/client/go/cmd/prod.go
+++ b/client/go/cmd/prod.go
@@ -6,6 +6,7 @@ import (
"bytes"
"errors"
"fmt"
+ "io"
"io/ioutil"
"log"
"os"
@@ -19,32 +20,29 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa/xml"
)
-func init() {
- rootCmd.AddCommand(prodCmd)
- prodCmd.AddCommand(prodInitCmd)
- prodCmd.AddCommand(prodSubmitCmd)
-}
-
-var prodCmd = &cobra.Command{
- Use: "prod",
- Short: "Deploy an application package to production in Vespa Cloud",
- Long: `Deploy an application package to production in Vespa Cloud.
+func newProdCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "prod",
+ Short: "Deploy an application package to production in Vespa Cloud",
+ Long: `Deploy an application package to production in Vespa Cloud.
Configure and deploy your application package to production in Vespa Cloud.`,
- Example: `$ vespa prod init
+ Example: `$ vespa prod init
$ vespa prod submit`,
- DisableAutoGenTag: true,
- SilenceUsage: false,
- Args: cobra.MinimumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- return fmt.Errorf("invalid command: %s", args[0])
- },
+ DisableAutoGenTag: true,
+ SilenceUsage: false,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return fmt.Errorf("invalid command: %s", args[0])
+ },
+ }
}
-var prodInitCmd = &cobra.Command{
- Use: "init",
- Short: "Modify service.xml and deployment.xml for production deployment",
- Long: `Modify service.xml and deployment.xml for production deployment.
+func newProdInitCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "init",
+ Short: "Modify service.xml and deployment.xml for production deployment",
+ Long: `Modify service.xml and deployment.xml for production deployment.
Only basic deployment configuration is available through this command. For
advanced configuration see the relevant Vespa Cloud documentation and make
@@ -53,62 +51,64 @@ changes to deployment.xml and services.xml directly.
Reference:
https://cloud.vespa.ai/en/reference/services
https://cloud.vespa.ai/en/reference/deployment`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- RunE: func(cmd *cobra.Command, args []string) error {
- appSource := applicationSource(args)
- pkg, err := vespa.FindApplicationPackage(appSource, false)
- if err != nil {
- return err
- }
- if pkg.IsZip() {
- return errHint(fmt.Errorf("cannot modify compressed application package %s", pkg.Path),
- "Try running 'mvn clean' and run this command again")
- }
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ appSource := applicationSource(args)
+ pkg, err := vespa.FindApplicationPackage(appSource, false)
+ if err != nil {
+ return err
+ }
+ if pkg.IsZip() {
+ return errHint(fmt.Errorf("cannot modify compressed application package %s", pkg.Path),
+ "Try running 'mvn clean' and run this command again")
+ }
- deploymentXML, err := readDeploymentXML(pkg)
- if err != nil {
- return fmt.Errorf("could not read deployment.xml: %w", err)
- }
- servicesXML, err := readServicesXML(pkg)
- if err != nil {
- return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err)
- }
- target, err := getTarget()
- if err != nil {
- return err
- }
+ deploymentXML, err := readDeploymentXML(pkg)
+ if err != nil {
+ return fmt.Errorf("could not read deployment.xml: %w", err)
+ }
+ servicesXML, err := readServicesXML(pkg)
+ if err != nil {
+ return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err)
+ }
+ target, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
- fmt.Fprint(stdout, "This will modify any existing ", color.YellowString("deployment.xml"), " and ", color.YellowString("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.GreenString("https://docs.vespa.ai/en/performance/sizing-search.html\n\n"))
- r := bufio.NewReader(stdin)
- deploymentXML, err = updateRegions(r, deploymentXML, target.Deployment().System)
- if err != nil {
- return err
- }
- servicesXML, err = updateNodes(r, servicesXML)
- if err != nil {
- return err
- }
+ fmt.Fprint(cli.Stdout, "This will modify any existing ", color.YellowString("deployment.xml"), " and ", color.YellowString("services.xml"),
+ "!\nBefore modification a backup of the original file will be created.\n\n")
+ fmt.Fprint(cli.Stdout, "A default value is suggested (shown inside brackets) based on\nthe files' existing contents. Press enter to use it.\n\n")
+ fmt.Fprint(cli.Stdout, "Abort the configuration at any time by pressing Ctrl-C. The\nfiles will remain untouched.\n\n")
+ fmt.Fprint(cli.Stdout, "See this guide for sizing a Vespa deployment:\n", color.GreenString("https://docs.vespa.ai/en/performance/sizing-search.html\n\n"))
+ r := bufio.NewReader(cli.Stdin)
+ deploymentXML, err = updateRegions(cli, r, deploymentXML, target.Deployment().System)
+ if err != nil {
+ return err
+ }
+ servicesXML, err = updateNodes(cli, r, servicesXML)
+ if err != nil {
+ return err
+ }
- fmt.Fprintln(stdout)
- if err := writeWithBackup(pkg, "deployment.xml", deploymentXML.String()); err != nil {
- return err
- }
- if err := writeWithBackup(pkg, "services.xml", servicesXML.String()); err != nil {
- return err
- }
- return nil
- },
+ fmt.Fprintln(cli.Stdout)
+ if err := writeWithBackup(cli.Stdout, pkg, "deployment.xml", deploymentXML.String()); err != nil {
+ return err
+ }
+ if err := writeWithBackup(cli.Stdout, pkg, "services.xml", servicesXML.String()); err != nil {
+ return err
+ }
+ return nil
+ },
+ }
}
-var prodSubmitCmd = &cobra.Command{
- Use: "submit",
- Short: "Submit your application for production deployment",
- Long: `Submit your application for production deployment.
+func newProdSubmitCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "submit",
+ Short: "Submit your application for production deployment",
+ Long: `Submit your application for production deployment.
This commands uploads your application package to Vespa Cloud and deploys it to
the production zones specified in deployment.xml.
@@ -123,58 +123,55 @@ by a continuous build system.
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
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Example: `$ mvn package # when adding custom Java components
$ vespa prod submit`,
- RunE: func(cmd *cobra.Command, args []string) error {
- target, err := getTarget()
- if err != nil {
- return err
- }
- if target.Type() != vespa.TargetCloud {
- // TODO: Add support for hosted
- return fmt.Errorf("prod submit does not support %s target", target.Type())
- }
- appSource := applicationSource(args)
- pkg, err := vespa.FindApplicationPackage(appSource, true)
- if err != nil {
- return err
- }
- cfg, err := LoadConfig()
- if err != nil {
- return err
- }
- if !pkg.HasDeployment() {
- return errHint(fmt.Errorf("no deployment.xml found"), "Try creating one with vespa prod init")
- }
- if pkg.TestPath == "" {
- 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")
- }
- if err := verifyTests(pkg, target); err != nil {
- return err
- }
- if !isCI() {
- printWarning("We recommend doing this only from a CD job", "See https://cloud.vespa.ai/en/getting-to-production")
- }
- opts, err := getDeploymentOptions(cfg, pkg, target)
- if err != nil {
- return err
- }
- if err := vespa.Submit(opts); err != nil {
- return fmt.Errorf("could not submit application for deployment: %w", err)
- } else {
- printSuccess("Submitted ", color.CyanString(pkg.Path), " for deployment")
- log.Printf("See %s for deployment progress\n", color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment",
- opts.Target.Deployment().System.ConsoleURL, opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application)))
- }
- return nil
- },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ target, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
+ if target.Type() != vespa.TargetCloud {
+ // TODO: Add support for hosted
+ return fmt.Errorf("prod submit does not support %s target", target.Type())
+ }
+ appSource := applicationSource(args)
+ pkg, err := vespa.FindApplicationPackage(appSource, true)
+ if err != nil {
+ return err
+ }
+ if !pkg.HasDeployment() {
+ return errHint(fmt.Errorf("no deployment.xml found"), "Try creating one with vespa prod init")
+ }
+ if pkg.TestPath == "" {
+ 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")
+ }
+ if err := verifyTests(cli, pkg); err != nil {
+ return err
+ }
+ if !cli.isCI() {
+ cli.printWarning("We recommend doing this only from a CD job", "See https://cloud.vespa.ai/en/getting-to-production")
+ }
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
+ if err := vespa.Submit(opts); err != nil {
+ return fmt.Errorf("could not submit application for deployment: %w", err)
+ } else {
+ cli.printSuccess("Submitted ", color.CyanString(pkg.Path), " for deployment")
+ log.Printf("See %s for deployment progress\n", color.CyanString(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment",
+ opts.Target.Deployment().System.ConsoleURL, opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application)))
+ }
+ return nil
+ },
+ }
}
-func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) error {
+func writeWithBackup(stdout io.Writer, pkg vespa.ApplicationPackage, filename, contents string) error {
dst := filepath.Join(pkg.Path, filename)
if util.PathExists(dst) {
data, err := ioutil.ReadFile(dst)
@@ -205,8 +202,8 @@ func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) er
return ioutil.WriteFile(dst, []byte(contents), 0644)
}
-func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (xml.Deployment, error) {
- regions, err := promptRegions(r, deploymentXML, system)
+func updateRegions(cli *CLI, stdin *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (xml.Deployment, error) {
+ regions, err := promptRegions(cli, stdin, deploymentXML, system)
if err != nil {
return xml.Deployment{}, err
}
@@ -225,10 +222,10 @@ func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.S
return deploymentXML, nil
}
-func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (string, error) {
- fmt.Fprintln(stdout, color.CyanString("> Deployment regions"))
- fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/zones"))
- fmt.Fprintf(stdout, "Example: %s\n\n", color.YellowString("aws-us-east-1c,aws-us-west-2a"))
+func promptRegions(cli *CLI, stdin *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (string, error) {
+ fmt.Fprintln(cli.Stdout, color.CyanString("> Deployment regions"))
+ fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/zones"))
+ fmt.Fprintf(cli.Stdout, "Example: %s\n\n", color.YellowString("aws-us-east-1c,aws-us-west-2a"))
var currentRegions []string
for _, r := range deploymentXML.Prod.Regions {
currentRegions = append(currentRegions, r.Name)
@@ -247,12 +244,12 @@ func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.S
}
return nil
}
- return prompt(r, "Which regions do you wish to deploy in?", strings.Join(currentRegions, ","), validator)
+ return prompt(cli, stdin, "Which regions do you wish to deploy in?", strings.Join(currentRegions, ","), validator)
}
-func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error) {
+func updateNodes(cli *CLI, r *bufio.Reader, servicesXML xml.Services) (xml.Services, error) {
for _, c := range servicesXML.Container {
- nodes, err := promptNodes(r, c.ID, c.Nodes)
+ nodes, err := promptNodes(cli, r, c.ID, c.Nodes)
if err != nil {
return xml.Services{}, err
}
@@ -261,7 +258,7 @@ func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error
}
}
for _, c := range servicesXML.Content {
- nodes, err := promptNodes(r, c.ID, c.Nodes)
+ nodes, err := promptNodes(cli, r, c.ID, c.Nodes)
if err != nil {
return xml.Services{}, err
}
@@ -272,8 +269,8 @@ func updateNodes(r *bufio.Reader, servicesXML xml.Services) (xml.Services, error
return servicesXML, nil
}
-func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml.Nodes, error) {
- count, err := promptNodeCount(r, clusterID, defaultValue.Count)
+func promptNodes(cli *CLI, r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml.Nodes, error) {
+ count, err := promptNodeCount(cli, r, clusterID, defaultValue.Count)
if err != nil {
return xml.Nodes{}, err
}
@@ -283,7 +280,7 @@ func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml
if resources != nil {
defaultSpec = defaultValue.Resources.String()
}
- spec, err := promptResources(r, clusterID, defaultSpec)
+ spec, err := promptResources(cli, r, clusterID, defaultSpec)
if err != nil {
return xml.Nodes{}, err
}
@@ -299,21 +296,21 @@ func promptNodes(r *bufio.Reader, clusterID string, defaultValue xml.Nodes) (xml
return xml.Nodes{Count: count, Resources: resources}, nil
}
-func promptNodeCount(r *bufio.Reader, clusterID string, nodeCount string) (string, error) {
- fmt.Fprintln(stdout, color.CyanString("\n> Node count: "+clusterID+" cluster"))
- fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services"))
- fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.YellowString("4"), color.YellowString("[2,8]"))
+func promptNodeCount(cli *CLI, stdin *bufio.Reader, clusterID string, nodeCount string) (string, error) {
+ fmt.Fprintln(cli.Stdout, color.CyanString("\n> Node count: "+clusterID+" cluster"))
+ fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services"))
+ fmt.Fprintf(cli.Stdout, "Example: %s\nExample: %s\n\n", color.YellowString("4"), color.YellowString("[2,8]"))
validator := func(input string) error {
_, _, err := xml.ParseNodeCount(input)
return err
}
- return prompt(r, fmt.Sprintf("How many nodes should the %s cluster have?", color.CyanString(clusterID)), nodeCount, validator)
+ return prompt(cli, stdin, fmt.Sprintf("How many nodes should the %s cluster have?", color.CyanString(clusterID)), nodeCount, validator)
}
-func promptResources(r *bufio.Reader, clusterID string, resources string) (string, error) {
- fmt.Fprintln(stdout, color.CyanString("\n> Node resources: "+clusterID+" cluster"))
- fmt.Fprintf(stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services"))
- fmt.Fprintf(stdout, "Example: %s\nExample: %s\n\n", color.YellowString("auto"), color.YellowString("vcpu=4,memory=8Gb,disk=100Gb"))
+func promptResources(cli *CLI, stdin *bufio.Reader, clusterID string, resources string) (string, error) {
+ fmt.Fprintln(cli.Stdout, color.CyanString("\n> Node resources: "+clusterID+" cluster"))
+ fmt.Fprintf(cli.Stdout, "Documentation: %s\n", color.GreenString("https://cloud.vespa.ai/en/reference/services"))
+ fmt.Fprintf(cli.Stdout, "Example: %s\nExample: %s\n\n", color.YellowString("auto"), color.YellowString("vcpu=4,memory=8Gb,disk=100Gb"))
validator := func(input string) error {
if input == "auto" {
return nil
@@ -321,7 +318,7 @@ func promptResources(r *bufio.Reader, clusterID string, resources string) (strin
_, err := xml.ParseResources(input)
return err
}
- return prompt(r, fmt.Sprintf("Which resources should each node in the %s cluster have?", color.CyanString(clusterID)), resources, validator)
+ return prompt(cli, stdin, fmt.Sprintf("Which resources should each node in the %s cluster have?", color.CyanString(clusterID)), resources, validator)
}
func readDeploymentXML(pkg vespa.ApplicationPackage) (xml.Deployment, error) {
@@ -345,17 +342,17 @@ 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, error) {
+func prompt(cli *CLI, stdin *bufio.Reader, question, defaultAnswer string, validator func(input string) error) (string, error) {
var input string
for input == "" {
- fmt.Fprint(stdout, question)
+ fmt.Fprint(cli.Stdout, question)
if defaultAnswer != "" {
- fmt.Fprint(stdout, " [", color.YellowString(defaultAnswer), "]")
+ fmt.Fprint(cli.Stdout, " [", color.YellowString(defaultAnswer), "]")
}
- fmt.Fprint(stdout, " ")
+ fmt.Fprint(cli.Stdout, " ")
var err error
- input, err = r.ReadString('\n')
+ input, err = stdin.ReadString('\n')
if err != nil {
return "", err
}
@@ -365,15 +362,15 @@ func prompt(r *bufio.Reader, question, defaultAnswer string, validator func(inpu
}
if err := validator(input); err != nil {
- printErrHint(err)
- fmt.Fprintln(stderr)
+ cli.printErrHint(err)
+ fmt.Fprintln(cli.Stderr)
input = ""
}
}
return input, nil
}
-func verifyTests(app vespa.ApplicationPackage, target vespa.Target) error {
+func verifyTests(cli *CLI, app vespa.ApplicationPackage) error {
// TODO: system-test, staging-setup and staging-test should be required if the application
// does not have any Java tests.
suites := map[string]bool{
@@ -392,14 +389,14 @@ func verifyTests(app vespa.ApplicationPackage, target vespa.Target) error {
testPath = path
}
for suite, required := range suites {
- if err := verifyTest(testPath, suite, target, required); err != nil {
+ if err := verifyTest(cli, testPath, suite, required); err != nil {
return err
}
}
return nil
}
-func verifyTest(testsParent string, suite string, target vespa.Target, required bool) error {
+func verifyTest(cli *CLI, testsParent string, suite string, required bool) error {
testDirectory := filepath.Join(testsParent, "tests", suite)
_, err := os.Stat(testDirectory)
if err != nil {
@@ -413,6 +410,6 @@ func verifyTest(testsParent string, suite string, target vespa.Target, required
}
return nil
}
- _, _, err = runTests(testDirectory, true)
+ _, _, err = runTests(cli, "", testDirectory, true)
return err
}
diff --git a/client/go/cmd/prod_test.go b/client/go/cmd/prod_test.go
index 99ec57c945f..38dd1a773e3 100644
--- a/client/go/cmd/prod_test.go
+++ b/client/go/cmd/prod_test.go
@@ -15,7 +15,6 @@ import (
)
func TestProdInit(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := filepath.Join(t.TempDir(), "app")
createApplication(t, pkgDir, false)
@@ -42,7 +41,10 @@ func TestProdInit(t *testing.T) {
}
var buf bytes.Buffer
buf.WriteString(strings.Join(answers, "\n") + "\n")
- execute(command{stdin: &buf, homeDir: homeDir, args: []string{"prod", "init", pkgDir}}, t, nil)
+
+ cli, _, _ := newTestCLI(t)
+ cli.Stdin = &buf
+ assert.Nil(t, cli.Run("prod", "init", pkgDir))
// Verify contents
deploymentPath := filepath.Join(pkgDir, "src", "main", "application", "deployment.xml")
@@ -144,18 +146,20 @@ func writeTest(path string, content []byte, t *testing.T) {
}
func TestProdSubmit(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := filepath.Join(t.TempDir(), "app")
createApplication(t, pkgDir, false)
httpClient := &mock.HTTPClient{}
httpClient.NextResponse(200, `ok`)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
- // Zipping requires relative paths, so much let command run from pkgDir, then reset cwd for subsequent tests.
+ cli, stdout, _ := newTestCLI(t, "CI=true")
+ cli.httpClient = httpClient
+ assert.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ assert.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ assert.Nil(t, cli.Run("auth", "api-key"))
+ assert.Nil(t, cli.Run("auth", "cert", pkgDir))
+
+ // Zipping requires relative paths, so must let command run from pkgDir, then reset cwd for subsequent tests.
if cwd, err := os.Getwd(); err != nil {
t.Fatal(err)
} else {
@@ -164,26 +168,25 @@ func TestProdSubmit(t *testing.T) {
if err := os.Chdir(pkgDir); err != nil {
t.Fatal(err)
}
- if err := os.Setenv("CI", "true"); err != nil {
- t.Fatal(err)
- }
- out, outErr := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- assert.Equal(t, "", outErr)
- assert.Contains(t, out, "Success: Submitted")
- assert.Contains(t, out, "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
+
+ stdout.Reset()
+ assert.Nil(t, cli.Run("prod", "submit", "-k", filepath.Join(cli.config.homeDir, "t1.api-key.pem")))
+ assert.Contains(t, stdout.String(), "Success: Submitted")
+ assert.Contains(t, stdout.String(), "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
}
func TestProdSubmitWithJava(t *testing.T) {
- homeDir := filepath.Join(t.TempDir(), ".vespa")
pkgDir := filepath.Join(t.TempDir(), "app")
createApplication(t, pkgDir, true)
httpClient := &mock.HTTPClient{}
httpClient.NextResponse(200, `ok`)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
+ cli, stdout, _ := newTestCLI(t, "CI=true")
+ cli.httpClient = httpClient
+ assert.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ assert.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ assert.Nil(t, cli.Run("auth", "api-key"))
+ assert.Nil(t, cli.Run("auth", "cert", pkgDir))
// Copy an application package pre-assembled with mvn package
testAppDir := filepath.Join("testdata", "applications", "withDeployment", "target")
@@ -192,10 +195,10 @@ func TestProdSubmitWithJava(t *testing.T) {
testZipFile := filepath.Join(testAppDir, "application-test.zip")
copyFile(t, filepath.Join(pkgDir, "target", "application-test.zip"), testZipFile)
- out, outErr := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem"), pkgDir}}, t, httpClient)
- assert.Equal(t, "", outErr)
- assert.Contains(t, out, "Success: Submitted")
- assert.Contains(t, out, "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
+ stdout.Reset()
+ assert.Nil(t, cli.Run("prod", "submit", "-k", filepath.Join(cli.config.homeDir, "t1.api-key.pem"), pkgDir))
+ assert.Contains(t, stdout.String(), "Success: Submitted")
+ assert.Contains(t, stdout.String(), "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
}
func copyFile(t *testing.T, dstFilename, srcFilename string) {
diff --git a/client/go/cmd/query.go b/client/go/cmd/query.go
index cb0fd923c4e..c5868e2f71c 100644
--- a/client/go/cmd/query.go
+++ b/client/go/cmd/query.go
@@ -7,7 +7,6 @@ package cmd
import (
"fmt"
"io"
- "io/ioutil"
"log"
"net/http"
"net/url"
@@ -21,49 +20,45 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-var (
- queryPrintCurl bool
- queryTimeoutSecs int
-)
-
-func init() {
- rootCmd.AddCommand(queryCmd)
- queryCmd.PersistentFlags().BoolVarP(&queryPrintCurl, "verbose", "v", false, "Print the equivalent curl command for the query")
- 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 music where album contains 'head';" hits=5`,
- Long: `Issue a query to Vespa.
+func newQueryCmd(cli *CLI) *cobra.Command {
+ var (
+ printCurl bool
+ queryTimeoutSecs int
+ )
+ cmd := &cobra.Command{
+ Use: "query query-parameters",
+ Short: "Issue a query to Vespa",
+ 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),
- RunE: query,
+ // TODO: Support referencing a query json file
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MinimumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return query(cli, args, queryTimeoutSecs, printCurl)
+ },
+ }
+ cmd.PersistentFlags().BoolVarP(&printCurl, "verbose", "v", false, "Print the equivalent curl command for the query")
+ cmd.Flags().IntVarP(&queryTimeoutSecs, "timeout", "T", 10, "Timeout for the query in seconds")
+ return cmd
}
-func printCurl(url string, service *vespa.Service) error {
- out := ioutil.Discard
- if queryPrintCurl {
- out = stderr
- }
+func printCurl(stderr io.Writer, url string, service *vespa.Service) error {
cmd, err := curl.RawArgs(url)
if err != nil {
return err
}
cmd.Certificate = service.TLSOptions.CertificateFile
cmd.PrivateKey = service.TLSOptions.PrivateKeyFile
- _, err = io.WriteString(out, cmd.String()+"\n")
+ _, err = io.WriteString(stderr, cmd.String()+"\n")
return err
}
-func query(cmd *cobra.Command, arguments []string) error {
- service, err := getService(vespa.QueryService, 0, "")
+func query(cli *CLI, arguments []string, timeoutSecs int, curl bool) error {
+ service, err := cli.service(vespa.QueryService, 0, "")
if err != nil {
return err
}
@@ -76,7 +71,7 @@ func query(cmd *cobra.Command, arguments []string) error {
queryTimeout := urlQuery.Get("timeout")
if queryTimeout == "" {
// No timeout set by user, use the timeout option
- queryTimeout = fmt.Sprintf("%ds", queryTimeoutSecs)
+ queryTimeout = fmt.Sprintf("%ds", timeoutSecs)
urlQuery.Set("timeout", queryTimeout)
}
url.RawQuery = urlQuery.Encode()
@@ -84,8 +79,10 @@ func query(cmd *cobra.Command, arguments []string) error {
if err != nil {
return fmt.Errorf("invalid query timeout: %w", err)
}
- if err := printCurl(url.String(), service); err != nil {
- return err
+ if curl {
+ if err := printCurl(cli.Stderr, url.String(), service); err != nil {
+ return err
+ }
}
response, err := service.Do(&http.Request{URL: url}, deadline+time.Second) // Slightly longer than query timeout
if err != nil {
diff --git a/client/go/cmd/query_test.go b/client/go/cmd/query_test.go
index d57268b248e..a9348f8ddeb 100644
--- a/client/go/cmd/query_test.go
+++ b/client/go/cmd/query_test.go
@@ -22,10 +22,13 @@ func TestQuery(t *testing.T) {
func TestQueryVerbose(t *testing.T) {
client := &mock.HTTPClient{}
client.NextResponse(200, "{\"query\":\"result\"}")
- cmd := command{args: []string{"query", "-v", "select from sources * where title contains 'foo'"}}
- out, errOut := execute(cmd, t, client)
- assert.Equal(t, "curl http://127.0.0.1:8080/search/\\?timeout=10s\\&yql=select+from+sources+%2A+where+title+contains+%27foo%27\n", errOut)
- assert.Equal(t, "{\n \"query\": \"result\"\n}\n", out)
+
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = client
+
+ assert.Nil(t, cli.Run("query", "-v", "select from sources * where title contains 'foo'"))
+ assert.Equal(t, "curl http://127.0.0.1:8080/search/\\?timeout=10s\\&yql=select+from+sources+%2A+where+title+contains+%27foo%27\n", stderr.String())
+ assert.Equal(t, "{\n \"query\": \"result\"\n}\n", stdout.String())
}
func TestQueryNonJsonResult(t *testing.T) {
@@ -57,9 +60,14 @@ func TestServerError(t *testing.T) {
func assertQuery(t *testing.T, expectedQuery string, query ...string) {
client := &mock.HTTPClient{}
client.NextResponse(200, "{\"query\":\"result\"}")
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+
+ args := []string{"query"}
+ assert.Nil(t, cli.Run(append(args, query...)...))
assert.Equal(t,
"{\n \"query\": \"result\"\n}\n",
- executeCommand(t, client, []string{"query"}, query),
+ stdout.String(),
"query output")
queryURL, err := queryServiceURL(client)
require.Nil(t, err)
@@ -69,27 +77,27 @@ func assertQuery(t *testing.T, expectedQuery string, query ...string) {
func assertQueryError(t *testing.T, status int, errorMessage string) {
client := &mock.HTTPClient{}
client.NextResponse(status, errorMessage)
- _, outErr := execute(command{args: []string{"query", "yql=select from sources * where title contains 'foo'"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("query", "yql=select from sources * where title contains 'foo'"))
assert.Equal(t,
"Error: invalid query: Status "+strconv.Itoa(status)+"\n"+errorMessage+"\n",
- outErr,
+ stderr.String(),
"error output")
}
func assertQueryServiceError(t *testing.T, status int, errorMessage string) {
client := &mock.HTTPClient{}
client.NextResponse(status, errorMessage)
- _, outErr := execute(command{args: []string{"query", "yql=select from sources * where title contains 'foo'"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("query", "yql=select from sources * where title contains 'foo'"))
assert.Equal(t,
"Error: Status "+strconv.Itoa(status)+" from container at 127.0.0.1:8080\n"+errorMessage+"\n",
- outErr,
+ stderr.String(),
"error output")
}
func queryServiceURL(client *mock.HTTPClient) (string, error) {
- service, err := getService("query", 0, "")
- if err != nil {
- return "", err
- }
- return service.BaseURL, nil
+ return "http://127.0.0.1:8080", nil
}
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go
index 6f29a09e177..d4e071ba244 100644
--- a/client/go/cmd/root.go
+++ b/client/go/cmd/root.go
@@ -1,22 +1,69 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-// Root Cobra command: vespa
-// author: bratseth
-
package cmd
import (
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
"github.com/fatih/color"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/build"
+ "github.com/vespa-engine/vespa/client/go/util"
+ "github.com/vespa-engine/vespa/client/go/version"
+ "github.com/vespa-engine/vespa/client/go/vespa"
+)
+
+const (
+ applicationFlag = "application"
+ targetFlag = "target"
+ waitFlag = "wait"
+ colorFlag = "color"
+ quietFlag = "quiet"
+ apiKeyFileFlag = "api-key-file"
+ apiKeyFlag = "api-key"
)
+// CLI holds the Vespa CLI command tree, configuration and dependencies.
+type CLI struct {
+ // Environment holds the process environment.
+ Environment map[string]string
+ Stdin io.ReadWriter
+ Stdout io.Writer
+ Stderr io.Writer
+
+ cmd *cobra.Command
+ flags *Flags
+ config *Config
+ version version.Version
+
+ httpClient util.HTTPClient
+ exec executor
+ isTerminal func() bool
+}
+
+// Flags holds the global Flags of Vespa CLI.
+type Flags struct {
+ target string
+ application string
+ waitSecs int
+ color string
+ quiet bool
+ apiKeyFile string
+}
+
// 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 {
@@ -26,8 +73,24 @@ type ErrCLI struct {
error
}
-var (
- rootCmd = &cobra.Command{
+// 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} }
+
+type executor interface {
+ LookPath(name string) (string, error)
+ Run(name string, args ...string) ([]byte, error)
+}
+
+type execSubprocess struct{}
+
+func (c *execSubprocess) LookPath(name string) (string, error) { return exec.LookPath(name) }
+func (c *execSubprocess) Run(name string, args ...string) ([]byte, error) {
+ return exec.Command(name, args...).Output()
+}
+
+// New creates the Vespa CLI, writing output to stdout and stderr, and reading environment variables from environment.
+func New(stdout, stderr io.Writer, environment []string) (*CLI, error) {
+ cmd := &cobra.Command{
Use: "vespa command-name",
Short: "The command-line tool for Vespa.ai",
Long: `The command-line tool for Vespa.ai.
@@ -39,63 +102,77 @@ Vespa documentation: https://docs.vespa.ai`,
DisableAutoGenTag: true,
SilenceErrors: true, // We have our own error printing
SilenceUsage: false,
- PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
- return configureOutput()
- },
- Args: cobra.MinimumNArgs(1),
+ Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid command: %s", args[0])
},
}
+ env := make(map[string]string)
+ for _, entry := range environment {
+ parts := strings.SplitN(entry, "=", 2)
+ env[parts[0]] = parts[1]
+ }
+ version, err := version.Parse(build.Version)
+ if err != nil {
+ return nil, err
+ }
+ cli := CLI{
+ Environment: env,
+ Stdin: os.Stdin,
+ Stdout: stdout,
+ Stderr: stderr,
- targetArg string
- applicationArg string
- waitSecsArg int
- colorArg string
- quietArg bool
- apiKeyFileArg string
- stdin io.ReadWriter = os.Stdin
-
- stdout = colorable.NewColorableStdout()
- stderr = colorable.NewColorableStderr()
-)
-
-const (
- applicationFlag = "application"
- targetFlag = "target"
- waitFlag = "wait"
- colorFlag = "color"
- quietFlag = "quiet"
- apiKeyFileFlag = "api-key-file"
- apiKeyFlag = "api-key"
-)
-
-func isTerminal() bool {
- if f, ok := stdout.(*os.File); ok {
- return isatty.IsTerminal(f.Fd())
+ version: version,
+ cmd: cmd,
+ httpClient: util.CreateClient(time.Second * 10),
+ exec: &execSubprocess{},
}
- if f, ok := stderr.(*os.File); ok {
- return isatty.IsTerminal(f.Fd())
+ cli.isTerminal = func() bool { return isTerminal(cli.Stdout) || isTerminal(cli.Stderr) }
+ cli.configureFlags()
+ if err := cli.loadConfig(); err != nil {
+ return nil, err
}
- return false
+ cli.configureCommands()
+ cmd.PersistentPreRunE = cli.configureOutput
+ return &cli, nil
}
-func configureOutput() error {
- config, err := LoadConfig()
+func (c *CLI) loadConfig() error {
+ bindings := NewConfigBindings()
+ bindings.bindFlag(targetFlag, c.cmd)
+ bindings.bindFlag(applicationFlag, c.cmd)
+ bindings.bindFlag(waitFlag, c.cmd)
+ bindings.bindFlag(colorFlag, c.cmd)
+ bindings.bindFlag(quietFlag, c.cmd)
+ bindings.bindFlag(apiKeyFileFlag, c.cmd)
+ bindings.bindEnvironment(apiKeyFlag, "VESPA_CLI_API_KEY")
+ bindings.bindEnvironment(apiKeyFileFlag, "VESPA_CLI_API_KEY_FILE")
+ config, err := loadConfig(c.Environment, bindings)
if err != nil {
return err
}
- if quiet, _ := config.Get(quietFlag); quiet == "true" {
- stdout = ioutil.Discard
+ c.config = config
+ return nil
+}
+
+func (c *CLI) configureOutput(cmd *cobra.Command, args []string) error {
+ if f, ok := c.Stdout.(*os.File); ok {
+ c.Stdout = colorable.NewColorable(f)
+ }
+ if f, ok := c.Stderr.(*os.File); ok {
+ c.Stderr = colorable.NewColorable(f)
+ }
+ if quiet, _ := c.config.get(quietFlag); quiet == "true" {
+ c.Stdout = ioutil.Discard
}
log.SetFlags(0) // No timestamps
- log.SetOutput(stdout)
- colorValue, _ := config.Get(colorFlag)
+ log.SetOutput(c.Stdout)
+ colorValue, _ := c.config.get(colorFlag)
colorize := false
switch colorValue {
case "auto":
- _, nocolor := os.LookupEnv("NO_COLOR") // https://no-color.org
- colorize = !nocolor && isTerminal()
+ _, nocolor := c.Environment["NO_COLOR"] // https://no-color.org
+ colorize = !nocolor && c.isTerminal()
case "always":
colorize = true
case "never":
@@ -106,39 +183,329 @@ func configureOutput() error {
return nil
}
-func init() {
- rootCmd.PersistentFlags().StringVarP(&targetArg, targetFlag, "t", "local", "The name or URL of the recipient of this command")
- rootCmd.PersistentFlags().StringVarP(&applicationArg, applicationFlag, "a", "", "The application to manage")
- rootCmd.PersistentFlags().IntVarP(&waitSecsArg, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready")
- rootCmd.PersistentFlags().StringVarP(&colorArg, colorFlag, "c", "auto", "Whether to use colors in output. Can be \"auto\", \"never\" or \"always\"")
- rootCmd.PersistentFlags().BoolVarP(&quietArg, quietFlag, "q", false, "Quiet mode. Only errors are printed.")
- rootCmd.PersistentFlags().StringVarP(&apiKeyFileArg, apiKeyFileFlag, "k", "", "Path to API key used for deployment authentication")
+func (c *CLI) configureFlags() {
+ flags := Flags{}
+ c.cmd.PersistentFlags().StringVarP(&flags.target, targetFlag, "t", "local", "The name or URL of the recipient of this command")
+ c.cmd.PersistentFlags().StringVarP(&flags.application, applicationFlag, "a", "", "The application to manage")
+ c.cmd.PersistentFlags().IntVarP(&flags.waitSecs, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready")
+ c.cmd.PersistentFlags().StringVarP(&flags.color, colorFlag, "c", "auto", "Whether to use colors in output.")
+ c.cmd.PersistentFlags().BoolVarP(&flags.quiet, quietFlag, "q", false, "Quiet mode. Only errors will be printed")
+ c.cmd.PersistentFlags().StringVarP(&flags.apiKeyFile, apiKeyFileFlag, "k", "", "Path to API key used for cloud authentication")
+ c.flags = &flags
+}
- bindFlagToConfig(targetFlag, rootCmd)
- bindFlagToConfig(applicationFlag, rootCmd)
- bindFlagToConfig(waitFlag, rootCmd)
- bindFlagToConfig(colorFlag, rootCmd)
- bindFlagToConfig(quietFlag, rootCmd)
- bindFlagToConfig(apiKeyFileFlag, rootCmd)
+func (c *CLI) configureCommands() {
+ rootCmd := c.cmd
+ authCmd := newAuthCmd()
+ certCmd := newCertCmd(c, false)
+ configCmd := newConfigCmd()
+ documentCmd := newDocumentCmd(c)
+ prodCmd := newProdCmd()
+ statusCmd := newStatusCmd(c)
+ certCmd.AddCommand(newCertAddCmd(c)) // auth cert add
+ authCmd.AddCommand(certCmd) // auth cert
+ authCmd.AddCommand(newAPIKeyCmd(c, false)) // auth api-key
+ authCmd.AddCommand(newLoginCmd(c)) // auth login
+ authCmd.AddCommand(newLogoutCmd(c)) // auth logout
+ rootCmd.AddCommand(authCmd) // auth
+ rootCmd.AddCommand(newCertCmd(c, true)) // cert TODO: Remove this after 2022-06-01
+ rootCmd.AddCommand(newAPIKeyCmd(c, true)) // api-key TODO: Remove this after 2022-06-01
+ rootCmd.AddCommand(newCloneCmd(c)) // clone
+ configCmd.AddCommand(newConfigGetCmd(c)) // config get
+ configCmd.AddCommand(newConfigSetCmd(c)) // config set
+ rootCmd.AddCommand(configCmd) // config
+ rootCmd.AddCommand(newCurlCmd(c)) // curl
+ rootCmd.AddCommand(newDeployCmd(c)) // deploy
+ rootCmd.AddCommand(newPrepareCmd(c)) // prepare
+ rootCmd.AddCommand(newActivateCmd(c)) // activate
+ documentCmd.AddCommand(newDocumentPutCmd(c)) // document put
+ documentCmd.AddCommand(newDocumentUpdateCmd(c)) // document update
+ documentCmd.AddCommand(newDocumentRemoveCmd(c)) // document remove
+ documentCmd.AddCommand(newDocumentGetCmd(c)) // document get
+ rootCmd.AddCommand(documentCmd) // document
+ rootCmd.AddCommand(newLogCmd(c)) // log
+ rootCmd.AddCommand(newManCmd(c)) // man
+ prodCmd.AddCommand(newProdInitCmd(c)) // prod init
+ prodCmd.AddCommand(newProdSubmitCmd(c)) // prod submit
+ rootCmd.AddCommand(prodCmd) // prod
+ rootCmd.AddCommand(newQueryCmd(c)) // query
+ statusCmd.AddCommand(newStatusQueryCmd(c)) // status query
+ statusCmd.AddCommand(newStatusDocumentCmd(c)) // status document
+ statusCmd.AddCommand(newStatusDeployCmd(c)) // status deploy
+ rootCmd.AddCommand(statusCmd) // status
+ rootCmd.AddCommand(newTestCmd(c)) // test
+ rootCmd.AddCommand(newVersionCmd(c)) // version
+}
- bindEnvToConfig(apiKeyFlag, "VESPA_CLI_API_KEY")
- bindEnvToConfig(apiKeyFileFlag, "VESPA_CLI_API_KEY_FILE")
+func (c *CLI) printErrHint(err error, hints ...string) {
+ fmt.Fprintln(c.Stderr, color.RedString("Error:"), err)
+ for _, hint := range hints {
+ fmt.Fprintln(c.Stderr, color.CyanString("Hint:"), hint)
+ }
}
-// 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} }
+func (c *CLI) printSuccess(msg ...interface{}) {
+ log.Print(color.GreenString("Success: "), fmt.Sprint(msg...))
+}
+
+func (c *CLI) printWarning(msg string, hints ...string) {
+ fmt.Fprintln(c.Stderr, color.YellowString("Warning:"), msg)
+ for _, hint := range hints {
+ fmt.Fprintln(c.Stderr, color.CyanString("Hint:"), hint)
+ }
+}
+
+// target creates a target according the configuration of this CLI. If zone is empty, the default zone for the system is
+// used. If logLevel is empty, it defaults to "info".
+func (c *CLI) target(zone, logLevel string) (vespa.Target, error) {
+ target, err := c.createTarget(zone, logLevel)
+ if err != nil {
+ return nil, err
+ }
+ if !c.isCloudCI() { // Vespa Cloud always runs an up-to-date version
+ if err := target.CheckVersion(c.version); err != nil {
+ c.printErrHint(err, "This is not a fatal error, but this version may not work as expected", "Try 'vespa version' to check for a new version")
+ }
+ }
+ return target, nil
+}
-// Execute executes command and prints any errors.
-func Execute() error {
- err := rootCmd.Execute()
+func (c *CLI) createTarget(zone, logLevel string) (vespa.Target, error) {
+ targetType, err := c.config.targetType()
+ if err != nil {
+ return nil, err
+ }
+ if strings.HasPrefix(targetType, "http") {
+ return vespa.CustomTarget(c.httpClient, targetType), nil
+ }
+ switch targetType {
+ case vespa.TargetLocal:
+ return vespa.LocalTarget(c.httpClient), nil
+ case vespa.TargetCloud, vespa.TargetHosted:
+ return c.createCloudTarget(targetType, zone, logLevel)
+ }
+ return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud', 'hosted' or an URL")
+}
+
+func (c *CLI) createCloudTarget(targetType, zone, logLevel string) (vespa.Target, error) {
+ system, err := c.system(targetType)
+ if err != nil {
+ return nil, err
+ }
+ deployment, err := c.config.deploymentIn(zone, system)
+ if err != nil {
+ return nil, err
+ }
+ endpoints, err := c.endpointsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+ var (
+ apiKey []byte
+ authConfigPath string
+ apiTLSOptions vespa.TLSOptions
+ deploymentTLSOptions vespa.TLSOptions
+ )
+ switch targetType {
+ case vespa.TargetCloud:
+ if c.config.useAPIKey(c, system, deployment.Application.Tenant) {
+ apiKey, err = c.config.readAPIKey(deployment.Application.Tenant)
+ if err != nil {
+ return nil, err
+ }
+ }
+ authConfigPath = c.config.authConfigPath()
+ kp, err := c.config.x509KeyPair(deployment.Application)
+ if err != nil {
+ return nil, errHint(err, "Deployment to cloud requires a certificate. Try 'vespa auth cert'")
+ }
+ deploymentTLSOptions = vespa.TLSOptions{
+ KeyPair: kp.KeyPair,
+ CertificateFile: kp.CertificateFile,
+ PrivateKeyFile: kp.PrivateKeyFile,
+ }
+ case vespa.TargetHosted:
+ kp, err := athenzKeyPair()
+ if err != nil {
+ return nil, err
+ }
+ apiTLSOptions = vespa.TLSOptions{
+ KeyPair: kp.KeyPair,
+ CertificateFile: kp.CertificateFile,
+ PrivateKeyFile: kp.PrivateKeyFile,
+ }
+ deploymentTLSOptions = apiTLSOptions
+ default:
+ return nil, fmt.Errorf("invalid cloud target: %s", targetType)
+ }
+ apiOptions := vespa.APIOptions{
+ System: system,
+ TLSOptions: apiTLSOptions,
+ APIKey: apiKey,
+ AuthConfigPath: authConfigPath,
+ }
+ deploymentOptions := vespa.CloudDeploymentOptions{
+ Deployment: deployment,
+ TLSOptions: deploymentTLSOptions,
+ ClusterURLs: endpoints,
+ }
+ if logLevel == "" {
+ logLevel = "info"
+ }
+ logOptions := vespa.LogOptions{
+ Writer: c.Stdout,
+ Level: vespa.LogLevel(logLevel),
+ }
+ return vespa.CloudTarget(c.httpClient, apiOptions, deploymentOptions, logOptions)
+}
+
+// system returns the appropiate system for the target configured in this CLI.
+func (c *CLI) system(targetType string) (vespa.System, error) {
+ name := c.Environment["VESPA_CLI_CLOUD_SYSTEM"]
+ if name != "" {
+ return vespa.GetSystem(name)
+ }
+ switch targetType {
+ case vespa.TargetHosted:
+ return vespa.MainSystem, nil
+ case vespa.TargetCloud:
+ return vespa.PublicSystem, nil
+ }
+ return vespa.System{}, fmt.Errorf("no default system found for %s target", targetType)
+}
+
+// service returns the service identified by given name and optionally cluster. This function blocks according to the
+// wait period configured in this CLI. The parameter sessionOrRunID specifies either the session ID (local target) or
+// run ID (cloud target) to wait for.
+func (c *CLI) service(name string, sessionOrRunID int64, cluster string) (*vespa.Service, error) {
+ t, err := c.target("", "")
+ if err != nil {
+ return nil, err
+ }
+ timeout := time.Duration(c.flags.waitSecs) * time.Second
+ if timeout > 0 {
+ log.Printf("Waiting up to %s %s for %s service to become available ...", color.CyanString(strconv.Itoa(c.flags.waitSecs)), color.CyanString("seconds"), color.CyanString(name))
+ }
+ s, err := t.Service(name, timeout, sessionOrRunID, cluster)
+ if err != nil {
+ return nil, fmt.Errorf("service '%s' is unavailable: %w", name, err)
+ }
+ return s, nil
+}
+
+func (c *CLI) createDeploymentOptions(pkg vespa.ApplicationPackage, target vespa.Target) (vespa.DeploymentOptions, error) {
+ opts := vespa.DeploymentOptions{ApplicationPackage: pkg, Target: target}
+ if opts.IsCloud() {
+ if target.Type() == vespa.TargetCloud && !opts.ApplicationPackage.HasCertificate() {
+ hint := "Try 'vespa auth cert'"
+ return vespa.DeploymentOptions{}, errHint(fmt.Errorf("missing certificate in application package"), "Applications in Vespa Cloud require a certificate", hint)
+ }
+ }
+ opts.Timeout = time.Duration(c.flags.waitSecs) * time.Second
+ opts.HTTPClient = c.httpClient
+ return opts, nil
+}
+
+// isCI returns true if running inside a continuous integration environment.
+func (c *CLI) isCI() bool {
+ _, ok := c.Environment["CI"]
+ return ok
+}
+
+// isCloudCI returns true if running inside a Vespa Cloud deployment job.
+func (c *CLI) isCloudCI() bool {
+ _, ok := c.Environment["VESPA_CLI_CLOUD_CI"]
+ return ok
+}
+
+func (c *CLI) endpointsFromEnv() (map[string]string, error) {
+ endpointsString := c.Environment["VESPA_CLI_ENDPOINTS"]
+ if endpointsString == "" {
+ return nil, nil
+ }
+ var endpoints endpoints
+ urlsByCluster := make(map[string]string)
+ if err := json.Unmarshal([]byte(endpointsString), &endpoints); err != nil {
+ return nil, fmt.Errorf("endpoints must be valid json: %w", err)
+ }
+ if len(endpoints.Endpoints) == 0 {
+ return nil, fmt.Errorf("endpoints must be non-empty")
+ }
+ for _, endpoint := range endpoints.Endpoints {
+ urlsByCluster[endpoint.Cluster] = endpoint.URL
+ }
+ return urlsByCluster, nil
+}
+
+// Run executes the CLI with given args. If args is nil, it defaults to os.Args[1:].
+func (c *CLI) Run(args ...string) error {
+ c.cmd.SetArgs(args)
+ err := c.cmd.Execute()
if err != nil {
if cliErr, ok := err.(ErrCLI); ok {
if !cliErr.quiet {
- printErrHint(cliErr, cliErr.hints...)
+ c.printErrHint(cliErr, cliErr.hints...)
}
} else {
- printErrHint(err)
+ c.printErrHint(err)
}
}
return err
}
+
+type endpoints struct {
+ Endpoints []endpoint `json:"endpoints"`
+}
+
+type endpoint struct {
+ Cluster string `json:"cluster"`
+ URL string `json:"url"`
+}
+
+func isTerminal(w io.Writer) bool {
+ if f, ok := w.(*os.File); ok {
+ return isatty.IsTerminal(f.Fd())
+ }
+ return false
+}
+
+func athenzPath(filename string) (string, error) {
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(userHome, ".athenz", filename), nil
+}
+
+func athenzKeyPair() (KeyPair, error) {
+ certFile, err := athenzPath("cert")
+ if err != nil {
+ return KeyPair{}, err
+ }
+ keyFile, err := athenzPath("key")
+ if err != nil {
+ return KeyPair{}, err
+ }
+ kp, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ return KeyPair{}, err
+ }
+ cert, err := x509.ParseCertificate(kp.Certificate[0])
+ if err != nil {
+ return KeyPair{}, err
+ }
+ now := time.Now()
+ expiredAt := cert.NotAfter
+ if expiredAt.Before(now) {
+ delta := now.Sub(expiredAt).Truncate(time.Second)
+ return KeyPair{}, errHint(fmt.Errorf("certificate %s expired at %s (%s ago)", certFile, cert.NotAfter, delta), "Try renewing certificate with 'athenz-user-cert'")
+ }
+ return KeyPair{KeyPair: kp, CertificateFile: certFile, PrivateKeyFile: keyFile}, nil
+}
+
+func applicationSource(args []string) string {
+ if len(args) > 0 {
+ return args[0]
+ }
+ return "."
+}
diff --git a/client/go/cmd/status.go b/client/go/cmd/status.go
index 93316b7b6de..2796999957d 100644
--- a/client/go/cmd/status.go
+++ b/client/go/cmd/status.go
@@ -5,61 +5,93 @@
package cmd
import (
+ "fmt"
+ "log"
+ "strconv"
+ "time"
+
+ "github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/vespa"
)
-func init() {
- rootCmd.AddCommand(statusCmd)
- statusCmd.AddCommand(statusQueryCmd)
- statusCmd.AddCommand(statusDocumentCmd)
- statusCmd.AddCommand(statusDeployCmd)
+func newStatusCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "status",
+ Short: "Verify that a service is ready to use (query by default)",
+ Example: `$ vespa status query`,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.MaximumNArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return printServiceStatus(cli, vespa.QueryService)
+ },
+ }
}
-var statusCmd = &cobra.Command{
- Use: "status",
- Short: "Verify that a service is ready to use (query by default)",
- Example: `$ vespa status query`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.MaximumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- return waitForService(vespa.QueryService, 0)
- },
+func newStatusQueryCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "query",
+ Short: "Verify that the query service is ready to use (default)",
+ Example: `$ vespa status query`,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(0),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return printServiceStatus(cli, vespa.QueryService)
+ },
+ }
}
-var statusQueryCmd = &cobra.Command{
- Use: "query",
- Short: "Verify that the query service is ready to use (default)",
- Example: `$ vespa status query`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.ExactArgs(0),
- RunE: func(cmd *cobra.Command, args []string) error {
- return waitForService(vespa.QueryService, 0)
- },
+func newStatusDocumentCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "document",
+ Short: "Verify that the document service is ready to use",
+ Example: `$ vespa status document`,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(0),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return printServiceStatus(cli, vespa.DocumentService)
+ },
+ }
}
-var statusDocumentCmd = &cobra.Command{
- Use: "document",
- Short: "Verify that the document service is ready to use",
- Example: `$ vespa status document`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.ExactArgs(0),
- RunE: func(cmd *cobra.Command, args []string) error {
- return waitForService(vespa.DocumentService, 0)
- },
+func newStatusDeployCmd(cli *CLI) *cobra.Command {
+ return &cobra.Command{
+ Use: "deploy",
+ Short: "Verify that the deploy service is ready to use",
+ Example: `$ vespa status deploy`,
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(0),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return printServiceStatus(cli, vespa.DeployService)
+ },
+ }
}
-var statusDeployCmd = &cobra.Command{
- Use: "deploy",
- Short: "Verify that the deploy service is ready to use",
- Example: `$ vespa status deploy`,
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.ExactArgs(0),
- RunE: func(cmd *cobra.Command, args []string) error {
- return waitForService(vespa.DeployService, 0)
- },
+func printServiceStatus(cli *CLI, name string) error {
+ t, err := cli.target("", "")
+ if err != nil {
+ return err
+ }
+ timeout := time.Duration(cli.flags.waitSecs) * time.Second
+ if timeout > 0 {
+ log.Printf("Waiting up to %s %s for service to become ready ...", color.CyanString(strconv.Itoa(cli.flags.waitSecs)), color.CyanString("seconds"))
+ }
+ s, err := t.Service(name, timeout, 0, "")
+ if err != nil {
+ return err
+ }
+ status, err := s.Wait(timeout)
+ if status/100 == 2 {
+ log.Print(s.Description(), " at ", color.CyanString(s.BaseURL), " is ", color.GreenString("ready"))
+ } else {
+ if err == nil {
+ err = fmt.Errorf("status %d", status)
+ }
+ return fmt.Errorf("%s at %s is %s: %w", s.Description(), color.CyanString(s.BaseURL), color.RedString("not ready"), err)
+ }
+ return nil
}
diff --git a/client/go/cmd/status_test.go b/client/go/cmd/status_test.go
index fe7228697c7..7dc2aeace84 100644
--- a/client/go/cmd/status_test.go
+++ b/client/go/cmd/status_test.go
@@ -45,33 +45,47 @@ func TestStatusErrorResponse(t *testing.T) {
func assertDeployStatus(target string, args []string, t *testing.T) {
client := &mock.HTTPClient{}
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ statusArgs := []string{"status", "deploy"}
+ assert.Nil(t, cli.Run(append(statusArgs, args...)...))
assert.Equal(t,
"Deploy API at "+target+" is ready\n",
- executeCommand(t, client, []string{"status", "deploy"}, args),
+ stdout.String(),
"vespa status config-server")
assert.Equal(t, target+"/status.html", client.LastRequest.URL.String())
}
func assertQueryStatus(target string, args []string, t *testing.T) {
client := &mock.HTTPClient{}
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ statusArgs := []string{"status", "query"}
+ assert.Nil(t, cli.Run(append(statusArgs, args...)...))
assert.Equal(t,
"Container (query API) at "+target+" is ready\n",
- executeCommand(t, client, []string{"status", "query"}, args),
+ stdout.String(),
"vespa status container")
assert.Equal(t, target+"/ApplicationStatus", client.LastRequest.URL.String())
+ statusArgs = []string{"status"}
+ stdout.Reset()
+ assert.Nil(t, cli.Run(append(statusArgs, args...)...))
assert.Equal(t,
"Container (query API) at "+target+" is ready\n",
- executeCommand(t, client, []string{"status"}, args),
+ stdout.String(),
"vespa status (the default)")
assert.Equal(t, target+"/ApplicationStatus", client.LastRequest.URL.String())
}
func assertDocumentStatus(target string, args []string, t *testing.T) {
client := &mock.HTTPClient{}
+ cli, stdout, _ := newTestCLI(t)
+ cli.httpClient = client
+ assert.Nil(t, cli.Run("status", "document"))
assert.Equal(t,
"Container (document API) at "+target+" is ready\n",
- executeCommand(t, client, []string{"status", "document"}, args),
+ stdout.String(),
"vespa status container")
assert.Equal(t, target+"/ApplicationStatus", client.LastRequest.URL.String())
}
@@ -79,11 +93,11 @@ func assertDocumentStatus(target string, args []string, t *testing.T) {
func assertQueryStatusError(target string, args []string, t *testing.T) {
client := &mock.HTTPClient{}
client.NextStatus(500)
- cmd := []string{"status", "container"}
- cmd = append(cmd, args...)
- _, outErr := execute(command{args: cmd}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("status", "container"))
assert.Equal(t,
"Error: Container (query API) at "+target+" is not ready: status 500\n",
- outErr,
+ stderr.String(),
"vespa status container")
}
diff --git a/client/go/cmd/test.go b/client/go/cmd/test.go
index 56ea9277842..2eb5d6fc9b5 100644
--- a/client/go/cmd/test.go
+++ b/client/go/cmd/test.go
@@ -25,51 +25,51 @@ import (
"github.com/vespa-engine/vespa/client/go/vespa"
)
-func init() {
- rootCmd.AddCommand(testCmd)
- testCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "", "The zone to use for deployment. This defaults to a dev zone")
-}
-
-var testCmd = &cobra.Command{
- Use: "test test-directory-or-file",
- Short: "Run a test suite, or a single test",
- Long: `Run a test suite, or a single test
+func newTestCmd(cli *CLI) *cobra.Command {
+ var zoneArg string
+ testCmd := &cobra.Command{
+ Use: "test test-directory-or-file",
+ Short: "Run a test suite, or a single test",
+ Long: `Run a test suite, or a single test
Runs all JSON test files in the specified directory, or the single JSON test file specified.
See https://docs.vespa.ai/en/reference/testing.html for details.`,
- Example: `$ vespa test src/test/application/tests/system-test
+ Example: `$ vespa test src/test/application/tests/system-test
$ vespa test src/test/application/tests/system-test/feed-and-query.json`,
- Args: cobra.ExactArgs(1),
- DisableAutoGenTag: true,
- 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 = ""
- }
- fmt.Fprintf(stdout, "\n%s %d of %d test%s failed:\n", color.RedString("Failure:"), len(failed), count, plural)
- for _, test := range failed {
- fmt.Fprintln(stdout, test)
+ Args: cobra.ExactArgs(1),
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ count, failed, err := runTests(cli, zoneArg, args[0], false)
+ if err != nil {
+ return err
}
- return ErrCLI{Status: 3, error: fmt.Errorf("tests failed"), quiet: true}
- } else {
- plural := "s"
- if count == 1 {
- plural = ""
+ if len(failed) != 0 {
+ plural := "s"
+ if count == 1 {
+ plural = ""
+ }
+ fmt.Fprintf(cli.Stdout, "\n%s %d of %d test%s failed:\n", color.RedString("Failure:"), len(failed), count, plural)
+ for _, test := range failed {
+ fmt.Fprintln(cli.Stdout, test)
+ }
+ return ErrCLI{Status: 3, error: fmt.Errorf("tests failed"), quiet: true}
+ } else {
+ plural := "s"
+ if count == 1 {
+ plural = ""
+ }
+ fmt.Fprintf(cli.Stdout, "\n%s %d test%s OK\n", color.GreenString("Success:"), count, plural)
+ return nil
}
- fmt.Fprintf(stdout, "\n%s %d test%s OK\n", color.GreenString("Success:"), count, plural)
- return nil
- }
- },
+ },
+ }
+ testCmd.PersistentFlags().StringVarP(&zoneArg, "zone", "z", "", "The zone to use for deployment. This defaults to a dev zone")
+ return testCmd
}
-func runTests(rootPath string, dryRun bool) (int, []string, error) {
+func runTests(cli *CLI, zone, rootPath string, dryRun bool) (int, []string, error) {
count := 0
failed := make([]string, 0)
if stat, err := os.Stat(rootPath); err != nil {
@@ -79,13 +79,13 @@ func runTests(rootPath string, dryRun bool) (int, []string, error) {
if err != nil {
return 0, nil, errHint(err, "See https://docs.vespa.ai/en/reference/testing")
}
- context := testContext{testsPath: rootPath, dryRun: dryRun}
+ context := testContext{testsPath: rootPath, dryRun: dryRun, cli: cli, zone: zone}
previousFailed := false
for _, test := range tests {
if !test.IsDir() && filepath.Ext(test.Name()) == ".json" {
testPath := filepath.Join(rootPath, test.Name())
if previousFailed {
- fmt.Fprintln(stdout, "")
+ fmt.Fprintln(cli.Stdout, "")
previousFailed = false
}
failure, err := runTest(testPath, context)
@@ -100,7 +100,7 @@ func runTests(rootPath string, dryRun bool) (int, []string, error) {
}
}
} else if strings.HasSuffix(stat.Name(), ".json") {
- failure, err := runTest(rootPath, testContext{testsPath: filepath.Dir(rootPath), dryRun: dryRun})
+ failure, err := runTest(rootPath, testContext{testsPath: filepath.Dir(rootPath), dryRun: dryRun, cli: cli})
if err != nil {
return 0, nil, err
}
@@ -131,17 +131,17 @@ func runTest(testPath string, context testContext) (string, error) {
testName = filepath.Base(testPath)
}
if !context.dryRun {
- fmt.Fprintf(stdout, "%s:", testName)
+ fmt.Fprintf(context.cli.Stdout, "%s:", testName)
}
defaultParameters, err := getParameters(test.Defaults.ParametersRaw, filepath.Dir(testPath))
if err != nil {
- fmt.Fprintln(stderr)
+ fmt.Fprintln(context.cli.Stderr)
return "", errHint(fmt.Errorf("invalid default parameters for %s: %w", testName, err), "See https://docs.vespa.ai/en/reference/testing")
}
if len(test.Steps) == 0 {
- fmt.Fprintln(stderr)
+ fmt.Fprintln(context.cli.Stderr)
return "", errHint(fmt.Errorf("a test must have at least one step, but none were found in %s", testPath), "See https://docs.vespa.ai/en/reference/testing")
}
for i, step := range test.Steps {
@@ -151,22 +151,22 @@ func runTest(testPath string, context testContext) (string, error) {
}
failure, longFailure, err := verify(step, test.Defaults.Cluster, defaultParameters, context)
if err != nil {
- fmt.Fprintln(stderr)
+ fmt.Fprintln(context.cli.Stderr)
return "", errHint(fmt.Errorf("error in %s: %w", stepName, err), "See https://docs.vespa.ai/en/reference/testing")
}
if !context.dryRun {
if failure != "" {
- fmt.Fprintf(stdout, " %s\n%s:\n%s\n", color.RedString("failed"), stepName, longFailure)
+ fmt.Fprintf(context.cli.Stdout, " %s\n%s:\n%s\n", color.RedString("failed"), stepName, longFailure)
return fmt.Sprintf("%s: %s: %s", testName, stepName, failure), nil
}
if i == 0 {
- fmt.Fprintf(stdout, " ")
+ fmt.Fprintf(context.cli.Stdout, " ")
}
- fmt.Fprint(stdout, ".")
+ fmt.Fprint(context.cli.Stdout, ".")
}
}
if !context.dryRun {
- fmt.Fprintln(stdout, color.GreenString(" OK"))
+ fmt.Fprintln(context.cli.Stdout, color.GreenString(" OK"))
}
return "", nil
}
@@ -265,8 +265,8 @@ func verify(step step, defaultCluster string, defaultParameters map[string]strin
var response *http.Response
if externalEndpoint {
- util.ActiveHttpClient.UseCertificate([]tls.Certificate{})
- response, err = util.ActiveHttpClient.Do(request, 60*time.Second)
+ context.cli.httpClient.UseCertificate([]tls.Certificate{})
+ response, err = context.cli.httpClient.Do(request, 60*time.Second)
} else {
response, err = service.Do(request, 600*time.Second) // Vespa should provide a response within the given request timeout
}
@@ -384,8 +384,8 @@ func compare(expected interface{}, actual interface{}, path string) (string, str
expectedJson, _ := json.Marshal(expected)
actualJson, _ := json.Marshal(actual)
return fmt.Sprintf("Unexpected %s at %s", mismatched, color.CyanString(path)),
- fmt.Sprintf("%s", color.CyanString(string(expectedJson))),
- fmt.Sprintf("%s", color.RedString(string(actualJson))),
+ color.CyanString(string(expectedJson)),
+ color.RedString(string(actualJson)),
nil
}
return "", "", "", nil
@@ -470,6 +470,8 @@ type response struct {
}
type testContext struct {
+ cli *CLI
+ zone string
lazyTarget vespa.Target
testsPath string
dryRun bool
@@ -477,7 +479,7 @@ type testContext struct {
func (t *testContext) target() (vespa.Target, error) {
if t.lazyTarget == nil {
- target, err := getTarget()
+ target, err := t.cli.target(t.zone, "")
if err != nil {
return nil, err
}
diff --git a/client/go/cmd/test_test.go b/client/go/cmd/test_test.go
index 1f7d0cff7b2..c3538c7cb1a 100644
--- a/client/go/cmd/test_test.go
+++ b/client/go/cmd/test_test.go
@@ -8,7 +8,6 @@ import (
"io/ioutil"
"net/http"
"net/url"
- "os"
"path/filepath"
"strings"
"testing"
@@ -30,7 +29,9 @@ func TestSuite(t *testing.T) {
}
expectedBytes, _ := ioutil.ReadFile("testdata/tests/expected-suite.out")
- outBytes, errBytes := execute(command{args: []string{"test", "testdata/tests/system-test"}}, t, client)
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("test", "testdata/tests/system-test"))
baseUrl := "http://127.0.0.1:8080"
urlWithQuery := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
@@ -41,47 +42,53 @@ func TestSuite(t *testing.T) {
requests = append(requests, createSearchRequest(baseUrl+"/search/"))
}
assertRequests(requests, client, t)
- assert.Equal(t, string(expectedBytes), outBytes)
- assert.Equal(t, "", errBytes)
+ assert.Equal(t, string(expectedBytes), stdout.String())
+ assert.Equal(t, "", stderr.String())
}
func TestIllegalFileReference(t *testing.T) {
client := &mock.HTTPClient{}
client.NextStatus(200)
client.NextStatus(200)
- _, errBytes := execute(command{args: []string{"test", "testdata/tests/production-test/illegal-reference.json"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("test", "testdata/tests/production-test/illegal-reference.json"))
assertRequests([]*http.Request{createRequest("GET", "https://domain.tld", "{}")}, client, t)
- 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://docs.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://docs.vespa.ai/en/reference/testing\n", stderr.String())
}
func TestIllegalRequestUri(t *testing.T) {
client := &mock.HTTPClient{}
client.NextStatus(200)
client.NextStatus(200)
- _, errBytes := execute(command{args: []string{"test", "testdata/tests/production-test/illegal-uri.json"}}, t, client)
+ cli, _, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.NotNil(t, cli.Run("test", "testdata/tests/production-test/illegal-uri.json"))
assertRequests([]*http.Request{createRequest("GET", "https://domain.tld/my-api", "")}, client, t)
- assert.Equal(t, "\nError: error in Step 2: production tests may not specify requests against Vespa endpoints\nHint: See https://docs.vespa.ai/en/reference/testing\n", errBytes)
+ assert.Equal(t, "\nError: error in Step 2: production tests may not specify requests against Vespa endpoints\nHint: See https://docs.vespa.ai/en/reference/testing\n", stderr.String())
}
func TestProductionTest(t *testing.T) {
client := &mock.HTTPClient{}
client.NextStatus(200)
- outBytes, errBytes := execute(command{args: []string{"test", "testdata/tests/production-test/external.json"}}, t, client)
- assert.Equal(t, "external.json: . OK\n\nSuccess: 1 test OK\n", outBytes)
- assert.Equal(t, "", errBytes)
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = client
+ assert.Nil(t, cli.Run("test", "testdata/tests/production-test/external.json"))
+ assert.Equal(t, "external.json: . OK\n\nSuccess: 1 test OK\n", stdout.String())
+ assert.Equal(t, "", stderr.String())
assertRequests([]*http.Request{createRequest("GET", "https://my.service:123/path?query=wohoo", "")}, client, t)
}
func TestTestWithoutAssertions(t *testing.T) {
- client := &mock.HTTPClient{}
- _, errBytes := execute(command{args: []string{"test", "testdata/tests/system-test/foo/query.json"}}, t, client)
- assert.Equal(t, "\nError: a test must have at least one step, but none were found in testdata/tests/system-test/foo/query.json\nHint: See https://docs.vespa.ai/en/reference/testing\n", errBytes)
+ cli, _, stderr := newTestCLI(t)
+ assert.NotNil(t, cli.Run("test", "testdata/tests/system-test/foo/query.json"))
+ assert.Equal(t, "\nError: a test must have at least one step, but none were found in testdata/tests/system-test/foo/query.json\nHint: See https://docs.vespa.ai/en/reference/testing\n", stderr.String())
}
func TestSuiteWithoutTests(t *testing.T) {
- client := &mock.HTTPClient{}
- _, 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://docs.vespa.ai/en/reference/testing\n", errBytes)
+ cli, _, stderr := newTestCLI(t)
+ assert.NotNil(t, cli.Run("test", "testdata/tests/staging-test"))
+ assert.Equal(t, "Error: failed to find any tests at testdata/tests/staging-test\nHint: See https://docs.vespa.ai/en/reference/testing\n", stderr.String())
}
func TestSingleTest(t *testing.T) {
@@ -91,11 +98,13 @@ func TestSingleTest(t *testing.T) {
client.NextStatus(200)
client.NextResponse(200, string(searchResponse))
client.NextResponse(200, string(searchResponse))
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = client
expectedBytes, _ := ioutil.ReadFile("testdata/tests/expected.out")
- outBytes, errBytes := execute(command{args: []string{"test", "testdata/tests/system-test/test.json"}}, t, client)
- assert.Equal(t, string(expectedBytes), outBytes)
- assert.Equal(t, "", errBytes)
+ assert.Nil(t, cli.Run("test", "testdata/tests/system-test/test.json"))
+ assert.Equal(t, string(expectedBytes), stdout.String())
+ assert.Equal(t, "", stderr.String())
baseUrl := "http://127.0.0.1:8080"
rawUrl := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
@@ -105,34 +114,36 @@ func TestSingleTest(t *testing.T) {
func TestSingleTestWithCloudAndEndpoints(t *testing.T) {
apiKey, err := vespa.CreateAPIKey()
require.Nil(t, err)
- cmd := command{
- args: []string{"test", "testdata/tests/system-test/test.json", "-t", "cloud", "-a", "t.a.i"},
- env: map[string]string{"VESPA_CLI_API_KEY": string(apiKey)},
- }
- cmd.homeDir = filepath.Join(t.TempDir(), ".vespa")
- os.MkdirAll(cmd.homeDir, 0700)
- keyFile := filepath.Join(cmd.homeDir, "key")
- certFile := filepath.Join(cmd.homeDir, "cert")
-
- os.Setenv("VESPA_CLI_DATA_PLANE_KEY_FILE", keyFile)
- os.Setenv("VESPA_CLI_DATA_PLANE_CERT_FILE", certFile)
- os.Setenv("VESPA_CLI_ENDPOINTS", "{\"endpoints\":[{\"cluster\":\"container\",\"url\":\"https://url\"}]}")
-
- kp, _ := vespa.CreateKeyPair()
- ioutil.WriteFile(keyFile, kp.PrivateKey, 0600)
- ioutil.WriteFile(certFile, kp.Certificate, 0600)
+ certDir := filepath.Join(t.TempDir())
+ keyFile := filepath.Join(certDir, "key")
+ certFile := filepath.Join(certDir, "cert")
+ kp, err := vespa.CreateKeyPair()
+ require.Nil(t, err)
+ require.Nil(t, ioutil.WriteFile(keyFile, kp.PrivateKey, 0600))
+ require.Nil(t, ioutil.WriteFile(certFile, kp.Certificate, 0600))
client := &mock.HTTPClient{}
- searchResponse, _ := ioutil.ReadFile("testdata/tests/response.json")
+ cli, stdout, stderr := newTestCLI(
+ t,
+ "VESPA_CLI_API_KEY="+string(apiKey),
+ "VESPA_CLI_DATA_PLANE_KEY_FILE="+keyFile,
+ "VESPA_CLI_DATA_PLANE_CERT_FILE="+certFile,
+ "VESPA_CLI_ENDPOINTS={\"endpoints\":[{\"cluster\":\"container\",\"url\":\"https://url\"}]}",
+ )
+ cli.httpClient = client
+
+ searchResponse, err := ioutil.ReadFile("testdata/tests/response.json")
+ require.Nil(t, err)
client.NextStatus(200)
client.NextStatus(200)
client.NextResponse(200, string(searchResponse))
client.NextResponse(200, string(searchResponse))
- expectedBytes, _ := ioutil.ReadFile("testdata/tests/expected.out")
- outBytes, errBytes := execute(cmd, t, client)
- assert.Equal(t, string(expectedBytes), outBytes)
- assert.Equal(t, "", errBytes)
+ assert.Nil(t, cli.Run("test", "testdata/tests/system-test/test.json", "-t", "cloud", "-a", "t.a.i"))
+ expectedBytes, err := ioutil.ReadFile("testdata/tests/expected.out")
+ require.Nil(t, err)
+ assert.Equal(t, "", stderr.String())
+ assert.Equal(t, string(expectedBytes), stdout.String())
baseUrl := "https://url"
rawUrl := baseUrl + "/search/?presentation.timing=true&query=artist%3A+foo&timeout=3.4s"
diff --git a/client/go/cmd/testutil_test.go b/client/go/cmd/testutil_test.go
new file mode 100644
index 00000000000..68f79187d3a
--- /dev/null
+++ b/client/go/cmd/testutil_test.go
@@ -0,0 +1,49 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package cmd
+
+import (
+ "bytes"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/vespa-engine/vespa/client/go/mock"
+)
+
+func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Buffer) {
+ t.Cleanup(viper.Reset)
+ homeDir := filepath.Join(t.TempDir(), ".vespa")
+ cacheDir := filepath.Join(t.TempDir(), ".cache", "vespa")
+ env := []string{"VESPA_CLI_HOME=" + homeDir, "VESPA_CLI_CACHE_DIR=" + cacheDir}
+ env = append(env, envVars...)
+ var (
+ stdout bytes.Buffer
+ stderr bytes.Buffer
+ )
+ cli, err := New(&stdout, &stderr, env)
+ if err != nil {
+ t.Fatal(err)
+ }
+ cli.httpClient = &mock.HTTPClient{}
+ cli.exec = &mock.Exec{}
+ return cli, &stdout, &stderr
+}
+
+func mockApplicationPackage(t *testing.T, java bool) string {
+ dir := t.TempDir()
+ appDir := filepath.Join(dir, "src", "main", "application")
+ if err := os.MkdirAll(appDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ servicesXML := filepath.Join(appDir, "services.xml")
+ if _, err := os.Create(servicesXML); err != nil {
+ t.Fatal(err)
+ }
+ if java {
+ if _, err := os.Create(filepath.Join(dir, "pom.xml")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return dir
+}
diff --git a/client/go/cmd/version.go b/client/go/cmd/version.go
index 875fc0cc3c1..0a47039a37d 100644
--- a/client/go/cmd/version.go
+++ b/client/go/cmd/version.go
@@ -6,7 +6,6 @@ import (
"log"
"net/http"
"os"
- "os/exec"
"path/filepath"
"runtime"
"sort"
@@ -16,61 +15,42 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/build"
- "github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
)
-var skipVersionCheck bool
-
-var sp subprocess = &execSubprocess{}
-
-type subprocess interface {
- pathOf(name string) (string, error)
- outputOf(name string, args ...string) ([]byte, error)
- isTerminal() bool
-}
-
-type execSubprocess struct{}
-
-func (c *execSubprocess) pathOf(name string) (string, error) { return exec.LookPath(name) }
-func (c *execSubprocess) isTerminal() bool { return isTerminal() }
-func (c *execSubprocess) outputOf(name string, args ...string) ([]byte, error) {
- return exec.Command(name, args...).Output()
-}
-
-func init() {
- rootCmd.AddCommand(versionCmd)
- versionCmd.Flags().BoolVarP(&skipVersionCheck, "no-check", "n", false, "Do not check if a new version is available")
-}
-
-var versionCmd = &cobra.Command{
- Use: "version",
- Short: "Show current version and check for updates",
- DisableAutoGenTag: true,
- SilenceUsage: true,
- Args: cobra.ExactArgs(0),
- 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() {
- return checkVersion()
- }
- return nil
- },
+func newVersionCmd(cli *CLI) *cobra.Command {
+ var skipVersionCheck bool
+ cmd := &cobra.Command{
+ Use: "version",
+ Short: "Show current version and check for updates",
+ DisableAutoGenTag: true,
+ SilenceUsage: true,
+ Args: cobra.ExactArgs(0),
+ 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 && cli.isTerminal() {
+ return checkVersion(cli)
+ }
+ return nil
+ },
+ }
+ cmd.Flags().BoolVarP(&skipVersionCheck, "no-check", "n", false, "Do not check if a new version is available")
+ return cmd
}
-func checkVersion() error {
+func checkVersion(cli *CLI) error {
current, err := version.Parse(build.Version)
if err != nil {
return err
}
- latest, err := latestRelease()
+ latest, err := latestRelease(cli)
if err != nil {
return err
}
if !current.Less(latest.Version) {
return nil
}
- usingHomebrew := usingHomebrew()
+ usingHomebrew := usingHomebrew(cli)
if usingHomebrew && latest.isRecent() {
return nil // Allow some time for new release to appear in Homebrew repo
}
@@ -82,12 +62,12 @@ func checkVersion() error {
return nil
}
-func latestRelease() (release, error) {
+func latestRelease(cli *CLI) (release, error) {
req, err := http.NewRequest("GET", "https://api.github.com/repos/vespa-engine/vespa/releases", nil)
if err != nil {
return release{}, err
}
- response, err := util.HttpDo(req, time.Minute, "GitHub")
+ response, err := cli.httpClient.Do(req, time.Minute)
if err != nil {
return release{}, err
}
@@ -118,12 +98,12 @@ func latestRelease() (release, error) {
return releases[len(releases)-1], nil
}
-func usingHomebrew() bool {
- selfPath, err := sp.pathOf("vespa")
+func usingHomebrew(cli *CLI) bool {
+ selfPath, err := cli.exec.LookPath("vespa")
if err != nil {
return false
}
- brewPrefix, err := sp.outputOf("brew", "--prefix")
+ brewPrefix, err := cli.exec.Run("brew", "--prefix")
if err != nil {
return false
}
diff --git a/client/go/cmd/version_test.go b/client/go/cmd/version_test.go
index 9c05b130e84..b78c66c9e7f 100644
--- a/client/go/cmd/version_test.go
+++ b/client/go/cmd/version_test.go
@@ -2,52 +2,44 @@
package cmd
import (
- "fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vespa-engine/vespa/client/go/mock"
- "github.com/vespa-engine/vespa/client/go/util"
)
func TestVersion(t *testing.T) {
c := &mock.HTTPClient{}
c.NextResponse(200, `[{"tag_name": "v1.2.3", "published_at": "2021-09-10T12:00:00Z"}]`)
- util.ActiveHttpClient = c
- sp = &mockSubprocess{}
- out, _ := execute(command{args: []string{"version"}}, t, nil)
- assert.Contains(t, out, "vespa version 0.0.0-devel compiled with")
- assert.Contains(t, out, "New release available: 1.2.3\nhttps://github.com/vespa-engine/vespa/releases/tag/v1.2.3")
+ sp := &mock.Exec{}
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = c
+ cli.exec = sp
+ cli.isTerminal = func() bool { return true }
+ if err := cli.Run("version", "--color", "never"); err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, "", stderr.String())
+ assert.Contains(t, stdout.String(), "vespa version 0.0.0-devel compiled with")
+ assert.Contains(t, stdout.String(), "New release available: 1.2.3\nhttps://github.com/vespa-engine/vespa/releases/tag/v1.2.3")
}
func TestVersionCheckHomebrew(t *testing.T) {
c := &mock.HTTPClient{}
c.NextResponse(200, `[{"tag_name": "v1.2.3", "published_at": "2021-09-10T12:00:00Z"}]`)
- util.ActiveHttpClient = c
- sp = &mockSubprocess{programPath: "/usr/local/bin/vespa", output: "/usr/local"}
- out, _ := execute(command{args: []string{"version"}}, t, nil)
- assert.Contains(t, out, "vespa version 0.0.0-devel compiled with")
- assert.Contains(t, out, "New release available: 1.2.3\n"+
+ sp := &mock.Exec{ProgramPath: "/usr/local/bin/vespa", CombinedOutput: "/usr/local"}
+ cli, stdout, stderr := newTestCLI(t)
+ cli.httpClient = c
+ cli.exec = sp
+ cli.isTerminal = func() bool { return true }
+ if err := cli.Run("version", "--color", "never"); err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, "", stderr.String())
+ assert.Contains(t, stdout.String(), "vespa version 0.0.0-devel compiled with")
+ assert.Contains(t, stdout.String(), "New release available: 1.2.3\n"+
"https://github.com/vespa-engine/vespa/releases/tag/v1.2.3\n"+
"\nUpgrade by running:\nbrew update && brew upgrade vespa-cli\n")
}
-
-type mockSubprocess struct {
- programPath string
- output string
-}
-
-func (c *mockSubprocess) pathOf(name string) (string, error) {
- if c.programPath == "" {
- return "", fmt.Errorf("no program path set in this mock")
- }
- return c.programPath, nil
-}
-
-func (c *mockSubprocess) outputOf(name string, args ...string) ([]byte, error) {
- return []byte(c.output), nil
-}
-
-func (c *mockSubprocess) isTerminal() bool { return true }
diff --git a/client/go/cmd/vespa/main.go b/client/go/cmd/vespa/main.go
index f7ce064f3a5..5e49f633877 100644
--- a/client/go/cmd/vespa/main.go
+++ b/client/go/cmd/vespa/main.go
@@ -5,17 +5,29 @@
package main
import (
+ "fmt"
"os"
"github.com/vespa-engine/vespa/client/go/cmd"
)
+func fatal(status int, err error) {
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ }
+ os.Exit(status)
+}
+
func main() {
- if err := cmd.Execute(); err != nil {
+ cli, err := cmd.New(os.Stdout, os.Stderr, os.Environ())
+ if err != nil {
+ fatal(1, err)
+ }
+ if err := cli.Run(); err != nil {
if cliErr, ok := err.(cmd.ErrCLI); ok {
- os.Exit(cliErr.Status)
+ fatal(cliErr.Status, nil)
} else {
- os.Exit(1)
+ fatal(1, nil)
}
}
}
diff --git a/client/go/mock/mock.go b/client/go/mock/http.go
index f2fcf9c5960..f2fcf9c5960 100644
--- a/client/go/mock/mock.go
+++ b/client/go/mock/http.go
diff --git a/client/go/mock/process.go b/client/go/mock/process.go
new file mode 100644
index 00000000000..5105bed8b0c
--- /dev/null
+++ b/client/go/mock/process.go
@@ -0,0 +1,19 @@
+package mock
+
+import "fmt"
+
+type Exec struct {
+ ProgramPath string
+ CombinedOutput string
+}
+
+func (c *Exec) LookPath(name string) (string, error) {
+ if c.ProgramPath == "" {
+ return "", fmt.Errorf("no program path set in this mock")
+ }
+ return c.ProgramPath, nil
+}
+
+func (c *Exec) Run(name string, args ...string) ([]byte, error) {
+ return []byte(c.CombinedOutput), nil
+}
diff --git a/client/go/util/http.go b/client/go/util/http.go
index bb70c3ec6db..ffdd3d1599b 100644
--- a/client/go/util/http.go
+++ b/client/go/util/http.go
@@ -1,7 +1,4 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-// A HTTP wrapper which handles some errors and provides a way to replace the HTTP client by a mock.
-// Author: bratseth
-
package util
import (
@@ -13,45 +10,32 @@ import (
"github.com/vespa-engine/vespa/client/go/build"
)
-// Set this to a mock HttpClient instead to unit test HTTP requests
-var ActiveHttpClient = CreateClient(time.Second * 10)
-
-type HttpClient interface {
+type HTTPClient interface {
Do(request *http.Request, timeout time.Duration) (response *http.Response, error error)
UseCertificate(certificate []tls.Certificate)
}
-type defaultHttpClient struct {
+type defaultHTTPClient struct {
client *http.Client
}
-func (c *defaultHttpClient) Do(request *http.Request, timeout time.Duration) (response *http.Response, error error) {
+func (c *defaultHTTPClient) Do(request *http.Request, timeout time.Duration) (response *http.Response, error error) {
if c.client.Timeout != timeout { // Set wanted timeout
c.client.Timeout = timeout
}
+ if request.Header == nil {
+ request.Header = make(http.Header)
+ }
+ request.Header.Set("User-Agent", fmt.Sprintf("Vespa CLI/%s", build.Version))
return c.client.Do(request)
}
-func (c *defaultHttpClient) UseCertificate(certificates []tls.Certificate) {
+func (c *defaultHTTPClient) UseCertificate(certificates []tls.Certificate) {
c.client.Transport = &http.Transport{TLSClientConfig: &tls.Config{
Certificates: certificates,
}}
}
-func CreateClient(timeout time.Duration) HttpClient {
- return &defaultHttpClient{
- client: &http.Client{Timeout: timeout},
- }
-}
-
-func HttpDo(request *http.Request, timeout time.Duration, description string) (*http.Response, error) {
- if request.Header == nil {
- request.Header = make(http.Header)
- }
- request.Header.Set("User-Agent", fmt.Sprintf("Vespa CLI/%s", build.Version))
- response, err := ActiveHttpClient.Do(request, timeout)
- if err != nil {
- return nil, err
- }
- return response, nil
+func CreateClient(timeout time.Duration) HTTPClient {
+ return &defaultHTTPClient{client: &http.Client{Timeout: timeout}}
}
diff --git a/client/go/util/http_test.go b/client/go/util/http_test.go
deleted file mode 100644
index ccb809d198b..00000000000
--- a/client/go/util/http_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-// Basic testing of our HTTP client wrapper
-// Author: bratseth
-
-package util
-
-import (
- "bytes"
- "crypto/tls"
- "io/ioutil"
- "net/http"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
-)
-
-type mockHttpClient struct{}
-
-func (c mockHttpClient) Do(request *http.Request, timeout time.Duration) (response *http.Response, error error) {
- var status int
- var body string
- if request.URL.String() == "http://host/okpath" {
- status = 200
- body = "OK body"
- } else {
- status = 500
- body = "Unexpected url body"
- }
-
- return &http.Response{
- StatusCode: status,
- Header: make(http.Header),
- Body: ioutil.NopCloser(bytes.NewBufferString(body)),
- },
- nil
-}
-
-func (c mockHttpClient) UseCertificate(certificates []tls.Certificate) {}
-
-func TestHttpRequest(t *testing.T) {
- ActiveHttpClient = mockHttpClient{}
-
- req, err := http.NewRequest("GET", "http://host/okpath", nil)
- assert.Nil(t, err)
- response, err := HttpDo(req, time.Second*10, "description")
- assert.Nil(t, err)
- assert.Equal(t, 200, response.StatusCode)
-
- req, err = http.NewRequest("GET", "http://host/otherpath", nil)
- assert.Nil(t, err)
- response, err = HttpDo(req, time.Second*10, "description")
- assert.Nil(t, err)
- assert.Equal(t, 500, response.StatusCode)
-}
diff --git a/client/go/version/version.go b/client/go/version/version.go
index b27529fa5e1..557007b18fc 100644
--- a/client/go/version/version.go
+++ b/client/go/version/version.go
@@ -68,6 +68,15 @@ func (v1 Version) Compare(v2 Version) int {
// Less returns true if v1 is lower than v2.
func (v1 Version) Less(v2 Version) bool { return v1.Compare(v2) < 0 }
+// MustParse is like Parse, but panics if s cannot be parsed.
+func MustParse(s string) Version {
+ v, err := Parse(s)
+ if err != nil {
+ panic(err)
+ }
+ return v
+}
+
// Parse parses a semantic version number from string s.
func Parse(s string) (Version, error) {
if len(s) > 0 && s[0] == 'v' {
diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go
index 7c30390763f..59ea8d22d62 100644
--- a/client/go/vespa/deploy.go
+++ b/client/go/vespa/deploy.go
@@ -43,6 +43,7 @@ type DeploymentOptions struct {
Target Target
ApplicationPackage ApplicationPackage
Timeout time.Duration
+ HTTPClient util.HTTPClient
}
type LogLinePrepareResponse struct {
@@ -124,7 +125,7 @@ func Prepare(deployment DeploymentOptions) (PrepareResult, error) {
return PrepareResult{}, err
}
serviceDescription := "Deploy service"
- response, err := util.HttpDo(req, time.Second*30, serviceDescription)
+ response, err := deployment.HTTPClient.Do(req, time.Second*30)
if err != nil {
return PrepareResult{}, err
}
@@ -149,7 +150,7 @@ func Activate(sessionID int64, deployment DeploymentOptions) error {
return err
}
serviceDescription := "Deploy service"
- response, err := util.HttpDo(req, time.Second*30, serviceDescription)
+ response, err := deployment.HTTPClient.Do(req, time.Second*30)
if err != nil {
return err
}
@@ -243,7 +244,7 @@ func Submit(opts DeploymentOptions) error {
if err := opts.Target.SignRequest(request, sigKeyId); err != nil {
return fmt.Errorf("failed to sign api request: %w", err)
}
- response, err := util.HttpDo(request, time.Minute*10, sigKeyId)
+ response, err := opts.HTTPClient.Do(request, time.Minute*10)
if err != nil {
return err
}
diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go
index 8d48f1ffa3a..94028fb235f 100644
--- a/client/go/vespa/target.go
+++ b/client/go/vespa/target.go
@@ -45,6 +45,7 @@ type Service struct {
Name string
TLSOptions TLSOptions
ztsClient ztsClient
+ httpClient util.HTTPClient
}
// Target represents a Vespa platform, running named Vespa services.
@@ -89,7 +90,7 @@ type LogOptions struct {
// Do sends request to this service. Any required authentication happens automatically.
func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Response, error) {
if s.TLSOptions.KeyPair.Certificate != nil {
- util.ActiveHttpClient.UseCertificate([]tls.Certificate{s.TLSOptions.KeyPair})
+ s.httpClient.UseCertificate([]tls.Certificate{s.TLSOptions.KeyPair})
}
if s.TLSOptions.AthenzDomain != "" {
accessToken, err := s.ztsClient.AccessToken(s.TLSOptions.AthenzDomain, s.TLSOptions.KeyPair)
@@ -101,7 +102,7 @@ func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Respon
}
request.Header.Add("Authorization", "Bearer "+accessToken)
}
- return util.HttpDo(request, timeout, s.Description())
+ return s.httpClient.Do(request, timeout)
}
// Wait polls the health check of this service until it succeeds or timeout passes.
@@ -115,7 +116,7 @@ func (s *Service) Wait(timeout time.Duration) (int, error) {
default:
return 0, fmt.Errorf("invalid service: %s", s.Name)
}
- return waitForOK(url, &s.TLSOptions.KeyPair, timeout)
+ return waitForOK(s.httpClient, url, &s.TLSOptions.KeyPair, timeout)
}
func (s *Service) Description() string {
@@ -138,18 +139,18 @@ type requestFunc func() *http.Request
// waitForOK queries url and returns its status code. If the url returns a non-200 status code, it is repeatedly queried
// until timeout elapses.
-func waitForOK(url string, certificate *tls.Certificate, timeout time.Duration) (int, error) {
+func waitForOK(client util.HTTPClient, url string, certificate *tls.Certificate, timeout time.Duration) (int, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return 0, err
}
okFunc := func(status int, response []byte) (bool, error) { return isOK(status), nil }
- return wait(okFunc, func() *http.Request { return req }, certificate, timeout)
+ return wait(client, okFunc, func() *http.Request { return req }, certificate, timeout)
}
-func wait(fn responseFunc, reqFn requestFunc, certificate *tls.Certificate, timeout time.Duration) (int, error) {
+func wait(client util.HTTPClient, fn responseFunc, reqFn requestFunc, certificate *tls.Certificate, timeout time.Duration) (int, error) {
if certificate != nil {
- util.ActiveHttpClient.UseCertificate([]tls.Certificate{*certificate})
+ client.UseCertificate([]tls.Certificate{*certificate})
}
var (
httpErr error
@@ -160,7 +161,7 @@ func wait(fn responseFunc, reqFn requestFunc, certificate *tls.Certificate, time
loopOnce := timeout == 0
for time.Now().Before(deadline) || loopOnce {
req := reqFn()
- response, httpErr = util.HttpDo(req, 10*time.Second, "")
+ response, httpErr = client.Do(req, 10*time.Second)
if httpErr == nil {
statusCode = response.StatusCode
body, err := ioutil.ReadAll(response.Body)
diff --git a/client/go/vespa/target_cloud.go b/client/go/vespa/target_cloud.go
index f4eccaacab6..697b0f23ba1 100644
--- a/client/go/vespa/target_cloud.go
+++ b/client/go/vespa/target_cloud.go
@@ -11,10 +11,10 @@ import (
"strconv"
"time"
- "github.com/vespa-engine/vespa/client/go/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/auth0"
+ "github.com/vespa-engine/vespa/client/go/auth/zts"
"github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
- "github.com/vespa-engine/vespa/client/go/zts"
)
// CloudOptions configures URL and authentication for a cloud target.
@@ -36,6 +36,7 @@ type cloudTarget struct {
apiOptions APIOptions
deploymentOptions CloudDeploymentOptions
logOptions LogOptions
+ httpClient util.HTTPClient
ztsClient ztsClient
}
@@ -67,12 +68,13 @@ type ztsClient interface {
}
// CloudTarget creates a Target for the Vespa Cloud or hosted Vespa platform.
-func CloudTarget(apiOptions APIOptions, deploymentOptions CloudDeploymentOptions, logOptions LogOptions) (Target, error) {
- ztsClient, err := zts.NewClient(zts.DefaultURL, util.ActiveHttpClient)
+func CloudTarget(httpClient util.HTTPClient, apiOptions APIOptions, deploymentOptions CloudDeploymentOptions, logOptions LogOptions) (Target, error) {
+ ztsClient, err := zts.NewClient(zts.DefaultURL, httpClient)
if err != nil {
return nil, err
}
return &cloudTarget{
+ httpClient: httpClient,
apiOptions: apiOptions,
deploymentOptions: deploymentOptions,
logOptions: logOptions,
@@ -117,7 +119,13 @@ func (t *cloudTarget) Deployment() Deployment { return t.deploymentOptions.Deplo
func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64, cluster string) (*Service, error) {
switch name {
case DeployService:
- service := &Service{Name: name, BaseURL: t.apiOptions.System.URL, TLSOptions: t.apiOptions.TLSOptions, ztsClient: t.ztsClient}
+ service := &Service{
+ Name: name,
+ BaseURL: t.apiOptions.System.URL,
+ TLSOptions: t.apiOptions.TLSOptions,
+ ztsClient: t.ztsClient,
+ httpClient: t.httpClient,
+ }
if timeout > 0 {
status, err := service.Wait(timeout)
if err != nil {
@@ -139,7 +147,13 @@ func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64, c
return nil, err
}
t.deploymentOptions.TLSOptions.AthenzDomain = t.apiOptions.System.AthenzDomain
- return &Service{Name: name, BaseURL: url, TLSOptions: t.deploymentOptions.TLSOptions, ztsClient: t.ztsClient}, nil
+ return &Service{
+ Name: name,
+ BaseURL: url,
+ TLSOptions: t.deploymentOptions.TLSOptions,
+ ztsClient: t.ztsClient,
+ httpClient: t.httpClient,
+ }, nil
}
return nil, fmt.Errorf("unknown service: %s", name)
}
@@ -168,7 +182,7 @@ func (t *cloudTarget) CheckVersion(clientVersion version.Version) error {
if err != nil {
return err
}
- response, err := util.HttpDo(req, 10*time.Second, "")
+ response, err := t.httpClient.Do(req, 10*time.Second)
if err != nil {
return err
}
@@ -254,7 +268,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
if options.Follow {
timeout = math.MaxInt64 // No timeout
}
- _, err = wait(logFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
+ _, err = wait(t.httpClient, logFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
return err
}
@@ -305,7 +319,7 @@ func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error {
}
return true, nil
}
- _, err = wait(jobSuccessFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
+ _, err = wait(t.httpClient, jobSuccessFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
return err
}
@@ -363,7 +377,7 @@ func (t *cloudTarget) discoverEndpoints(timeout time.Duration) error {
}
return true, nil
}
- if _, err = wait(endpointFunc, func() *http.Request { return req }, &t.apiOptions.TLSOptions.KeyPair, timeout); err != nil {
+ if _, err = wait(t.httpClient, endpointFunc, func() *http.Request { return req }, &t.apiOptions.TLSOptions.KeyPair, timeout); err != nil {
return err
}
if len(urlsByCluster) == 0 {
diff --git a/client/go/vespa/target_custom.go b/client/go/vespa/target_custom.go
index 072ec8649e4..bc25f19bf1a 100644
--- a/client/go/vespa/target_custom.go
+++ b/client/go/vespa/target_custom.go
@@ -7,12 +7,14 @@ import (
"net/url"
"time"
+ "github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
)
type customTarget struct {
targetType string
baseURL string
+ httpClient util.HTTPClient
}
type serviceConvergeResponse struct {
@@ -20,13 +22,13 @@ type serviceConvergeResponse struct {
}
// LocalTarget creates a target for a Vespa platform running locally.
-func LocalTarget() Target {
- return &customTarget{targetType: TargetLocal, baseURL: "http://127.0.0.1"}
+func LocalTarget(httpClient util.HTTPClient) Target {
+ return &customTarget{targetType: TargetLocal, baseURL: "http://127.0.0.1", httpClient: httpClient}
}
// CustomTarget creates a Target for a Vespa platform running at baseURL.
-func CustomTarget(baseURL string) Target {
- return &customTarget{targetType: TargetCustom, baseURL: baseURL}
+func CustomTarget(httpClient util.HTTPClient, baseURL string) Target {
+ return &customTarget{targetType: TargetCustom, baseURL: baseURL, httpClient: httpClient}
}
func (t *customTarget) Type() string { return t.targetType }
@@ -40,7 +42,7 @@ func (t *customTarget) createService(name string) (*Service, error) {
if err != nil {
return nil, err
}
- return &Service{BaseURL: url, Name: name}, nil
+ return &Service{BaseURL: url, Name: name, httpClient: t.httpClient}, nil
}
return nil, fmt.Errorf("unknown service: %s", name)
}
@@ -118,7 +120,7 @@ func (t *customTarget) waitForConvergence(timeout time.Duration) error {
converged = resp.Converged
return converged, nil
}
- if _, err := wait(convergedFunc, func() *http.Request { return req }, nil, timeout); err != nil {
+ if _, err := wait(t.httpClient, convergedFunc, func() *http.Request { return req }, nil, timeout); err != nil {
return err
}
if !converged {
diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go
index bf3e0fae7d0..ca4ed86162c 100644
--- a/client/go/vespa/target_test.go
+++ b/client/go/vespa/target_test.go
@@ -13,6 +13,8 @@ import (
"time"
"github.com/stretchr/testify/assert"
+ "github.com/vespa-engine/vespa/client/go/mock"
+ "github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
)
@@ -64,17 +66,17 @@ func (v *mockVespaApi) mockVespaHandler(w http.ResponseWriter, req *http.Request
}
func TestCustomTarget(t *testing.T) {
- lt := LocalTarget()
+ lt := LocalTarget(&mock.HTTPClient{})
assertServiceURL(t, "http://127.0.0.1:19071", lt, "deploy")
assertServiceURL(t, "http://127.0.0.1:8080", lt, "query")
assertServiceURL(t, "http://127.0.0.1:8080", lt, "document")
- ct := CustomTarget("http://192.0.2.42")
+ ct := CustomTarget(&mock.HTTPClient{}, "http://192.0.2.42")
assertServiceURL(t, "http://192.0.2.42:19071", ct, "deploy")
assertServiceURL(t, "http://192.0.2.42:8080", ct, "query")
assertServiceURL(t, "http://192.0.2.42:8080", ct, "document")
- ct2 := CustomTarget("http://192.0.2.42:60000")
+ ct2 := CustomTarget(&mock.HTTPClient{}, "http://192.0.2.42:60000")
assertServiceURL(t, "http://192.0.2.42:60000", ct2, "deploy")
assertServiceURL(t, "http://192.0.2.42:60000", ct2, "query")
assertServiceURL(t, "http://192.0.2.42:60000", ct2, "document")
@@ -84,7 +86,7 @@ func TestCustomTargetWait(t *testing.T) {
vc := mockVespaApi{}
srv := httptest.NewServer(http.HandlerFunc(vc.mockVespaHandler))
defer srv.Close()
- target := CustomTarget(srv.URL)
+ target := CustomTarget(util.CreateClient(time.Second*10), srv.URL)
_, err := target.Service("query", time.Millisecond, 42, "")
assert.NotNil(t, err)
@@ -147,17 +149,9 @@ func TestCheckVersion(t *testing.T) {
defer srv.Close()
target := createCloudTarget(t, srv.URL, ioutil.Discard)
- assert.Nil(t, target.CheckVersion(mustVersion("8.0.0")))
- assert.Nil(t, target.CheckVersion(mustVersion("8.1.0")))
- assert.NotNil(t, target.CheckVersion(mustVersion("7.0.0")))
-}
-
-func mustVersion(s string) version.Version {
- v, err := version.Parse(s)
- if err != nil {
- panic(err)
- }
- return v
+ assert.Nil(t, target.CheckVersion(version.MustParse("8.0.0")))
+ assert.Nil(t, target.CheckVersion(version.MustParse("8.1.0")))
+ assert.NotNil(t, target.CheckVersion(version.MustParse("7.0.0")))
}
func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target {
@@ -170,6 +164,7 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target {
assert.Nil(t, err)
target, err := CloudTarget(
+ util.CreateClient(time.Second*10),
APIOptions{APIKey: apiKey, System: PublicSystem},
CloudDeploymentOptions{
Deployment: Deployment{