diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-08-25 09:27:05 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-25 09:27:05 +0200 |
commit | c43e37dcfb49d50a2b4109e4f8ac9a1d3ae0609e (patch) | |
tree | ace4fa1e27bd4eaa6343aa41e57fb10920278ca9 /client | |
parent | 62f522143bbecfc26d910d39e728bd33b1e61f16 (diff) | |
parent | 3b1eca74dce4aea412425606c53680eecb780f13 (diff) |
Merge pull request #18847 from vespa-engine/bratseth/deploy
Bratseth/deploy
Diffstat (limited to 'client')
-rw-r--r-- | client/go/cmd/activate.go | 25 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 174 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 12 | ||||
-rw-r--r-- | client/go/cmd/document.go | 10 | ||||
-rw-r--r-- | client/go/cmd/document_test.go | 13 | ||||
-rw-r--r-- | client/go/cmd/prepare.go | 29 | ||||
-rw-r--r-- | client/go/cmd/query.go | 2 | ||||
-rw-r--r-- | client/go/cmd/status.go | 8 | ||||
-rw-r--r-- | client/go/cmd/target.go | 12 | ||||
-rw-r--r-- | client/go/cmd/testdata/A-Head-Full-of-Dreams-With-Put.json | 15 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 137 |
11 files changed, 253 insertions, 184 deletions
diff --git a/client/go/cmd/activate.go b/client/go/cmd/activate.go new file mode 100644 index 00000000000..fe989726253 --- /dev/null +++ b/client/go/cmd/activate.go @@ -0,0 +1,25 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa activate command +// Author: bratseth + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/vespa-engine/vespa/vespa" +) + +func init() { + rootCmd.AddCommand(activateCmd) +} + +// TODO: Implement and test + +var activateCmd = &cobra.Command{ + Use: "activate", + Short: "Activates (deploys) the previously prepared application package", + Long: `TODO`, + Run: func(cmd *cobra.Command, args []string) { + vespa.Deploy(true, "", deployTarget()) + }, +} diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go index bfd3837cd5a..0b0cd55abf9 100644 --- a/client/go/cmd/deploy.go +++ b/client/go/cmd/deploy.go @@ -5,187 +5,23 @@ package cmd import ( - "archive/zip" - "errors" "github.com/spf13/cobra" - "github.com/vespa-engine/vespa/util" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "time" + "github.com/vespa-engine/vespa/vespa" ) func init() { rootCmd.AddCommand(deployCmd) - deployCmd.AddCommand(deployPrepareCmd) - deployCmd.AddCommand(deployActivateCmd) } var deployCmd = &cobra.Command{ Use: "deploy", - Short: "Deploys an application package", - Long: `TODO: Use prepare or deploy activate`, - Run: func(cmd *cobra.Command, args []string) { - util.Error("Use either deploy prepare or deploy activate") - }, -} - -var deployPrepareCmd = &cobra.Command{ - Use: "prepare", - Short: "Prepares an application for activation", - Long: `TODO: prepare application-package-dir OR application.zip`, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) > 1 { - return errors.New("Expected an application package as the only argument") - } - return nil - }, + Short: "Deploys (prepares and activates) an application package", + Long: `TODO`, Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { - deploy(true, "src/main/application") + vespa.Deploy(false, "", deployTarget()) } else { - deploy(true, args[0]) + vespa.Deploy(false, args[0], deployTarget()) } }, } - -var deployActivateCmd = &cobra.Command{ - Use: "activate", - Short: "Activates an application package. If no package argument, the previously prepared package is activated.", - Long: `TODO: activate [application-package-dir OR application.zip]`, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) > 1 { - return errors.New("Expected an application package as the only argument") - } - return nil - }, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - deploy(false, "") - } else { - deploy(false, args[0]) - } - }, -} - -func deploy(prepare bool, application string) { - // TODO: Support no application (activate) - // TODO: Support application home as argument instead of src/main and - // - if target exists, use target/application.zip - // - else if src/main/application exists, use that - // - else if current dir has services.xml use that - if filepath.Ext(application) != ".zip" { - tempZip, error := ioutil.TempFile("", "application.zip") - if error != nil { - util.Error("Could not create a temporary zip file for the application package") - util.Detail(error.Error()) - return - } - - error = zipDir(application, tempZip.Name()) - if (error != nil) { - util.Error(error.Error()) - return - } - defer os.Remove(tempZip.Name()) - application = tempZip.Name() - } - - zipFileReader, zipFileError := os.Open(application) - if zipFileError != nil { - util.Error("Could not open application package at " + application) - util.Detail(zipFileError.Error()) - return - } - - var deployUrl *url.URL - if prepare { - deployUrl, _ = url.Parse(getTarget(deployContext).deploy + "/application/v2/tenant/default/prepare") - } else if application == "" { - deployUrl, _ = url.Parse(getTarget(deployContext).deploy + "/application/v2/tenant/default/activate") - } else { - deployUrl, _ = url.Parse(getTarget(deployContext).deploy + "/application/v2/tenant/default/prepareandactivate") - } - - header := http.Header{} - header.Add("Content-Type", "application/zip") - request := &http.Request{ - URL: deployUrl, - Method: "POST", - Header: header, - Body: ioutil.NopCloser(zipFileReader), - } - serviceDescription := "Deploy service" - response := util.HttpDo(request, time.Minute * 10, serviceDescription) - if (response == nil) { - return - } - - defer response.Body.Close() - if response.StatusCode == 200 { - util.Success("Success") - } else if response.StatusCode / 100 == 4 { - util.Error("Invalid application package", "(" + response.Status + "):") - util.PrintReader(response.Body) - } else { - util.Error("Error from", strings.ToLower(serviceDescription), "at", request.URL.Host, "(" + response.Status + "):") - util.PrintReader(response.Body) - } -} - -func zipDir(dir string, destination string) error { - if filepath.IsAbs(dir) { - message := "Path must be relative, but '" + dir + "'" - return errors.New(message) - } - if ! util.PathExists(dir) { - message := "'" + dir + "' should be an application package zip or dir, but does not exist" - return errors.New(message) - } - if ! util.IsDirectory(dir) { - message := "'" + dir + "' should be an application package dir, but is a (non-zip) file" - return errors.New(message) - } - - file, err := os.Create(destination) - if err != nil { - message := "Could not create a temporary zip file for the application package: " + err.Error() - return errors.New(message) - } - defer file.Close() - - w := zip.NewWriter(file) - defer w.Close() - - walker := func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - - zippath := strings.TrimPrefix(path, dir) - zipfile, err := w.Create(zippath) - if err != nil { - return err - } - - _, err = io.Copy(zipfile, file) - if err != nil { - return err - } - return nil - } - return filepath.Walk(dir, walker) -} - diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go index 64ede50546c..c80b18d0878 100644 --- a/client/go/cmd/deploy_test.go +++ b/client/go/cmd/deploy_test.go @@ -14,7 +14,7 @@ func TestDeployZip(t *testing.T) { client := &mockHttpClient{} assert.Equal(t, "\x1b[32mSuccess\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/application.zip"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/application.zip"}, []string{})) assertDeployRequestMade("http://127.0.0.1:19071", client, t) } @@ -22,7 +22,7 @@ func TestDeployZipWithURLTargetArgument(t *testing.T) { client := &mockHttpClient{} assert.Equal(t, "\x1b[32mSuccess\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/application.zip", "-t", "http://target:19071"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/application.zip", "-t", "http://target:19071"}, []string{})) assertDeployRequestMade("http://target:19071", client, t) } @@ -30,7 +30,7 @@ func TestDeployZipWitLocalTargetArgument(t *testing.T) { client := &mockHttpClient{} assert.Equal(t, "\x1b[32mSuccess\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/application.zip", "-t", "local"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/application.zip", "-t", "local"}, []string{})) assertDeployRequestMade("http://127.0.0.1:19071", client, t) } @@ -38,7 +38,7 @@ func TestDeployDirectory(t *testing.T) { client := &mockHttpClient{} assert.Equal(t, "\x1b[32mSuccess\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/src/main/application"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/src/main/application"}, []string{})) assertDeployRequestMade("http://127.0.0.1:19071", client, t) } @@ -67,12 +67,12 @@ func assertApplicationPackageError(t *testing.T, status int, errorMessage string client := &mockHttpClient{ nextStatus: status, nextBody: errorMessage, } assert.Equal(t, "\x1b[31mInvalid application package (Status " + strconv.Itoa(status) + "):\n" + errorMessage + "\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/src/main/application"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/src/main/application"}, []string{})) } func assertDeployServerError(t *testing.T, status int, errorMessage string) { client := &mockHttpClient{ nextStatus: status, nextBody: errorMessage, } assert.Equal(t, "\x1b[31mError from deploy service at 127.0.0.1:19071 (Status " + strconv.Itoa(status) + "):\n" + errorMessage + "\n", - executeCommand(t, client, []string{"deploy", "activate", "testdata/src/main/application"}, []string{})) + executeCommand(t, client, []string{"deploy", "testdata/src/main/application"}, []string{})) } diff --git a/client/go/cmd/document.go b/client/go/cmd/document.go index f2d7b80289d..9e51f3c8857 100644 --- a/client/go/cmd/document.go +++ b/client/go/cmd/document.go @@ -77,17 +77,17 @@ func post(documentId string, jsonFile string) { if documentId == "" { var doc map[string]interface{} json.Unmarshal(documentData, &doc) - documentId = doc["id"].(string) - if documentId == "" { + if doc["id"] != nil { + documentId = doc["id"].(string) + } else if doc["put"] != nil { documentId = doc["put"].(string) // document feeder format - } - if documentId == "" { + } else { util.Error("No document id given neither as argument or an 'id' key in the json file") return } } - url, _ := url.Parse(getTarget(documentContext).document + "/document/v1/" + documentId) + url, _ := url.Parse(documentTarget() + "/document/v1/" + documentId) request := &http.Request{ URL: url, diff --git a/client/go/cmd/document_test.go b/client/go/cmd/document_test.go index c3930247dcd..688e9f58080 100644 --- a/client/go/cmd/document_test.go +++ b/client/go/cmd/document_test.go @@ -27,6 +27,19 @@ func TestDocumentPostWithIdInDocumentShortForm(t *testing.T) { "mynamespace/music/docid/1", "testdata/A-Head-Full-of-Dreams-With-Id.json", t) } +func TestDocumentPostWithIdAsPutInDocument(t *testing.T) { + assertDocumentPost([]string{"document", "post", "testdata/A-Head-Full-of-Dreams-With-Put.json"}, + "mynamespace/music/docid/1", "testdata/A-Head-Full-of-Dreams-With-Put.json", t) +} + +func TestDocumentIdNotSpecified(t *testing.T) { + arguments := []string{"document", "post", "testdata/A-Head-Full-of-Dreams.json"} + client := &mockHttpClient{} + assert.Equal(t, + "\x1b[31mNo document id given neither as argument or an 'id' key in the json file\n", + executeCommand(t, client, arguments, []string{})) +} + func TestDocumentPostDocumentError(t *testing.T) { assertDocumentError(t, 401, "Document error") } diff --git a/client/go/cmd/prepare.go b/client/go/cmd/prepare.go new file mode 100644 index 00000000000..b65f845be81 --- /dev/null +++ b/client/go/cmd/prepare.go @@ -0,0 +1,29 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa prepare command +// Author: bratseth + +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/vespa-engine/vespa/vespa" +) + +func init() { + rootCmd.AddCommand(prepareCmd) +} + +// TODO: Implement and test + +var prepareCmd = &cobra.Command{ + Use: "prepare", + Short: "Prepares an application package for activation", + Long: `TODO`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + vespa.Deploy(true, "", deployTarget()) + } else { + vespa.Deploy(true, args[0], deployTarget()) + } + }, +} diff --git a/client/go/cmd/query.go b/client/go/cmd/query.go index 58828dbb7cf..ed412d4a396 100644 --- a/client/go/cmd/query.go +++ b/client/go/cmd/query.go @@ -35,7 +35,7 @@ var queryCmd = &cobra.Command{ } func query(arguments []string) { - url, _ := url.Parse(getTarget(queryContext).query + "/search/") + url, _ := url.Parse(queryTarget() + "/search/") urlQuery := url.Query() for i := 0; i < len(arguments); i++ { key, value := splitArg(arguments[i]) diff --git a/client/go/cmd/status.go b/client/go/cmd/status.go index a011678be09..4187c34c2b1 100644 --- a/client/go/cmd/status.go +++ b/client/go/cmd/status.go @@ -15,12 +15,14 @@ func init() { statusCmd.AddCommand(statusConfigServerCmd) } +// TODO: Use deploy, query and document instead of container and config-server + var statusCmd = &cobra.Command{ Use: "status", Short: "Verifies that a vespa target is ready to use (container by default)", Long: `TODO`, Run: func(cmd *cobra.Command, args []string) { - status(getTarget(queryContext).query, "Container") + status(queryTarget(), "Container") }, } @@ -29,7 +31,7 @@ var statusContainerCmd = &cobra.Command{ Short: "Verifies that your Vespa container endpoint is ready [Default]", Long: `TODO`, Run: func(cmd *cobra.Command, args []string) { - status(getTarget(queryContext).query, "Container") + status(queryTarget(), "Container") }, } @@ -38,7 +40,7 @@ var statusConfigServerCmd = &cobra.Command{ Short: "Verifies that your Vespa config server endpoint is ready", Long: `TODO`, Run: func(cmd *cobra.Command, args []string) { - status(getTarget(deployContext).deploy, "Config server") + status(deployTarget(), "Config server") }, } diff --git a/client/go/cmd/target.go b/client/go/cmd/target.go index 4c1ead7f74e..48ce71bba41 100644 --- a/client/go/cmd/target.go +++ b/client/go/cmd/target.go @@ -22,6 +22,18 @@ const ( documentContext context = 2 ) +func deployTarget() string { + return getTarget(deployContext).deploy +} + +func queryTarget() string { + return getTarget(queryContext).query +} + +func documentTarget() string { + return getTarget(documentContext).document +} + func getTarget(targetContext context) *target { if strings.HasPrefix(targetArgument, "http") { // TODO: Add default ports if missing diff --git a/client/go/cmd/testdata/A-Head-Full-of-Dreams-With-Put.json b/client/go/cmd/testdata/A-Head-Full-of-Dreams-With-Put.json new file mode 100644 index 00000000000..d6467073816 --- /dev/null +++ b/client/go/cmd/testdata/A-Head-Full-of-Dreams-With-Put.json @@ -0,0 +1,15 @@ +{ + "put": "mynamespace/music/docid/1", + "fields": { + "album": "A Head Full of Dreams", + "artist": "Coldplay", + "year": 2015, + "category_scores": { + "cells": [ + { "address" : { "cat" : "pop" }, "value": 1 }, + { "address" : { "cat" : "rock" }, "value": 0.2 }, + { "address" : { "cat" : "jazz" }, "value": 0 } + ] + } + } +} diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go new file mode 100644 index 00000000000..3dd3fc21993 --- /dev/null +++ b/client/go/vespa/deploy.go @@ -0,0 +1,137 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa deploy API +// Author: bratseth + +package vespa + +import ( + "archive/zip" + "errors" + "github.com/vespa-engine/vespa/util" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" +) + +func Deploy(prepare bool, application string, target string) { + // TODO: Support no application (activate) + // TODO: Support application home as argument instead of src/main and + // - if target exists, use target/application.zip + // - else if src/main/application exists, use that + // - else if current dir has services.xml use that + if filepath.Ext(application) != ".zip" { + tempZip, error := ioutil.TempFile("", "application.zip") + if error != nil { + util.Error("Could not create a temporary zip file for the application package") + util.Detail(error.Error()) + return + } + + error = zipDir(application, tempZip.Name()) + if (error != nil) { + util.Error(error.Error()) + return + } + defer os.Remove(tempZip.Name()) + application = tempZip.Name() + } + + zipFileReader, zipFileError := os.Open(application) + if zipFileError != nil { + util.Error("Could not open application package at " + application) + util.Detail(zipFileError.Error()) + return + } + + var deployUrl *url.URL + if prepare { + deployUrl, _ = url.Parse(target + "/application/v2/tenant/default/prepare") + } else if application == "" { + deployUrl, _ = url.Parse(target + "/application/v2/tenant/default/activate") + } else { + deployUrl, _ = url.Parse(target + "/application/v2/tenant/default/prepareandactivate") + } + + header := http.Header{} + header.Add("Content-Type", "application/zip") + request := &http.Request{ + URL: deployUrl, + Method: "POST", + Header: header, + Body: ioutil.NopCloser(zipFileReader), + } + serviceDescription := "Deploy service" + response := util.HttpDo(request, time.Minute * 10, serviceDescription) + if (response == nil) { + return + } + + defer response.Body.Close() + if response.StatusCode == 200 { + util.Success("Success") + } else if response.StatusCode / 100 == 4 { + util.Error("Invalid application package", "(" + response.Status + "):") + util.PrintReader(response.Body) + } else { + util.Error("Error from", strings.ToLower(serviceDescription), "at", request.URL.Host, "(" + response.Status + "):") + util.PrintReader(response.Body) + } +} + +func zipDir(dir string, destination string) error { + if filepath.IsAbs(dir) { + message := "Path must be relative, but '" + dir + "'" + return errors.New(message) + } + if ! util.PathExists(dir) { + message := "'" + dir + "' should be an application package zip or dir, but does not exist" + return errors.New(message) + } + if ! util.IsDirectory(dir) { + message := "'" + dir + "' should be an application package dir, but is a (non-zip) file" + return errors.New(message) + } + + file, err := os.Create(destination) + if err != nil { + message := "Could not create a temporary zip file for the application package: " + err.Error() + return errors.New(message) + } + defer file.Close() + + w := zip.NewWriter(file) + defer w.Close() + + walker := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + zippath := strings.TrimPrefix(path, dir) + zipfile, err := w.Create(zippath) + if err != nil { + return err + } + + _, err = io.Copy(zipfile, file) + if err != nil { + return err + } + return nil + } + return filepath.Walk(dir, walker) +} + |