aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2023-06-19 15:37:10 +0200
committerGitHub <noreply@github.com>2023-06-19 15:37:10 +0200
commitb62474b0429892b9bb6b4ca55da020caf73745df (patch)
treee48c08f2656f057609a1799e697063b5c9613ff9
parent23ce1275df759542e6557cc72836b1ddba348822 (diff)
parent4ebfe34853abc55c3ce6b425107c08b9994554ad (diff)
Merge pull request #27480 from vespa-engine/mpolden/deploy-cert
Add certificate via vespa deploy
-rw-r--r--client/go/internal/cli/cmd/cert.go132
-rw-r--r--client/go/internal/cli/cmd/cert_test.go65
-rw-r--r--client/go/internal/cli/cmd/deploy.go14
-rw-r--r--client/go/internal/cli/cmd/deploy_test.go50
-rw-r--r--client/go/internal/cli/cmd/document.go4
-rw-r--r--client/go/internal/cli/cmd/feed.go4
-rw-r--r--client/go/internal/cli/cmd/login.go25
-rw-r--r--client/go/internal/cli/cmd/prod.go8
-rw-r--r--client/go/internal/cli/cmd/prod_test.go27
-rw-r--r--client/go/internal/cli/cmd/root.go23
-rw-r--r--client/go/internal/cli/cmd/visit.go6
-rw-r--r--client/go/internal/vespa/deploy.go6
-rw-r--r--client/go/internal/vespa/target_custom.go2
13 files changed, 201 insertions, 165 deletions
diff --git a/client/go/internal/cli/cmd/cert.go b/client/go/internal/cli/cmd/cert.go
index 95206b7e77d..1fa5339e42e 100644
--- a/client/go/internal/cli/cmd/cert.go
+++ b/client/go/internal/cli/cmd/cert.go
@@ -4,9 +4,7 @@
package cmd
import (
- "errors"
"fmt"
- "io"
"os"
"path/filepath"
@@ -18,8 +16,8 @@ import (
func newCertCmd(cli *CLI) *cobra.Command {
var (
- noApplicationPackage bool
- overwriteCertificate bool
+ skipApplicationPackage bool
+ overwriteCertificate bool
)
cmd := &cobra.Command{
Use: "cert",
@@ -60,11 +58,12 @@ $ vespa auth cert -a my-tenant.my-app.my-instance path/to/application/package`,
SilenceUsage: true,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
- return doCert(cli, overwriteCertificate, noApplicationPackage, args)
+ return doCert(cli, overwriteCertificate, skipApplicationPackage, args)
},
}
cmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key")
- cmd.Flags().BoolVarP(&noApplicationPackage, "no-add", "N", false, "Do not add certificate to the application package")
+ // TODO(mpolden): Stop adding certificate to application package and remove this flag
+ cmd.Flags().BoolVarP(&skipApplicationPackage, "no-add", "N", false, "Do not add certificate to the application package")
cmd.MarkPersistentFlagRequired(applicationFlag)
return cmd
}
@@ -95,18 +94,11 @@ $ vespa auth cert add -a my-tenant.my-app.my-instance path/to/application/packag
return cmd
}
-func doCert(cli *CLI, overwriteCertificate, noApplicationPackage bool, args []string) error {
+func doCert(cli *CLI, overwriteCertificate, skipApplicationPackage bool, args []string) error {
app, err := cli.config.application()
if err != nil {
return err
}
- var pkg vespa.ApplicationPackage
- if !noApplicationPackage {
- pkg, err = cli.applicationPackageFrom(args, false)
- if err != nil {
- return err
- }
- }
targetType, err := cli.targetType()
if err != nil {
return err
@@ -122,11 +114,6 @@ func doCert(cli *CLI, overwriteCertificate, noApplicationPackage bool, args []st
if !overwriteCertificate {
hint := "Use -f flag to force overwriting"
- if !noApplicationPackage {
- if pkg.HasCertificate() {
- return errHint(fmt.Errorf("application package %s already contains a certificate", pkg.Path), hint)
- }
- }
if util.PathExists(privateKeyFile) {
return errHint(fmt.Errorf("private key %s already exists", color.CyanString(privateKeyFile)), hint)
}
@@ -134,91 +121,86 @@ func doCert(cli *CLI, overwriteCertificate, noApplicationPackage bool, args []st
return errHint(fmt.Errorf("certificate %s already exists", color.CyanString(certificateFile)), hint)
}
}
- if !noApplicationPackage {
- if pkg.IsZip() {
- hint := "Try running 'mvn clean' before 'vespa auth cert', and then 'mvn package'"
- return errHint(fmt.Errorf("cannot add certificate to compressed application package %s", pkg.Path), hint)
- }
- }
keyPair, err := vespa.CreateKeyPair()
if err != nil {
return err
}
- var pkgCertificateFile string
- if !noApplicationPackage {
- pkgCertificateFile = filepath.Join(pkg.Path, "security", "clients.pem")
- if err := os.MkdirAll(filepath.Dir(pkgCertificateFile), 0755); err != nil {
- return fmt.Errorf("could not create security directory: %w", err)
- }
- if err := keyPair.WriteCertificateFile(pkgCertificateFile, overwriteCertificate); err != nil {
- return fmt.Errorf("could not write certificate to application package: %w", err)
- }
- }
if err := keyPair.WriteCertificateFile(certificateFile, overwriteCertificate); err != nil {
return fmt.Errorf("could not write certificate: %w", err)
}
if err := keyPair.WritePrivateKeyFile(privateKeyFile, overwriteCertificate); err != nil {
return fmt.Errorf("could not write private key: %w", err)
}
- if !noApplicationPackage {
- cli.printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
- }
cli.printSuccess("Certificate written to ", color.CyanString(certificateFile))
cli.printSuccess("Private key written to ", color.CyanString(privateKeyFile))
+ if !skipApplicationPackage {
+ return doCertAdd(cli, overwriteCertificate, args)
+ }
return nil
}
func doCertAdd(cli *CLI, overwriteCertificate bool, args []string) error {
- app, err := cli.config.application()
- if err != nil {
- return err
- }
pkg, err := cli.applicationPackageFrom(args, false)
if err != nil {
return err
}
- targetType, err := cli.targetType()
+ target, err := cli.target(targetOptions{})
if err != nil {
return err
}
- certificateFile, err := cli.config.certificatePath(app, targetType.name)
- if err != nil {
- return err
+ if pkg.HasCertificate() && !overwriteCertificate {
+ return errHint(fmt.Errorf("application package %s already contains a certificate", pkg.Path), "Use -f flag to force overwriting")
}
+ return maybeCopyCertificate(true, false, cli, target, pkg)
+}
- if pkg.IsZip() {
- hint := "Try running 'mvn clean' before 'vespa auth cert add', and then 'mvn package'"
- return errHint(fmt.Errorf("unable to add certificate to compressed application package: %s", pkg.Path), hint)
+func maybeCopyCertificate(force, ignoreZip bool, cli *CLI, target vespa.Target, pkg vespa.ApplicationPackage) error {
+ if pkg.IsZip() && !ignoreZip {
+ hint := "Try running 'mvn clean', then 'vespa auth cert add' and finally 'mvn package'"
+ return errHint(fmt.Errorf("cannot add certificate to compressed application package: %s", pkg.Path), hint)
}
-
- pkgCertificateFile := filepath.Join(pkg.Path, "security", "clients.pem")
- if err := os.MkdirAll(filepath.Dir(pkgCertificateFile), 0755); err != nil {
- return fmt.Errorf("could not create security directory: %w", err)
+ if force {
+ return copyCertificate(cli, target, pkg)
}
- src, err := os.Open(certificateFile)
- if errors.Is(err, os.ErrNotExist) {
- return errHint(fmt.Errorf("there is not key pair generated for application '%s'", app), "Try running 'vespa auth cert' to generate it")
- } else if err != nil {
- return fmt.Errorf("could not open certificate file: %w", err)
- }
- defer src.Close()
- flags := os.O_CREATE | os.O_RDWR
- if overwriteCertificate {
- flags |= os.O_TRUNC
- } else {
- flags |= os.O_EXCL
- }
- dst, err := os.OpenFile(pkgCertificateFile, flags, 0755)
- if errors.Is(err, os.ErrExist) {
- return errHint(fmt.Errorf("application package %s already contains a certificate", pkg.Path), "Use -f flag to force overwriting")
- } else if err != nil {
- return fmt.Errorf("could not open application certificate file for writing: %w", err)
+ if pkg.HasCertificate() {
+ return nil
}
- if _, err := io.Copy(dst, src); err != nil {
- return fmt.Errorf("could not copy certificate file to application: %w", err)
+ if cli.isTerminal() {
+ cli.printWarning("Application package does not contain " + color.CyanString("security/clients.pem") + ", which is required for deployments to Vespa Cloud")
+ ok, err := cli.confirm("Do you want to copy the certificate of application " + color.GreenString(target.Deployment().Application.String()) + " into this application package?")
+ if err != nil {
+ return err
+ }
+ if ok {
+ return copyCertificate(cli, target, pkg)
+ }
}
+ return errHint(fmt.Errorf("deployment to Vespa Cloud requires certificate in application package"),
+ "See https://cloud.vespa.ai/en/security/guide",
+ "Pass --add-cert to use the certificate of the current application")
+}
- cli.printSuccess("Certificate written to ", color.CyanString(pkgCertificateFile))
- return nil
+func copyCertificate(cli *CLI, target vespa.Target, pkg vespa.ApplicationPackage) error {
+ tlsOptions, err := cli.config.readTLSOptions(target.Deployment().Application, target.Type())
+ if err != nil {
+ return err
+ }
+ hint := "Try generating the certificate with 'vespa auth cert'"
+ if tlsOptions.CertificateFile == "" {
+ return errHint(fmt.Errorf("no certificate exists for "+target.Deployment().Application.String()), hint)
+ }
+ data, err := os.ReadFile(tlsOptions.CertificateFile)
+ if err != nil {
+ return errHint(fmt.Errorf("could not read certificate file: %w", err))
+ }
+ dstPath := filepath.Join(pkg.Path, "security", "clients.pem")
+ if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
+ return fmt.Errorf("could not create security directory: %w", err)
+ }
+ err = util.AtomicWriteFile(dstPath, data)
+ if err == nil {
+ cli.printSuccess("Copied certificate from ", tlsOptions.CertificateFile, " to ", dstPath)
+ }
+ return err
}
diff --git a/client/go/internal/cli/cmd/cert_test.go b/client/go/internal/cli/cmd/cert_test.go
index d6b47083e7b..0a20ab8eb1a 100644
--- a/client/go/internal/cli/cmd/cert_test.go
+++ b/client/go/internal/cli/cmd/cert_test.go
@@ -22,13 +22,21 @@ func TestCert(t *testing.T) {
})
}
+func configureCloud(t *testing.T, cli *CLI) {
+ require.Nil(t, cli.Run("config", "set", "application", "t1.a1.i1"))
+ require.Nil(t, cli.Run("config", "set", "target", "cloud"))
+ require.Nil(t, cli.Run("auth", "api-key"))
+}
+
func testCert(t *testing.T, subcommand []string) {
appDir, pkgDir := mock.ApplicationPackageDir(t, false, false)
- cli, stdout, stderr := newTestCLI(t)
- args := append(subcommand, "-a", "t1.a1.i1", pkgDir)
- err := cli.Run(args...)
- assert.Nil(t, err)
+ cli, stdout, _ := newTestCLI(t)
+ configureCloud(t, cli)
+ stdout.Reset()
+
+ args := append(subcommand, pkgDir)
+ require.Nil(t, cli.Run(args...))
app, err := vespa.ApplicationFromString("t1.a1.i1")
assert.Nil(t, err)
@@ -38,12 +46,7 @@ func testCert(t *testing.T, subcommand []string) {
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), stdout.String())
-
- args = append(subcommand, "-a", "t1.a1.i1", pkgDir)
- err = cli.Run(args...)
- assert.NotNil(t, err)
- assert.Contains(t, stderr.String(), fmt.Sprintf("Error: application package %s already contains a certificate", appDir))
+ assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Private key written to %s\nSuccess: Copied certificate from %s to %s\n", certificate, privateKey, certificate, pkgCertificate), stdout.String())
}
func TestCertCompressedPackage(t *testing.T) {
@@ -61,8 +64,11 @@ func testCertCompressedPackage(t *testing.T, subcommand []string) {
assert.Nil(t, err)
cli, stdout, stderr := newTestCLI(t)
+ configureCloud(t, cli)
+ stdout.Reset()
+ stderr.Reset()
- args := append(subcommand, "-a", "t1.a1.i1", pkgDir)
+ args := append(subcommand, pkgDir)
err = cli.Run(args...)
assert.NotNil(t, err)
assert.Contains(t, stderr.String(), "Error: cannot add certificate to compressed application package")
@@ -70,7 +76,7 @@ func testCertCompressedPackage(t *testing.T, subcommand []string) {
err = os.Remove(zipFile)
assert.Nil(t, err)
- args = append(subcommand, "-f", "-a", "t1.a1.i1", pkgDir)
+ args = append(subcommand, "-f", pkgDir)
err = cli.Run(args...)
assert.Nil(t, err)
assert.Contains(t, stdout.String(), "Success: Certificate written to")
@@ -79,30 +85,30 @@ func testCertCompressedPackage(t *testing.T, subcommand []string) {
func TestCertAdd(t *testing.T) {
cli, stdout, stderr := newTestCLI(t)
- err := cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
- assert.Nil(t, err)
+ configureCloud(t, cli)
+ stdout.Reset()
+ require.Nil(t, cli.Run("auth", "cert", "-N"))
appDir, pkgDir := mock.ApplicationPackageDir(t, false, false)
stdout.Reset()
- err = cli.Run("auth", "cert", "add", "-a", "t1.a1.i1", pkgDir)
- assert.Nil(t, err)
+ require.Nil(t, cli.Run("auth", "cert", "add", pkgDir))
pkgCertificate := filepath.Join(appDir, "security", "clients.pem")
- assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\n", pkgCertificate), stdout.String())
+ homeDir := cli.config.homeDir
+ certificate := filepath.Join(homeDir, "t1.a1.i1", "data-plane-public-cert.pem")
+ assert.Equal(t, fmt.Sprintf("Success: Copied certificate from %s to %s\n", certificate, pkgCertificate), stdout.String())
- err = cli.Run("auth", "cert", "add", "-a", "t1.a1.i1", pkgDir)
- assert.NotNil(t, err)
+ require.NotNil(t, cli.Run("auth", "cert", "add", pkgDir))
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())
+ require.Nil(t, cli.Run("auth", "cert", "add", "-f", pkgDir))
+ assert.Equal(t, fmt.Sprintf("Success: Copied certificate from %s to %s\n", certificate, pkgCertificate), stdout.String())
}
func TestCertNoAdd(t *testing.T) {
cli, stdout, stderr := newTestCLI(t)
-
- err := cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
- assert.Nil(t, err)
+ configureCloud(t, cli)
+ stdout.Reset()
+ require.Nil(t, cli.Run("auth", "cert", "-N"))
homeDir := cli.config.homeDir
app, err := vespa.ApplicationFromString("t1.a1.i1")
@@ -112,18 +118,15 @@ func TestCertNoAdd(t *testing.T) {
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), stdout.String())
- err = cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
- assert.NotNil(t, err)
+ require.NotNil(t, cli.Run("auth", "cert", "-N"))
assert.Contains(t, stderr.String(), fmt.Sprintf("Error: private key %s already exists", privateKey))
require.Nil(t, os.Remove(privateKey))
stderr.Reset()
- err = cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
- assert.NotNil(t, err)
+ require.NotNil(t, cli.Run("auth", "cert", "-N"))
assert.Contains(t, stderr.String(), fmt.Sprintf("Error: certificate %s already exists", certificate))
stdout.Reset()
- err = cli.Run("auth", "cert", "-N", "-f", "-a", "t1.a1.i1")
- assert.Nil(t, err)
+ require.Nil(t, cli.Run("auth", "cert", "-N", "-f"))
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/internal/cli/cmd/deploy.go b/client/go/internal/cli/cmd/deploy.go
index 76027744268..35b9ee0f300 100644
--- a/client/go/internal/cli/cmd/deploy.go
+++ b/client/go/internal/cli/cmd/deploy.go
@@ -20,6 +20,7 @@ func newDeployCmd(cli *CLI) *cobra.Command {
var (
logLevelArg string
versionArg string
+ copyCert bool
)
cmd := &cobra.Command{
Use: "deploy [application-directory]",
@@ -67,16 +68,18 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
}
opts.Version = version
}
-
+ if target.Type() == vespa.TargetCloud {
+ if err := maybeCopyCertificate(copyCert, true, cli, target, pkg); err != nil {
+ return err
+ }
+ }
var result vespa.PrepareResult
- err = cli.spinner(cli.Stderr, "Uploading application package ...", func() error {
+ if err := cli.spinner(cli.Stderr, "Uploading application package ...", func() error {
result, err = vespa.Deploy(opts)
return err
- })
- if err != nil {
+ }); err != nil {
return err
}
-
log.Println()
if opts.Target.IsCloud() {
cli.printSuccess("Triggered deployment of ", color.CyanString(pkg.Path), " with run ID ", color.CyanString(strconv.FormatInt(result.ID, 10)))
@@ -97,6 +100,7 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
}
cmd.Flags().StringVarP(&logLevelArg, "log-level", "l", "error", `Log level for Vespa logs. Must be "error", "warning", "info" or "debug"`)
cmd.Flags().StringVarP(&versionArg, "version", "V", "", `Override the Vespa runtime version to use in Vespa Cloud`)
+ cmd.Flags().BoolVarP(&copyCert, "add-cert", "A", false, `Copy certificate of the configured application to the current application package`)
return cmd
}
diff --git a/client/go/internal/cli/cmd/deploy_test.go b/client/go/internal/cli/cmd/deploy_test.go
index 9eaf878bc5e..78834b7185b 100644
--- a/client/go/internal/cli/cmd/deploy_test.go
+++ b/client/go/internal/cli/cmd/deploy_test.go
@@ -5,14 +5,62 @@
package cmd
import (
+ "bytes"
+ "path/filepath"
"strconv"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
"github.com/vespa-engine/vespa/client/go/internal/mock"
"github.com/vespa-engine/vespa/client/go/internal/vespa"
)
+func TestDeployCloud(t *testing.T) {
+ pkgDir := filepath.Join(t.TempDir(), "app")
+ createApplication(t, pkgDir, false, false)
+
+ cli, stdout, stderr := newTestCLI(t, "CI=true", "NO_COLOR=true")
+ httpClient := &mock.HTTPClient{}
+ httpClient.NextResponseString(200, `ok`)
+ cli.httpClient = httpClient
+
+ app := vespa.ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"}
+ assert.Nil(t, cli.Run("config", "set", "application", app.String()))
+ 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"))
+
+ stderr.Reset()
+ require.NotNil(t, cli.Run("deploy", pkgDir))
+ certError := `Error: deployment to Vespa Cloud requires certificate in application package
+Hint: See https://cloud.vespa.ai/en/security/guide
+Hint: Pass --add-cert to use the certificate of the current application
+`
+ assert.Equal(t, certError, stderr.String())
+
+ require.Nil(t, cli.Run("deploy", "--add-cert", pkgDir))
+ assert.Contains(t, stdout.String(), "Success: Triggered deployment")
+
+ // Answer interactive certificate copy prompt
+ stdout.Reset()
+ stderr.Reset()
+ cli.isTerminal = func() bool { return true }
+ pkgDir2 := filepath.Join(t.TempDir(), "app")
+ createApplication(t, pkgDir2, false, false)
+
+ var buf bytes.Buffer
+ buf.WriteString("wat\nthe\nfck\nn\n")
+ cli.Stdin = &buf
+ require.NotNil(t, cli.Run("deploy", "--add-cert=false", pkgDir2))
+ warning := "Warning: Application package does not contain security/clients.pem, which is required for deployments to Vespa Cloud\n"
+ assert.Equal(t, warning+strings.Repeat("Error: please answer 'Y' or 'n'\n", 3)+certError, stderr.String())
+ buf.WriteString("y\n")
+ require.Nil(t, cli.Run("deploy", "--add-cert=false", pkgDir2))
+ assert.Contains(t, stdout.String(), "Success: Triggered deployment")
+}
+
func TestPrepareZip(t *testing.T) {
assertPrepare("testdata/applications/withTarget/target/application.zip",
[]string{"prepare", "testdata/applications/withTarget/target/application.zip"}, t)
@@ -42,7 +90,7 @@ func TestDeployZipWithURLTargetArgument(t *testing.T) {
assertDeployRequestMade("http://target:19071", client, t)
}
-func TestDeployZipWitLocalTargetArgument(t *testing.T) {
+func TestDeployZipWithLocalTargetArgument(t *testing.T) {
assertDeploy("testdata/applications/withTarget/target/application.zip",
[]string{"deploy", "testdata/applications/withTarget/target/application.zip", "-t", "local"}, t)
}
diff --git a/client/go/internal/cli/cmd/document.go b/client/go/internal/cli/cmd/document.go
index 0ed68e30ced..6a07121a13b 100644
--- a/client/go/internal/cli/cmd/document.go
+++ b/client/go/internal/cli/cmd/document.go
@@ -160,8 +160,8 @@ func newDocumentCmd(cli *CLI) *cobra.Command {
)
cmd := &cobra.Command{
Use: "document json-file",
- Short: "Issue a document operation to Vespa",
- Long: `Issue a document operation to Vespa.
+ Short: "Issue a single document operation to Vespa",
+ Long: `Issue a single 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
diff --git a/client/go/internal/cli/cmd/feed.go b/client/go/internal/cli/cmd/feed.go
index 6d368cb210b..cad3568a89f 100644
--- a/client/go/internal/cli/cmd/feed.go
+++ b/client/go/internal/cli/cmd/feed.go
@@ -56,8 +56,8 @@ func newFeedCmd(cli *CLI) *cobra.Command {
var options feedOptions
cmd := &cobra.Command{
Use: "feed FILE [FILE]...",
- Short: "Feed documents to a Vespa cluster",
- Long: `Feed documents to a Vespa cluster.
+ Short: "Feed multiple document operations to a Vespa cluster",
+ Long: `Feed multiple document operations to a Vespa cluster.
This command can be used to feed large amounts of documents to a Vespa cluster
efficiently.
diff --git a/client/go/internal/cli/cmd/login.go b/client/go/internal/cli/cmd/login.go
index d2075bdfcf0..54c0dfef770 100644
--- a/client/go/internal/cli/cmd/login.go
+++ b/client/go/internal/cli/cmd/login.go
@@ -4,7 +4,6 @@ import (
"fmt"
"log"
"os"
- "strings"
"time"
"github.com/pkg/browser"
@@ -46,7 +45,10 @@ func newLoginCmd(cli *CLI) *cobra.Command {
log.Printf("Your Device Confirmation code is: %s\n", state.UserCode)
- auto_open := confirm(cli, "Automatically open confirmation page in your default browser?")
+ auto_open, err := cli.confirm("Automatically open confirmation page in your default browser?")
+ if err != nil {
+ return err
+ }
if auto_open {
log.Printf("Opened link in your browser: %s\n", state.VerificationURI)
@@ -90,22 +92,3 @@ func newLoginCmd(cli *CLI) *cobra.Command {
},
}
}
-
-func confirm(cli *CLI, question string) bool {
- for {
- var answer string
-
- fmt.Fprintf(cli.Stdout, "%s [Y/n] ", question)
- fmt.Fscanln(cli.Stdin, &answer)
-
- answer = strings.TrimSpace(strings.ToLower(answer))
-
- if answer == "y" || answer == "" {
- return true
- } else if answer == "n" {
- return false
- } else {
- log.Printf("Please answer Y or N.\n")
- }
- }
-}
diff --git a/client/go/internal/cli/cmd/prod.go b/client/go/internal/cli/cmd/prod.go
index 318dcefe7f7..6daa8db6e81 100644
--- a/client/go/internal/cli/cmd/prod.go
+++ b/client/go/internal/cli/cmd/prod.go
@@ -103,7 +103,8 @@ https://cloud.vespa.ai/en/reference/deployment`,
}
func newProdDeployCmd(cli *CLI) *cobra.Command {
- return &cobra.Command{
+ copyCert := false
+ cmd := &cobra.Command{
Use: "deploy",
Aliases: []string{"submit"}, // TODO: Remove in Vespa 9
Short: "Deploy an application to production",
@@ -145,6 +146,9 @@ $ vespa prod deploy`,
if err != nil {
return err
}
+ if err := maybeCopyCertificate(copyCert, true, cli, target, pkg); err != nil {
+ return err
+ }
if err := vespa.Submit(opts); err != nil {
return fmt.Errorf("could not deploy application: %w", err)
} else {
@@ -155,6 +159,8 @@ $ vespa prod deploy`,
return nil
},
}
+ cmd.Flags().BoolVarP(&copyCert, "add-cert", "A", false, `Copy certificate of the configured application to the current application package`)
+ return cmd
}
func writeWithBackup(stdout io.Writer, pkg vespa.ApplicationPackage, filename, contents string) error {
diff --git a/client/go/internal/cli/cmd/prod_test.go b/client/go/internal/cli/cmd/prod_test.go
index 6a6cc494dcb..a01056b7178 100644
--- a/client/go/internal/cli/cmd/prod_test.go
+++ b/client/go/internal/cli/cmd/prod_test.go
@@ -9,7 +9,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"github.com/vespa-engine/vespa/client/go/internal/mock"
"github.com/vespa-engine/vespa/client/go/internal/util"
"github.com/vespa-engine/vespa/client/go/internal/vespa"
@@ -172,19 +171,7 @@ func prodDeploy(pkgDir string, t *testing.T) {
assert.Nil(t, cli.Run("config", "set", "application", app.String()))
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))
-
- // Remove certificate as it's not required for submission (but it must be part of the application package)
- if path, err := cli.config.privateKeyPath(app, vespa.TargetCloud); err == nil {
- os.RemoveAll(path)
- } else {
- require.Nil(t, err)
- }
- if path, err := cli.config.certificatePath(app, vespa.TargetCloud); err == nil {
- os.RemoveAll(path)
- } else {
- require.Nil(t, err)
- }
+ assert.Nil(t, cli.Run("auth", "cert", "--no-add"))
// Zipping requires relative paths, so must let command run from pkgDir, then reset cwd for subsequent tests.
if cwd, err := os.Getwd(); err != nil {
@@ -198,11 +185,11 @@ func prodDeploy(pkgDir string, t *testing.T) {
stdout.Reset()
cli.Environment["VESPA_CLI_API_KEY_FILE"] = filepath.Join(cli.config.homeDir, "t1.api-key.pem")
- assert.Nil(t, cli.Run("prod", "deploy"))
+ assert.Nil(t, cli.Run("prod", "deploy", "--add-cert"))
assert.Contains(t, stdout.String(), "Success: Deployed")
assert.Contains(t, stdout.String(), "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
stdout.Reset()
- assert.Nil(t, cli.Run("prod", "submit")) // old variant also works
+ assert.Nil(t, cli.Run("prod", "submit", "--add-cert")) // old variant also works
assert.Contains(t, stdout.String(), "Success: Deployed")
assert.Contains(t, stdout.String(), "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
}
@@ -218,7 +205,7 @@ func TestProdDeployWithJava(t *testing.T) {
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("auth", "cert", "--no-add"))
// Copy an application package pre-assembled with mvn package
testAppDir := filepath.Join("testdata", "applications", "withDeployment", "target")
@@ -229,7 +216,7 @@ func TestProdDeployWithJava(t *testing.T) {
stdout.Reset()
cli.Environment["VESPA_CLI_API_KEY_FILE"] = filepath.Join(cli.config.homeDir, "t1.api-key.pem")
- assert.Nil(t, cli.Run("prod", "submit", pkgDir))
+ assert.Nil(t, cli.Run("prod", "deploy", pkgDir))
assert.Contains(t, stdout.String(), "Success: Deployed")
assert.Contains(t, stdout.String(), "See https://console.vespa-cloud.com/tenant/t1/application/a1/prod/deployment for deployment progress")
}
@@ -245,7 +232,7 @@ func TestProdDeployInvalidZip(t *testing.T) {
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("auth", "cert", "--no-add"))
// Copy an invalid application package containing relative file names
testAppDir := filepath.Join("testdata", "applications", "withInvalidEntries", "target")
@@ -254,7 +241,7 @@ func TestProdDeployInvalidZip(t *testing.T) {
testZipFile := filepath.Join(testAppDir, "application-test.zip")
copyFile(t, filepath.Join(pkgDir, "target", "application-test.zip"), testZipFile)
- assert.NotNil(t, cli.Run("prod", "submit", pkgDir))
+ assert.NotNil(t, cli.Run("prod", "deploy", pkgDir))
assert.Equal(t, "Error: found invalid path inside zip: ../../../../../../../tmp/foo\n", stderr.String())
}
diff --git a/client/go/internal/cli/cmd/root.go b/client/go/internal/cli/cmd/root.go
index 17c4fc41625..41de0bfc9d3 100644
--- a/client/go/internal/cli/cmd/root.go
+++ b/client/go/internal/cli/cmd/root.go
@@ -105,8 +105,7 @@ func New(stdout, stderr io.Writer, environment []string) (*CLI, error) {
Short: "The command-line tool for Vespa.ai",
Long: `The command-line tool for Vespa.ai.
-Use it on Vespa instances running locally, remotely or in the cloud.
-Prefer web service API's to this in production.
+Use it on Vespa instances running locally, remotely or in Vespa Cloud.
Vespa documentation: https://docs.vespa.ai
@@ -301,6 +300,26 @@ func (c *CLI) printWarning(msg interface{}, hints ...string) {
}
}
+func (c *CLI) confirm(question string) (bool, error) {
+ if !c.isTerminal() {
+ return false, fmt.Errorf("terminal is not interactive")
+ }
+ for {
+ var answer string
+ fmt.Fprintf(c.Stdout, "%s [Y/n] ", question)
+ fmt.Fscanln(c.Stdin, &answer)
+ answer = strings.TrimSpace(strings.ToLower(answer))
+ switch answer {
+ case "y", "":
+ return true, nil
+ case "n":
+ return false, nil
+ default:
+ c.printErr(fmt.Errorf("please answer 'Y' or 'n'"))
+ }
+ }
+}
+
// target creates a target according the configuration of this CLI and given opts.
func (c *CLI) target(opts targetOptions) (vespa.Target, error) {
targetType, err := c.targetType()
diff --git a/client/go/internal/cli/cmd/visit.go b/client/go/internal/cli/cmd/visit.go
index 1875c768c60..a588474bd2b 100644
--- a/client/go/internal/cli/cmd/visit.go
+++ b/client/go/internal/cli/cmd/visit.go
@@ -89,10 +89,10 @@ func newVisitCmd(cli *CLI) *cobra.Command {
)
cmd := &cobra.Command{
Use: "visit",
- Short: "Visit and print all documents in a vespa cluster",
- Long: `Run visiting to retrieve all documents.
+ Short: "Visit and print all documents in a Vespa cluster",
+ Long: `Visit and print all documents in a Vespa cluster.
-By default prints each document received on its own line (JSON-L format).
+By default prints each document received on its own line (JSONL format).
`,
Example: `$ vespa visit # get documents from any cluster
$ vespa visit --content-cluster search # get documents from cluster named "search"
diff --git a/client/go/internal/vespa/deploy.go b/client/go/internal/vespa/deploy.go
index 4531af75737..8b2cb6ea05d 100644
--- a/client/go/internal/vespa/deploy.go
+++ b/client/go/internal/vespa/deploy.go
@@ -21,7 +21,11 @@ import (
"github.com/vespa-engine/vespa/client/go/internal/version"
)
-var DefaultApplication = ApplicationID{Tenant: "default", Application: "application", Instance: "default"}
+var (
+ DefaultApplication = ApplicationID{Tenant: "default", Application: "application", Instance: "default"}
+ DefaultZone = ZoneID{Environment: "prod", Region: "default"}
+ DefaultDeployment = Deployment{Application: DefaultApplication, Zone: DefaultZone}
+)
type ApplicationID struct {
Tenant string
diff --git a/client/go/internal/vespa/target_custom.go b/client/go/internal/vespa/target_custom.go
index 93397287ac8..fd0af0e8d53 100644
--- a/client/go/internal/vespa/target_custom.go
+++ b/client/go/internal/vespa/target_custom.go
@@ -36,7 +36,7 @@ func (t *customTarget) Type() string { return t.targetType }
func (t *customTarget) IsCloud() bool { return false }
-func (t *customTarget) Deployment() Deployment { return Deployment{} }
+func (t *customTarget) Deployment() Deployment { return DefaultDeployment }
func (t *customTarget) createService(name string) (*Service, error) {
switch name {