diff options
author | Martin Polden <mpolden@mpolden.no> | 2023-06-19 15:37:10 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-19 15:37:10 +0200 |
commit | b62474b0429892b9bb6b4ca55da020caf73745df (patch) | |
tree | e48c08f2656f057609a1799e697063b5c9613ff9 | |
parent | 23ce1275df759542e6557cc72836b1ddba348822 (diff) | |
parent | 4ebfe34853abc55c3ce6b425107c08b9994554ad (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.go | 132 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/cert_test.go | 65 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/deploy.go | 14 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/deploy_test.go | 50 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/document.go | 4 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/feed.go | 4 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/login.go | 25 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/prod.go | 8 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/prod_test.go | 27 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/root.go | 23 | ||||
-rw-r--r-- | client/go/internal/cli/cmd/visit.go | 6 | ||||
-rw-r--r-- | client/go/internal/vespa/deploy.go | 6 | ||||
-rw-r--r-- | client/go/internal/vespa/target_custom.go | 2 |
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(©Cert, "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(©Cert, "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 { |