summaryrefslogtreecommitdiffstats
path: root/client/go
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-04-06 10:59:54 +0200
committerMartin Polden <mpolden@mpolden.no>2022-04-06 10:59:54 +0200
commit77e9c658f9df1fd6d5353bf0cc7435250d466106 (patch)
treee5eef2417f91cf6e3ad2ed75cdaf82f691be00ef /client/go
parentbb310e8cb1d2a9766ee61be3b888430ed78c6967 (diff)
Add version flag
Diffstat (limited to 'client/go')
-rw-r--r--client/go/cmd/cert_test.go9
-rw-r--r--client/go/cmd/deploy.go17
-rw-r--r--client/go/cmd/log_test.go4
-rw-r--r--client/go/cmd/testutil_test.go19
-rw-r--r--client/go/mock/vespa.go39
-rw-r--r--client/go/vespa/deploy.go48
-rw-r--r--client/go/vespa/deploy_test.go89
7 files changed, 193 insertions, 32 deletions
diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go
index e5837170d15..ee0c21adaf5 100644
--- a/client/go/cmd/cert_test.go
+++ b/client/go/cmd/cert_test.go
@@ -12,6 +12,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
+ "github.com/vespa-engine/vespa/client/go/mock"
"github.com/vespa-engine/vespa/client/go/vespa"
)
@@ -25,7 +26,7 @@ func TestCert(t *testing.T) {
}
func testCert(t *testing.T, subcommand []string) {
- pkgDir := mockApplicationPackage(t, false)
+ appDir, pkgDir := mock.ApplicationPackageDir(t, false, false)
cli, stdout, stderr := newTestCLI(t)
args := append(subcommand, "-a", "t1.a1.i1", pkgDir)
@@ -35,7 +36,6 @@ func testCert(t *testing.T, subcommand []string) {
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")
@@ -59,7 +59,7 @@ func TestCertCompressedPackage(t *testing.T) {
}
func testCertCompressedPackage(t *testing.T, subcommand []string) {
- pkgDir := mockApplicationPackage(t, true)
+ _, pkgDir := mock.ApplicationPackageDir(t, true, false)
zipFile := filepath.Join(pkgDir, "target", "application.zip")
err := os.MkdirAll(filepath.Dir(zipFile), 0755)
assert.Nil(t, err)
@@ -88,11 +88,10 @@ func TestCertAdd(t *testing.T) {
err := cli.Run("auth", "cert", "-N", "-a", "t1.a1.i1")
assert.Nil(t, err)
- pkgDir := mockApplicationPackage(t, false)
+ appDir, pkgDir := mock.ApplicationPackageDir(t, false, false)
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), stdout.String())
diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go
index a287165bb5e..77a40f53522 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -12,12 +12,14 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/version"
"github.com/vespa-engine/vespa/client/go/vespa"
)
func newDeployCmd(cli *CLI) *cobra.Command {
var (
logLevelArg string
+ versionArg string
)
cmd := &cobra.Command{
Use: "deploy [application-directory]",
@@ -32,7 +34,12 @@ 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.`,
+only.
+
+In Vespa Cloud you may override the Vespa runtime version for your deployment.
+This option should only be used if you have a reason for using a specific
+version. By default Vespa Cloud chooses a suitable version for you.
+`,
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
@@ -50,6 +57,13 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
return err
}
opts := cli.createDeploymentOptions(pkg, target)
+ if versionArg != "" {
+ version, err := version.Parse(versionArg)
+ if err != nil {
+ return err
+ }
+ opts.Version = version
+ }
var result vespa.PrepareResult
err = cli.spinner(cli.Stderr, "Uploading application package ...", func() error {
@@ -79,6 +93,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`)
return cmd
}
diff --git a/client/go/cmd/log_test.go b/client/go/cmd/log_test.go
index d3e7b630869..d7b5e20fab5 100644
--- a/client/go/cmd/log_test.go
+++ b/client/go/cmd/log_test.go
@@ -10,7 +10,7 @@ import (
)
func TestLog(t *testing.T) {
- pkgDir := mockApplicationPackage(t, false)
+ _, pkgDir := mock.ApplicationPackageDir(t, false, false)
httpClient := &mock.HTTPClient{}
httpClient.NextResponseString(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, stdout, stderr := newTestCLI(t)
@@ -34,7 +34,7 @@ func TestLogOldClient(t *testing.T) {
cli, _, stderr := newTestCLI(t)
cli.version = version.MustParse("7.0.0")
- pkgDir := mockApplicationPackage(t, false)
+ _, pkgDir := mock.ApplicationPackageDir(t, false, false)
httpClient := &mock.HTTPClient{}
httpClient.NextResponseString(200, `{"minVersion": "8.0.0"}`)
httpClient.NextResponseString(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`)
diff --git a/client/go/cmd/testutil_test.go b/client/go/cmd/testutil_test.go
index 68f79187d3a..e5c69e38e93 100644
--- a/client/go/cmd/testutil_test.go
+++ b/client/go/cmd/testutil_test.go
@@ -3,7 +3,6 @@ package cmd
import (
"bytes"
- "os"
"path/filepath"
"testing"
@@ -29,21 +28,3 @@ func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Bu
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/mock/vespa.go b/client/go/mock/vespa.go
new file mode 100644
index 00000000000..ca09a389360
--- /dev/null
+++ b/client/go/mock/vespa.go
@@ -0,0 +1,39 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package mock
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+// ApplicationPackageDir creates a mock application package directory using test helper t, returning the path to the
+// "application" directory and the root directory where it was created. If java is true, create a file that indicates
+// this package contains Java code. If cert is true, create an empty certificate file.
+func ApplicationPackageDir(t *testing.T, java, cert bool) (string, string) {
+ t.Helper()
+ rootDir := t.TempDir()
+ appDir := filepath.Join(rootDir, "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(rootDir, "pom.xml")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if cert {
+ securityDir := filepath.Join(appDir, "security")
+ if err := os.MkdirAll(securityDir, 0755); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Create(filepath.Join(securityDir, "clients.pem")); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return appDir, rootDir
+}
diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go
index d479c86a4c7..4fa0bb5b839 100644
--- a/client/go/vespa/deploy.go
+++ b/client/go/vespa/deploy.go
@@ -12,11 +12,13 @@ import (
"mime/multipart"
"net/http"
"net/url"
+ "path/filepath"
"strconv"
"strings"
"time"
"github.com/vespa-engine/vespa/client/go/util"
+ "github.com/vespa-engine/vespa/client/go/version"
)
var DefaultApplication = ApplicationID{Tenant: "default", Application: "application", Instance: "default"}
@@ -42,6 +44,7 @@ type DeploymentOptions struct {
Target Target
ApplicationPackage ApplicationPackage
Timeout time.Duration
+ Version version.Version
HTTPClient util.HTTPClient
}
@@ -259,21 +262,56 @@ func checkDeploymentOpts(opts DeploymentOptions) error {
if opts.Target.Type() == TargetCloud && !opts.ApplicationPackage.HasCertificate() {
return fmt.Errorf("%s: missing certificate in package", opts)
}
+ if !opts.IsCloud() && !opts.Version.IsZero() {
+ return fmt.Errorf("%s: custom runtime version is not supported by %s target", opts, opts.Target.Type())
+ }
return nil
}
-func uploadApplicationPackage(url *url.URL, opts DeploymentOptions) (PrepareResult, error) {
+func newDeploymentRequest(url *url.URL, opts DeploymentOptions) (*http.Request, error) {
zipReader, err := opts.ApplicationPackage.zipReader(false)
if err != nil {
- return PrepareResult{}, err
+ return nil, err
}
+ var body io.Reader
header := http.Header{}
- header.Add("Content-Type", "application/zip")
- request := &http.Request{
+ if opts.IsCloud() {
+ var buf bytes.Buffer
+ form := multipart.NewWriter(&buf)
+ formFile, err := form.CreateFormFile("applicationZip", filepath.Base(opts.ApplicationPackage.Path))
+ if err != nil {
+ return nil, err
+ }
+ if _, err := io.Copy(formFile, zipReader); err != nil {
+ return nil, err
+ }
+ if !opts.Version.IsZero() {
+ deployOptions := fmt.Sprintf(`{"vespaVersion":"%s"}`, opts.Version.String())
+ if err := form.WriteField("deployOptions", deployOptions); err != nil {
+ return nil, err
+ }
+ }
+ if err := form.Close(); err != nil {
+ return nil, err
+ }
+ header.Set("Content-Type", form.FormDataContentType())
+ body = &buf
+ } else {
+ header.Set("Content-Type", "application/zip")
+ body = zipReader
+ }
+ return &http.Request{
URL: url,
Method: "POST",
Header: header,
- Body: io.NopCloser(zipReader),
+ Body: io.NopCloser(body),
+ }, nil
+}
+
+func uploadApplicationPackage(url *url.URL, opts DeploymentOptions) (PrepareResult, error) {
+ request, err := newDeploymentRequest(url, opts)
+ if err != nil {
+ return PrepareResult{}, err
}
service, err := opts.Target.Service(DeployService, opts.Timeout, 0, "")
if err != nil {
diff --git a/client/go/vespa/deploy_test.go b/client/go/vespa/deploy_test.go
index f27a2f2927d..297d028ee91 100644
--- a/client/go/vespa/deploy_test.go
+++ b/client/go/vespa/deploy_test.go
@@ -2,14 +2,75 @@
package vespa
import (
+ "io"
+ "mime"
+ "mime/multipart"
+ "net/http"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/vespa-engine/vespa/client/go/mock"
+ "github.com/vespa-engine/vespa/client/go/version"
)
+func TestDeploy(t *testing.T) {
+ httpClient := mock.HTTPClient{}
+ target := LocalTarget(&httpClient)
+ appDir, _ := mock.ApplicationPackageDir(t, false, false)
+ opts := DeploymentOptions{
+ Target: target,
+ ApplicationPackage: ApplicationPackage{Path: appDir},
+ HTTPClient: &httpClient,
+ }
+ _, err := Deploy(opts)
+ assert.Nil(t, err)
+ assert.Equal(t, 1, len(httpClient.Requests))
+ req := httpClient.LastRequest
+ assert.Equal(t, "http://127.0.0.1:19071/application/v2/tenant/default/prepareandactivate", req.URL.String())
+ assert.Equal(t, "application/zip", req.Header.Get("content-type"))
+ buf := make([]byte, 5)
+ req.Body.Read(buf)
+ assert.Equal(t, "PK\x03\x04\x14", string(buf))
+}
+
+func TestDeployCloud(t *testing.T) {
+ httpClient := mock.HTTPClient{}
+ target := createCloudTarget(t, "http://vespacloud", io.Discard)
+ cloudTarget, ok := target.(*cloudTarget)
+ require.True(t, ok)
+ cloudTarget.httpClient = &httpClient
+ appDir, _ := mock.ApplicationPackageDir(t, false, true)
+ opts := DeploymentOptions{
+ Target: target,
+ ApplicationPackage: ApplicationPackage{Path: appDir},
+ HTTPClient: &httpClient,
+ }
+ _, err := Deploy(opts)
+ require.Nil(t, err)
+ assert.Equal(t, 1, len(httpClient.Requests))
+ req := httpClient.LastRequest
+ assert.Equal(t, "http://vespacloud/application/v4/tenant/t1/application/a1/instance/i1/deploy/dev-us-north-1", req.URL.String())
+
+ values := parseMultiPart(t, req)
+ zipData := values["applicationZip"]
+ assert.Equal(t, "PK\x03\x04\x14", string(zipData[:5]))
+ _, hasDeployOptions := values["deployOptions"]
+ assert.False(t, hasDeployOptions)
+
+ opts.Version = version.MustParse("1.2.3")
+ _, err = Deploy(opts)
+ require.Nil(t, err)
+ req = httpClient.LastRequest
+ values = parseMultiPart(t, req)
+ zipData = values["applicationZip"]
+ assert.Equal(t, "PK\x03\x04\x14", string(zipData[:5]))
+ assert.Equal(t, string(values["deployOptions"]), `{"vespaVersion":"1.2.3"}`)
+}
+
func TestApplicationFromString(t *testing.T) {
app, err := ApplicationFromString("t1.a1.i1")
assert.Nil(t, err)
@@ -65,6 +126,7 @@ type pkgFixture struct {
}
func assertFindApplicationPackage(t *testing.T, zipOrDir string, fixture pkgFixture) {
+ t.Helper()
if fixture.existingFile != "" {
writeFile(t, fixture.existingFile)
}
@@ -77,6 +139,7 @@ func assertFindApplicationPackage(t *testing.T, zipOrDir string, fixture pkgFixt
}
func writeFile(t *testing.T, name string) {
+ t.Helper()
err := os.MkdirAll(filepath.Dir(name), 0755)
assert.Nil(t, err)
if !strings.HasSuffix(name, string(os.PathSeparator)) {
@@ -84,3 +147,29 @@ func writeFile(t *testing.T, name string) {
assert.Nil(t, err)
}
}
+
+func parseMultiPart(t *testing.T, req *http.Request) map[string][]byte {
+ t.Helper()
+
+ mediaType, params, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
+ require.Nil(t, err)
+ assert.Equal(t, mediaType, "multipart/form-data")
+
+ values := make(map[string][]byte)
+ mr := multipart.NewReader(req.Body, params["boundary"])
+ for {
+ p, err := mr.NextPart()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ data, err := io.ReadAll(p)
+ if err != nil {
+ t.Fatal(err)
+ }
+ values[p.FormName()] = data
+ }
+ return values
+}