diff options
-rw-r--r-- | client/go/cmd/cert_test.go | 9 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 17 | ||||
-rw-r--r-- | client/go/cmd/log_test.go | 4 | ||||
-rw-r--r-- | client/go/cmd/testutil_test.go | 19 | ||||
-rw-r--r-- | client/go/mock/vespa.go | 39 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 48 | ||||
-rw-r--r-- | client/go/vespa/deploy_test.go | 89 |
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 +} |