diff options
-rw-r--r-- | client/go/cmd/clone.go | 16 | ||||
-rw-r--r-- | client/go/cmd/clone_test.go | 3 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 4 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 61 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 57 | ||||
-rw-r--r-- | client/go/cmd/document.go | 2 | ||||
-rw-r--r-- | client/go/cmd/print.go | 2 | ||||
-rw-r--r-- | client/go/cmd/query.go | 2 | ||||
-rw-r--r-- | client/go/cmd/status.go | 8 | ||||
-rw-r--r-- | client/go/util/io.go | 2 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 99 |
11 files changed, 189 insertions, 67 deletions
diff --git a/client/go/cmd/clone.go b/client/go/cmd/clone.go index 8d4b382db76..9604476df4e 100644 --- a/client/go/cmd/clone.go +++ b/client/go/cmd/clone.go @@ -52,7 +52,7 @@ func cloneApplication(source string, name string) { zipReader, zipOpenError := zip.OpenReader(zipFile.Name()) if zipOpenError != nil { log.Print(color.Red("Error: "), "Could not open sample apps zip '", color.Cyan(zipFile.Name()), "'") - log.Print(color.Brown(zipOpenError)) + log.Print(color.Yellow(zipOpenError)) } defer zipReader.Close() @@ -64,7 +64,7 @@ func cloneApplication(source string, name string) { createErr := os.Mkdir(name, 0755) if createErr != nil { log.Print(color.Red("Error: "), "Could not create directory '", color.Cyan(name), "'") - log.Print(color.Brown(createErr)) + log.Print(color.Yellow(createErr)) return } } @@ -73,7 +73,7 @@ func cloneApplication(source string, name string) { copyError := copy(f, name, zipEntryPrefix) if copyError != nil { log.Print(color.Red("Error: "), "Could not copy zip entry '", color.Cyan(f.Name), "' to ", color.Cyan(name)) - log.Print(color.Brown(copyError)) + log.Print(color.Yellow(copyError)) return } } @@ -90,13 +90,13 @@ func getSampleAppsZip() *os.File { existing, openExistingError := os.Open(existingSampleAppsZip) if openExistingError != nil { log.Print(color.Red("Error: "), "Could not open existing sample apps zip file '", color.Cyan(existingSampleAppsZip), "'") - log.Print(color.Brown(openExistingError)) + log.Print(color.Yellow(openExistingError)) } return existing } // TODO: Cache it? - log.Print(color.Brown("Downloading sample apps ...")) // TODO: Spawn thread to indicate progress + log.Print(color.Yellow("Downloading sample apps ...")) // TODO: Spawn thread to indicate progress zipUrl, _ := url.Parse("https://github.com/vespa-engine/sample-apps/archive/refs/heads/master.zip") request := &http.Request{ URL: zipUrl, @@ -105,7 +105,7 @@ func getSampleAppsZip() *os.File { response, reqErr := util.HttpDo(request, time.Minute*60, "GitHub") if reqErr != nil { log.Print(color.Red("Error: "), "Could not download sample apps from github") - log.Print(color.Brown(reqErr)) + log.Print(color.Yellow(reqErr)) return nil } defer response.Body.Close() @@ -117,14 +117,14 @@ func getSampleAppsZip() *os.File { destination, tempFileError := ioutil.TempFile("", "prefix") if tempFileError != nil { log.Print(color.Red("Error: "), "Could not create a temp file to hold sample apps") - log.Print(color.Brown(tempFileError)) + log.Print(color.Yellow(tempFileError)) } // destination, _ := os.Create("./" + name + "/sample-apps.zip") // defer destination.Close() _, err := io.Copy(destination, response.Body) if err != nil { log.Print(color.Red("Error: "), "Could not download sample apps from GitHub") - log.Print(color.Brown(err)) + log.Print(color.Yellow(err)) return nil } return destination diff --git a/client/go/cmd/clone_test.go b/client/go/cmd/clone_test.go index a98fed694fe..313ad849038 100644 --- a/client/go/cmd/clone_test.go +++ b/client/go/cmd/clone_test.go @@ -27,7 +27,6 @@ func assertCreated(sampleAppName string, app string, t *testing.T) { assert.True(t, util.IsDirectory(filepath.Join(app, "src", "main", "application"))) servicesStat, _ := os.Stat(filepath.Join(app, "src", "main", "application", "services.xml")) - var servicesSize int64 - servicesSize = 2474 + servicesSize := int64(2474) assert.Equal(t, servicesSize, servicesStat.Size()) } diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go index 074089b399d..f40594c288e 100644 --- a/client/go/cmd/command_tester.go +++ b/client/go/cmd/command_tester.go @@ -78,6 +78,9 @@ type mockHttpClient struct { // A recording of the last HTTP request made through this lastRequest *http.Request + + // All requests made through this + requests []*http.Request } func (c *mockHttpClient) Do(request *http.Request, timeout time.Duration) (response *http.Response, error error) { @@ -85,6 +88,7 @@ func (c *mockHttpClient) Do(request *http.Request, timeout time.Duration) (respo c.nextStatus = 200 } c.lastRequest = request + c.requests = append(c.requests, request) return &http.Response{ Status: "Status " + strconv.Itoa(c.nextStatus), StatusCode: c.nextStatus, diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go index 4d8caacce22..0dffe42cc5c 100644 --- a/client/go/cmd/deploy.go +++ b/client/go/cmd/deploy.go @@ -9,6 +9,8 @@ import ( "log" "os" "path/filepath" + "strconv" + "strings" "github.com/spf13/cobra" "github.com/vespa-engine/vespa/vespa" @@ -64,15 +66,14 @@ var deployCmd = &cobra.Command{ if configDir == "" { return } - d.APIKey = loadApiKey(configDir, d.Application.Tenant) + d.APIKey = readAPIKey(configDir, d.Application.Tenant) if d.APIKey == nil { printErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") return } } - resolvedSrc, err := vespa.Deploy(d) - if err == nil { - printSuccess("Deployed ", color.Cyan(resolvedSrc)) + if err := vespa.Deploy(d); err == nil { + printSuccess("Deployed ", color.Cyan(pkg.Path)) if d.IsCloud() { log.Print("See ", color.Cyan(fmt.Sprintf("https://console.vespa.oath.cloud/tenant/%s/application/%s/dev/instance/%s", d.Application.Tenant, d.Application.Application, d.Application.Instance)), " for deployment status") } @@ -92,9 +93,21 @@ var prepareCmd = &cobra.Command{ printErr(err, "Could not find application package") return } - resolvedSrc, err := vespa.Prepare(vespa.Deployment{ApplicationPackage: pkg}) + configDir := configDir("default") + if configDir == "" { + return + } + sessionID, err := vespa.Prepare(vespa.Deployment{ + ApplicationPackage: pkg, + TargetType: getTargetType(), + TargetURL: deployTarget(), + }) if err == nil { - printSuccess("Prepared ", color.Cyan(resolvedSrc)) + if err := writeSessionID(configDir, sessionID); err != nil { + printErr(err, "Could not write session ID") + return + } + printSuccess("Prepared ", color.Cyan(pkg.Path), " with session ", sessionID) } else { printErr(nil, err.Error()) } @@ -111,16 +124,46 @@ var activateCmd = &cobra.Command{ printErr(err, "Could not find application package") return } - resolvedSrc, err := vespa.Activate(vespa.Deployment{ApplicationPackage: pkg}) + configDir := configDir("default") + if configDir == "" { + return + } + sessionID, err := readSessionID(configDir) + if err != nil { + printErr(err, "Could not read session ID") + return + } + err = vespa.Activate(sessionID, vespa.Deployment{ + ApplicationPackage: pkg, + TargetType: getTargetType(), + TargetURL: deployTarget(), + }) if err == nil { - printSuccess("Activated ", color.Cyan(resolvedSrc)) + printSuccess("Activated ", color.Cyan(pkg.Path), " with session ", sessionID) } else { printErr(nil, err.Error()) } }, } -func loadApiKey(configDir, tenant string) []byte { +func writeSessionID(appConfigDir string, sessionID int64) error { + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return err + } + return os.WriteFile(sessionIDFile(appConfigDir), []byte(fmt.Sprintf("%d\n", sessionID)), 0600) +} + +func readSessionID(appConfigDir string) (int64, error) { + b, err := os.ReadFile(sessionIDFile(appConfigDir)) + if err != nil { + return 0, err + } + return strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) +} + +func sessionIDFile(appConfigDir string) string { return filepath.Join(appConfigDir, "session_id") } + +func readAPIKey(configDir, tenant string) []byte { apiKeyPath := filepath.Join(configDir, tenant+".api-key.pem") key, err := os.ReadFile(apiKeyPath) if err != nil { diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go index d4fddf11f99..0a8db5f4fd2 100644 --- a/client/go/cmd/deploy_test.go +++ b/client/go/cmd/deploy_test.go @@ -5,12 +5,23 @@ package cmd import ( + "path/filepath" "strconv" "testing" "github.com/stretchr/testify/assert" ) +func TestPrepareZip(t *testing.T) { + assertPrepare("testdata/applications/withTarget/target/application.zip", + []string{"prepare", "testdata/applications/withTarget/target/application.zip"}, t) +} + +func TestActivateZip(t *testing.T) { + assertActivate("testdata/applications/withTarget/target/application.zip", + []string{"activate", "testdata/applications/withTarget/target/application.zip"}, t) +} + func TestDeployZip(t *testing.T) { assertDeploy("testdata/applications/withTarget/target/application.zip", []string{"deploy", "testdata/applications/withTarget/target/application.zip"}, t) @@ -89,17 +100,53 @@ func assertDeploy(applicationPackage string, arguments []string, t *testing.T) { assertDeployRequestMade("http://127.0.0.1:19071", client, t) } -func assertDeployRequestMade(target string, client *mockHttpClient, t *testing.T) { - assert.Equal(t, target+"/application/v2/tenant/default/prepareandactivate", client.lastRequest.URL.String()) - assert.Equal(t, "application/zip", client.lastRequest.Header.Get("Content-Type")) - assert.Equal(t, "POST", client.lastRequest.Method) - var body = client.lastRequest.Body +func assertPrepare(applicationPackage string, arguments []string, t *testing.T) { + client := &mockHttpClient{} + client.nextBody = `{"session-id":"42"}` + assert.Equal(t, + "Success: Prepared "+applicationPackage+" with session 42\n", + executeCommand(t, client, arguments, []string{})) + + assertPackageUpload(0, "http://127.0.0.1:19071/application/v2/tenant/default/session", client, t) + sessionURL := "http://127.0.0.1:19071/application/v2/tenant/default/session/42/prepared" + assert.Equal(t, sessionURL, client.requests[1].URL.String()) + assert.Equal(t, "PUT", client.requests[1].Method) +} + +func assertActivate(applicationPackage string, arguments []string, t *testing.T) { + client := &mockHttpClient{} + configDir := t.TempDir() + appConfigDir := filepath.Join(configDir, ".vespa", "default") + if err := writeSessionID(appConfigDir, 42); err != nil { + t.Fatal(err) + } + assert.Equal(t, + "Success: Activated "+applicationPackage+" with session 42\n", + execute(command{args: arguments, configDir: configDir}, t, client)) + url := "http://127.0.0.1:19071/application/v2/tenant/default/session/42/active" + assert.Equal(t, url, client.lastRequest.URL.String()) + assert.Equal(t, "PUT", client.lastRequest.Method) +} + +func assertPackageUpload(requestNumber int, url string, client *mockHttpClient, t *testing.T) { + req := client.lastRequest + if requestNumber >= 0 { + req = client.requests[requestNumber] + } + assert.Equal(t, url, req.URL.String()) + assert.Equal(t, "application/zip", req.Header.Get("Content-Type")) + assert.Equal(t, "POST", req.Method) + var body = req.Body assert.NotNil(t, body) buf := make([]byte, 7) // Just check the first few bytes body.Read(buf) assert.Equal(t, "PK\x03\x04\x14\x00\b", string(buf)) } +func assertDeployRequestMade(target string, client *mockHttpClient, t *testing.T) { + assertPackageUpload(-1, target+"/application/v2/tenant/default/prepareandactivate", client, t) +} + func assertApplicationPackageError(t *testing.T, status int, expectedMessage string, returnBody string) { client := &mockHttpClient{nextStatus: status, nextBody: returnBody} assert.Equal(t, diff --git a/client/go/cmd/document.go b/client/go/cmd/document.go index a709da222bd..4819d89fb7e 100644 --- a/client/go/cmd/document.go +++ b/client/go/cmd/document.go @@ -107,7 +107,7 @@ func printResult(result util.OperationResult, payloadOnlyOnSuccess bool) { } if result.Detail != "" { - log.Print(color.Brown(result.Detail)) + log.Print(color.Yellow(result.Detail)) } if result.Payload != "" { diff --git a/client/go/cmd/print.go b/client/go/cmd/print.go index 55d74536393..b6394dbe340 100644 --- a/client/go/cmd/print.go +++ b/client/go/cmd/print.go @@ -19,7 +19,7 @@ func printErr(err error, msg ...interface{}) { log.Print(color.Red("Error: "), fmt.Sprint(msg...)) } if err != nil { - log.Print(color.Brown(err)) + log.Print(color.Yellow(err)) } } diff --git a/client/go/cmd/query.go b/client/go/cmd/query.go index 5143dcf6f36..e49b51b79b8 100644 --- a/client/go/cmd/query.go +++ b/client/go/cmd/query.go @@ -62,6 +62,6 @@ func splitArg(argument string) (string, string) { if equalsIndex < 1 { return "yql", argument } else { - return argument[0:equalsIndex], argument[equalsIndex+1 : len(argument)] + return argument[0:equalsIndex], argument[equalsIndex+1:] } } diff --git a/client/go/cmd/status.go b/client/go/cmd/status.go index 4d0b44ee826..92e86573198 100644 --- a/client/go/cmd/status.go +++ b/client/go/cmd/status.go @@ -29,7 +29,7 @@ var statusCmd = &cobra.Command{ var statusQueryCmd = &cobra.Command{ Use: "query", - Short: "Verify that your Vespa query API container endpoint is ready [Default]", + Short: "Verify that your Vespa query API container endpoint is ready (default)", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { status(queryTarget(), "Query API") @@ -38,7 +38,7 @@ var statusQueryCmd = &cobra.Command{ var statusDocumentCmd = &cobra.Command{ Use: "document", - Short: "Verify that your Vespa document API container endpoint is ready [Default]", + Short: "Verify that your Vespa document API container endpoint is ready", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { status(documentTarget(), "Document API") @@ -59,14 +59,14 @@ func status(target string, description string) { response, err := util.HttpGet(target, path, description) if err != nil { log.Print(description, " at ", color.Cyan(target), " is ", color.Red("not ready")) - log.Print(color.Brown(err)) + log.Print(color.Yellow(err)) return } defer response.Body.Close() if response.StatusCode != 200 { log.Print(description, " at ", color.Cyan(target), " is ", color.Red("not ready")) - log.Print(color.Brown(response.Status)) + log.Print(color.Yellow(response.Status)) } else { log.Print(description, " at ", color.Cyan(target), " is ", color.Green("ready")) } diff --git a/client/go/util/io.go b/client/go/util/io.go index 5ce9708ed7a..51361e344f0 100644 --- a/client/go/util/io.go +++ b/client/go/util/io.go @@ -47,5 +47,5 @@ func ReaderToJSON(reader io.Reader) string { if parseError != nil { // Not JSON: Print plainly return string(bodyBytes) } - return string(prettyJSON.Bytes()) + return prettyJSON.String() } diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go index d81b8decdde..2bebeb92e0c 100644 --- a/client/go/vespa/deploy.go +++ b/client/go/vespa/deploy.go @@ -16,6 +16,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "time" @@ -127,43 +128,69 @@ func ZoneFromString(s string) (ZoneID, error) { return ZoneID{Environment: parts[0], Region: parts[1]}, nil } -func Prepare(deployment Deployment) (string, error) { +// Prepare deployment and return the session ID +func Prepare(deployment Deployment) (int64, error) { if deployment.IsCloud() { - return "", fmt.Errorf("%s: prepare is not supported", deployment) + return 0, fmt.Errorf("%s: prepare is not supported", deployment) } - // TODO: Save session id in .vespa - // https://docs.vespa.ai/en/cloudconfig/deploy-rest-api-v2.html - u, err := url.Parse(deployment.TargetURL + "/application/v2/tenant/default/prepare") + sessionURL, err := url.Parse(deployment.TargetURL + "/application/v2/tenant/default/session") if err != nil { - return "", err + return 0, err } - return deploy(u, deployment) + sessionID, err := uploadApplicationPackage(sessionURL, deployment) + if err != nil { + return 0, err + } + prepareURL, err := url.Parse(fmt.Sprintf("%s/application/v2/tenant/default/session/%d/prepared", deployment.TargetURL, sessionID)) + if err != nil { + return 0, err + } + req, err := http.NewRequest("PUT", prepareURL.String(), nil) + if err != nil { + return 0, err + } + serviceDescription := "Deploy service" + response, err := util.HttpDo(req, time.Second*30, serviceDescription) + if err != nil { + return 0, err + } + defer response.Body.Close() + return sessionID, nil } -func Activate(deployment Deployment) (string, error) { +// Activate deployment with sessionID from a past prepare +func Activate(sessionID int64, deployment Deployment) error { if deployment.IsCloud() { - return "", fmt.Errorf("%s: activate is not supported", deployment) + return fmt.Errorf("%s: activate is not supported", deployment) + } + u, err := url.Parse(fmt.Sprintf("%s/application/v2/tenant/default/session/%d/active", deployment.TargetURL, sessionID)) + if err != nil { + return err + } + req, err := http.NewRequest("PUT", u.String(), nil) + if err != nil { + return err } - // TODO: Look up session id in .vespa - // https://docs.vespa.ai/en/cloudconfig/deploy-rest-api-v2.html - u, err := url.Parse(deployment.TargetURL + "/application/v2/tenant/default/activate") + serviceDescription := "Deploy service" + response, err := util.HttpDo(req, time.Second*30, serviceDescription) if err != nil { - return "", err + return err } - return deploy(u, deployment) + defer response.Body.Close() + return nil } -func Deploy(deployment Deployment) (string, error) { +func Deploy(deployment Deployment) error { path := "/application/v2/tenant/default/prepareandactivate" if deployment.IsCloud() { if !deployment.ApplicationPackage.HasCertificate() { - return "", fmt.Errorf("%s: missing certificate in package", deployment) + return fmt.Errorf("%s: missing certificate in package", deployment) } if deployment.APIKey == nil { - return "", fmt.Errorf("%s: missing api key", deployment.String()) + return fmt.Errorf("%s: missing api key", deployment.String()) } if deployment.Zone.Environment == "" || deployment.Zone.Region == "" { - return "", fmt.Errorf("%s: missing zone", deployment) + return fmt.Errorf("%s: missing zone", deployment) } path = fmt.Sprintf("/application/v4/tenant/%s/application/%s/instance/%s/deploy/%s-%s", deployment.Application.Tenant, @@ -174,23 +201,17 @@ func Deploy(deployment Deployment) (string, error) { } u, err := url.Parse(deployment.TargetURL + path) if err != nil { - return "", err + return err } - return deploy(u, deployment) + _, err = uploadApplicationPackage(u, deployment) + return err } -func deploy(url *url.URL, deployment Deployment) (string, error) { +func uploadApplicationPackage(url *url.URL, deployment Deployment) (int64, error) { zipReader, err := deployment.ApplicationPackage.zipReader() if err != nil { - return "", err + return 0, err } - if err := postApplicationPackage(url, zipReader, deployment); err != nil { - return "", err - } - return deployment.ApplicationPackage.Path, nil -} - -func postApplicationPackage(url *url.URL, zipReader io.Reader, deployment Deployment) error { header := http.Header{} header.Add("Content-Type", "application/zip") request := &http.Request{ @@ -202,22 +223,30 @@ func postApplicationPackage(url *url.URL, zipReader io.Reader, deployment Deploy if deployment.APIKey != nil { signer := NewRequestSigner(deployment.Application.SerializedForm(), deployment.APIKey) if err := signer.SignRequest(request); err != nil { - return err + return 0, err } } serviceDescription := "Deploy service" response, err := util.HttpDo(request, time.Minute*10, serviceDescription) if err != nil { - return err + return 0, err } defer response.Body.Close() + var sessionResponse struct { + SessionID string `json:"session-id"` + } if response.StatusCode/100 == 4 { - return fmt.Errorf("Invalid application package (%s)\n\n%s", response.Status, extractError(response.Body)) + return 0, fmt.Errorf("Invalid application package (%s)\n\n%s", response.Status, extractError(response.Body)) } else if response.StatusCode != 200 { - return fmt.Errorf("Error from %s at %s (%s):\n%s", strings.ToLower(serviceDescription), request.URL.Host, response.Status, util.ReaderToJSON(response.Body)) + return 0, fmt.Errorf("Error from %s at %s (%s):\n%s", strings.ToLower(serviceDescription), request.URL.Host, response.Status, util.ReaderToJSON(response.Body)) + } else { + jsonDec := json.NewDecoder(response.Body) + if err := jsonDec.Decode(&sessionResponse); err != nil { + sessionResponse.SessionID = "0" // No JSON in response + } } - return nil + return strconv.ParseInt(sessionResponse.SessionID, 10, 64) } func isZip(filename string) bool { return filepath.Ext(filename) == ".zip" } @@ -287,6 +316,6 @@ func extractError(reader io.Reader) string { if parseError != nil { // Not JSON: Print plainly return string(responseData) } - return string(prettyJSON.Bytes()) + return prettyJSON.String() } } |