diff options
Diffstat (limited to 'client')
-rw-r--r-- | client/go/cmd/api_key.go | 2 | ||||
-rw-r--r-- | client/go/cmd/api_key_test.go | 12 | ||||
-rw-r--r-- | client/go/cmd/cert_test.go | 8 | ||||
-rw-r--r-- | client/go/cmd/clone.go | 99 | ||||
-rw-r--r-- | client/go/cmd/clone_test.go | 11 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 13 | ||||
-rw-r--r-- | client/go/cmd/config.go | 24 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 3 | ||||
-rw-r--r-- | client/go/cmd/curl_test.go | 7 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 8 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 2 | ||||
-rw-r--r-- | client/go/cmd/document_test.go | 5 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 61 | ||||
-rw-r--r-- | client/go/cmd/query_test.go | 3 | ||||
-rw-r--r-- | client/go/cmd/status_test.go | 5 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 2 | ||||
-rw-r--r-- | client/go/vespa/target.go | 67 | ||||
-rw-r--r-- | client/go/vespa/target_test.go | 18 |
18 files changed, 212 insertions, 138 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go index a838f1a05c8..b3284daa993 100644 --- a/client/go/cmd/api_key.go +++ b/client/go/cmd/api_key.go @@ -77,6 +77,6 @@ func printPublicKey(apiKeyFile, tenant string) { log.Printf("\nThis is your public key:\n%s", color.Green(pemPublicKey)) log.Printf("Its fingerprint is:\n%s\n", color.Cyan(fingerprint)) log.Print("\nTo use this key in Vespa Cloud click 'Add custom key' at") - log.Printf(color.Cyan("%s/tenant/%s/keys").String(), defaultConsoleURL, tenant) + log.Printf(color.Cyan("%s/tenant/%s/keys").String(), getConsoleURL(), tenant) log.Print("and paste the entire public key including the BEGIN and END lines.") } diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go index 2497568604f..1deb628c21e 100644 --- a/client/go/cmd/api_key_test.go +++ b/client/go/cmd/api_key_test.go @@ -4,20 +4,20 @@ package cmd import ( - "strings" + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestAPIKey(t *testing.T) { - homeDir := t.TempDir() - keyFile := homeDir + "/.vespa/t1.api-key.pem" + homeDir := filepath.Join(t.TempDir(), ".vespa") + keyFile := filepath.Join(homeDir, "t1.api-key.pem") out, _ := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) - assert.True(t, strings.HasPrefix(out, "Success: API private key written to "+keyFile+"\n")) + assert.Contains(t, out, "Success: API private key written to "+keyFile+"\n") out, _ = execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) - assert.True(t, strings.HasPrefix(out, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n")) - assert.True(t, strings.Contains(out, "This is your public key")) + assert.Contains(t, out, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n") + assert.Contains(t, out, "This is your public key") } diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go index d93def2fa70..cd5f88764b9 100644 --- a/client/go/cmd/cert_test.go +++ b/client/go/cmd/cert_test.go @@ -14,7 +14,7 @@ import ( ) func TestCert(t *testing.T) { - homeDir := t.TempDir() + homeDir := filepath.Join(t.TempDir(), ".vespa") pkgDir := mockApplicationPackage(t, false) out, _ := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) @@ -23,8 +23,8 @@ func TestCert(t *testing.T) { appDir := filepath.Join(pkgDir, "src", "main", "application") pkgCertificate := filepath.Join(appDir, "security", "clients.pem") - certificate := filepath.Join(homeDir, ".vespa", app.String(), "data-plane-public-cert.pem") - privateKey := filepath.Join(homeDir, ".vespa", app.String(), "data-plane-private-key.pem") + 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), out) @@ -33,7 +33,7 @@ func TestCert(t *testing.T) { } func TestCertCompressedPackage(t *testing.T) { - homeDir := t.TempDir() + homeDir := filepath.Join(t.TempDir(), ".vespa") pkgDir := mockApplicationPackage(t, true) zipFile := filepath.Join(pkgDir, "target", "application.zip") err := os.MkdirAll(filepath.Dir(zipFile), 0755) diff --git a/client/go/cmd/clone.go b/client/go/cmd/clone.go index 9503a81debf..508ad49438f 100644 --- a/client/go/cmd/clone.go +++ b/client/go/cmd/clone.go @@ -6,11 +6,10 @@ package cmd import ( "archive/zip" + "errors" "io" - "io/ioutil" "log" "net/http" - "net/url" "os" "path/filepath" "strings" @@ -20,22 +19,29 @@ import ( "github.com/vespa-engine/vespa/client/go/util" ) -// Set this to test without downloading this file from github -var existingSampleAppsZip string +const sampleAppsCacheTTL = time.Hour * 168 // 1 week + var listApps bool +var forceClone bool func init() { rootCmd.AddCommand(cloneCmd) cloneCmd.Flags().BoolVarP(&listApps, "list", "l", false, "List available sample applications") + cloneCmd.Flags().BoolVarP(&forceClone, "force", "f", false, "Ignore cache and force downloading the latest sample application from GitHub") } var cloneCmd = &cobra.Command{ - // TODO: "application" and "list" subcommands? Use: "clone sample-application-path target-directory", Short: "Create files and directory structure for a new Vespa application from a sample application", - Long: `Creates an application package file structure. + Long: `Create files and directory structure for a new Vespa application +from a sample application. + +Sample applications are downloaded from +https://github.com/vespa-engine/sample-apps. -The application package is copied from a sample application in https://github.com/vespa-engine/sample-apps`, +By default sample applications are cached in the user's cache directory. This +directory can be overriden by setting the VESPA_CLI_CACHE_DIR environment +variable.`, Example: "$ vespa clone vespa-cloud/album-recommendation my-app", DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { @@ -60,12 +66,7 @@ The application package is copied from a sample application in https://github.co func cloneApplication(source string, name string) { zipFile := getSampleAppsZip() - if zipFile == nil { - return - } - if existingSampleAppsZip == "" { // Indicates we created a temp file now - defer os.Remove(zipFile.Name()) - } + defer zipFile.Close() zipReader, zipOpenError := zip.OpenReader(zipFile.Name()) if zipOpenError != nil { @@ -101,45 +102,67 @@ func cloneApplication(source string, name string) { } } +func openOutputFile() (*os.File, error) { + cacheDir, err := vespaCliCacheDir() + if err != nil { + return nil, err + } + cacheFile := filepath.Join(cacheDir, "sample-apps-master.zip") + return os.OpenFile(cacheFile, os.O_RDWR|os.O_CREATE, 0755) +} + +func useCache(cacheFile *os.File) (bool, error) { + if forceClone { + return false, nil + } + stat, err := cacheFile.Stat() + if errors.Is(err, os.ErrNotExist) { + return false, nil + } else if err != nil { + return false, err + } + expiry := stat.ModTime().Add(sampleAppsCacheTTL) + return stat.Size() > 0 && time.Now().Before(expiry), nil +} + func getSampleAppsZip() *os.File { - if existingSampleAppsZip != "" { - existing, openExistingError := os.Open(existingSampleAppsZip) - if openExistingError != nil { - printErr(openExistingError, "Could not open existing sample apps zip file '", color.Cyan(existingSampleAppsZip), "'") - } - return existing + f, err := openOutputFile() + if err != nil { + fatalErr(err, "Could not determine location of cache file") + return nil + } + useCache, err := useCache(f) + if err != nil { + fatalErr(err, "Could not determine cache status", "Try ignoring the cache with the -f flag") + return nil + } + if useCache { + log.Print(color.Yellow("Using cached sample apps ...")) + return f } - // TODO: Cache it? 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, - Method: "GET", + request, err := http.NewRequest("GET", "https://github.com/vespa-engine/sample-apps/archive/refs/heads/master.zip", nil) + if err != nil { + fatalErr(err, "Invalid URL") + return nil } - response, reqErr := util.HttpDo(request, time.Minute*60, "GitHub") - if reqErr != nil { - printErr(reqErr, "Could not download sample apps from GitHub") + response, err := util.HttpDo(request, time.Minute*60, "GitHub") + if err != nil { + fatalErr(err, "Could not download sample apps from GitHub") return nil } defer response.Body.Close() if response.StatusCode != 200 { - printErr(nil, "Could not download sample apps from GitHub: ", response.StatusCode) + fatalErr(nil, "Could not download sample apps from GitHub: ", response.StatusCode) return nil } - destination, tempFileError := ioutil.TempFile("", "prefix") - if tempFileError != nil { - printErr(tempFileError, "Could not create a temporary file to hold sample apps") - } - // destination, _ := os.Create("./" + name + "/sample-apps.zip") - // defer destination.Close() - _, err := io.Copy(destination, response.Body) - if err != nil { - printErr(err, "Could not download sample apps from GitHub") + if _, err := io.Copy(f, response.Body); err != nil { + fatalErr(err, "Could not write sample apps to file: ", f.Name()) return nil } - return destination + return f } func copy(f *zip.File, destinationDir string, zipEntryPrefix string) error { diff --git a/client/go/cmd/clone_test.go b/client/go/cmd/clone_test.go index 5027c5bf972..6cf11dd4d40 100644 --- a/client/go/cmd/clone_test.go +++ b/client/go/cmd/clone_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/vespa-engine/vespa/client/go/util" @@ -18,10 +19,14 @@ func TestClone(t *testing.T) { } func assertCreated(sampleAppName string, app string, t *testing.T) { - existingSampleAppsZip = "testdata/sample-apps-master.zip" - standardOut := executeCommand(t, &mockHttpClient{}, []string{"clone", sampleAppName, app}, []string{}) + testFile := filepath.Join("testdata", "sample-apps-master.zip") + now := time.Now() + if err := os.Chtimes(testFile, now, now); err != nil { // Ensure test file is considered new enough by cache mechanism + t.Fatal(err) + } + out, _ := execute(command{cacheDir: filepath.Dir(testFile), args: []string{"clone", sampleAppName, app}}, t, nil) defer os.RemoveAll(app) - assert.Equal(t, "Created "+app+"\n", standardOut) + assert.Equal(t, "Using cached sample apps ...\nCreated "+app+"\n", out) assert.True(t, util.PathExists(filepath.Join(app, "README.md"))) assert.True(t, util.PathExists(filepath.Join(app, "src", "main", "application"))) assert.True(t, util.IsDirectory(filepath.Join(app, "src", "main", "application"))) diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go index f455ffa9957..6929b59decb 100644 --- a/client/go/cmd/command_tester.go +++ b/client/go/cmd/command_tester.go @@ -22,6 +22,7 @@ import ( type command struct { homeDir string + cacheDir string args []string moreArgs []string } @@ -31,12 +32,16 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) (string, string) util.ActiveHttpClient = client } - // Set config dir. Use a separate one per test if none is specified + // Set Vespa CLI directories. Use a separate one per test if none is specified if cmd.homeDir == "" { - cmd.homeDir = t.TempDir() + cmd.homeDir = filepath.Join(t.TempDir(), ".vespa") viper.Reset() } - os.Setenv("VESPA_CLI_HOME", filepath.Join(cmd.homeDir, ".vespa")) + if cmd.cacheDir == "" { + cmd.cacheDir = filepath.Join(t.TempDir(), ".cache", "vespa") + } + os.Setenv("VESPA_CLI_HOME", cmd.homeDir) + os.Setenv("VESPA_CLI_CACHE_DIR", cmd.cacheDir) // Reset flags to their default value - persistent flags in Cobra persists over tests rootCmd.Flags().VisitAll(func(f *pflag.Flag) { @@ -111,5 +116,3 @@ func (c *mockHttpClient) Do(request *http.Request, timeout time.Duration) (*http } func (c *mockHttpClient) UseCertificate(certificate tls.Certificate) {} - -func convergeServices(client *mockHttpClient) { client.NextResponse(200, `{"converged":true}`) } diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go index 3753d9a9390..863f247bd7c 100644 --- a/client/go/cmd/config.go +++ b/client/go/cmd/config.go @@ -34,8 +34,16 @@ func init() { } var configCmd = &cobra.Command{ - Use: "config", - Short: "Configure default values for flags", + Use: "config", + Short: "Configure persistent values for flags", + Long: `Configure persistent values for flags. + +This command allows setting a persistent value for a given flag. On future +invocations the flag can then be omitted as it is read from the config file +instead. + +Configuration is written to $HOME/.vespa by default. This path can be +overridden by setting the VESPA_CLI_HOME environment variable.`, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { // Root command does nothing @@ -100,16 +108,8 @@ type Config struct { } func LoadConfig() (*Config, error) { - home := os.Getenv("VESPA_CLI_HOME") - if home == "" { - var err error - home, err = os.UserHomeDir() - if err != nil { - return nil, err - } - home = filepath.Join(home, ".vespa") - } - if err := os.MkdirAll(home, 0700); err != nil { + home, err := vespaCliHome() + if err != nil { return nil, err } c := &Config{Home: home, createDirs: true} diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go index cf50f561f0f..25ba7cc0655 100644 --- a/client/go/cmd/config_test.go +++ b/client/go/cmd/config_test.go @@ -1,13 +1,14 @@ package cmd import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestConfig(t *testing.T) { - homeDir := t.TempDir() + homeDir := filepath.Join(t.TempDir(), ".vespa") assertConfigCommand(t, "invalid option or value: \"foo\": \"bar\"\n", homeDir, "config", "set", "foo", "bar") assertConfigCommand(t, "foo = <unset>\n", homeDir, "config", "get", "foo") assertConfigCommand(t, "target = local\n", homeDir, "config", "get", "target") diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go index 340eacd0bd3..d5021e19cf2 100644 --- a/client/go/cmd/curl_test.go +++ b/client/go/cmd/curl_test.go @@ -10,13 +10,12 @@ import ( ) func TestCurl(t *testing.T) { - homeDir := t.TempDir() + homeDir := filepath.Join(t.TempDir(), ".vespa") httpClient := &mockHttpClient{} - convergeServices(httpClient) out, _ := execute(command{homeDir: homeDir, args: []string{"curl", "-n", "-a", "t1.a1.i1", "--", "-v", "--data-urlencode", "arg=with space", "/search"}}, t, httpClient) expected := fmt.Sprintf("curl --key %s --cert %s -v --data-urlencode 'arg=with space' https://127.0.0.1:8080/search\n", - filepath.Join(homeDir, ".vespa", "t1.a1.i1", "data-plane-private-key.pem"), - filepath.Join(homeDir, ".vespa", "t1.a1.i1", "data-plane-public-cert.pem")) + filepath.Join(homeDir, "t1.a1.i1", "data-plane-private-key.pem"), + filepath.Join(homeDir, "t1.a1.i1", "data-plane-public-cert.pem")) assert.Equal(t, expected, out) } diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go index 9bf59187778..b3171d184e0 100644 --- a/client/go/cmd/deploy.go +++ b/client/go/cmd/deploy.go @@ -39,7 +39,11 @@ When this returns successfully the application package has been validated and activated on config servers. The process of applying it on individual nodes has started but may not have completed. -If application directory is not specified, it defaults to working directory.`, +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.`, Example: "$ vespa deploy .", Args: cobra.MaximumNArgs(1), DisableAutoGenTag: true, @@ -78,7 +82,7 @@ If application directory is not specified, it defaults to working directory.`, if opts.IsCloud() { log.Printf("\nUse %s for deployment status, or follow this deployment at", color.Cyan("vespa status")) log.Print(color.Cyan(fmt.Sprintf("%s/tenant/%s/application/%s/dev/instance/%s/job/%s-%s/run/%d", - defaultConsoleURL, + getConsoleURL(), opts.Deployment.Application.Tenant, opts.Deployment.Application.Application, opts.Deployment.Application.Instance, opts.Deployment.Zone.Environment, opts.Deployment.Zone.Region, sessionOrRunID))) diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go index 443f7e8846f..9614806b968 100644 --- a/client/go/cmd/deploy_test.go +++ b/client/go/cmd/deploy_test.go @@ -130,7 +130,7 @@ func assertActivate(applicationPackage string, arguments []string, t *testing.T) if err := cfg.WriteSessionID(vespa.DefaultApplication, 42); err != nil { t.Fatal(err) } - out, _ := execute(command{args: arguments, homeDir: homeDir}, t, client) + out, _ := execute(command{args: arguments, homeDir: cfg.Home}, t, client) assert.Equal(t, "Success: Activated "+applicationPackage+" with session 42\n", out) diff --git a/client/go/cmd/document_test.go b/client/go/cmd/document_test.go index 8aecb538f89..1f82b85f915 100644 --- a/client/go/cmd/document_test.go +++ b/client/go/cmd/document_test.go @@ -67,7 +67,6 @@ func TestDocumentRemoveWithoutIdArg(t *testing.T) { func TestDocumentSendMissingId(t *testing.T) { arguments := []string{"document", "put", "testdata/A-Head-Full-of-Dreams-Without-Operation.json"} client := &mockHttpClient{} - convergeServices(client) assert.Equal(t, "Error: No document id given neither as argument or as a 'put' key in the json file\n", executeCommand(t, client, arguments, []string{})) @@ -76,7 +75,6 @@ func TestDocumentSendMissingId(t *testing.T) { func TestDocumentSendWithDisagreeingOperations(t *testing.T) { arguments := []string{"document", "update", "testdata/A-Head-Full-of-Dreams-Put.json"} client := &mockHttpClient{} - convergeServices(client) assert.Equal(t, "Error: Wanted document operation is update but the JSON file specifies put\n", executeCommand(t, client, arguments, []string{})) @@ -140,7 +138,6 @@ func assertDocumentGet(arguments []string, documentId string, t *testing.T) { func assertDocumentError(t *testing.T, status int, errorMessage string) { client := &mockHttpClient{} - convergeServices(client) client.NextResponse(status, errorMessage) assert.Equal(t, "Error: Invalid document operation: Status "+strconv.Itoa(status)+"\n\n"+errorMessage+"\n", @@ -151,7 +148,6 @@ func assertDocumentError(t *testing.T, status int, errorMessage string) { func assertDocumentServerError(t *testing.T, status int, errorMessage string) { client := &mockHttpClient{} - convergeServices(client) client.NextResponse(status, errorMessage) assert.Equal(t, "Error: Container (document API) at 127.0.0.1:8080: Status "+strconv.Itoa(status)+"\n\n"+errorMessage+"\n", @@ -161,6 +157,5 @@ func assertDocumentServerError(t *testing.T, status int, errorMessage string) { } func documentServiceURL(client *mockHttpClient) string { - convergeServices(client) return getService("document", 0).BaseURL } diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go index f29a842aed2..98d6814d16f 100644 --- a/client/go/cmd/helpers.go +++ b/client/go/cmd/helpers.go @@ -10,14 +10,13 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "strings" "time" "github.com/vespa-engine/vespa/client/go/vespa" ) -const defaultConsoleURL = "https://console.vespa.oath.cloud" - var exitFunc = os.Exit // To allow overriding Exit in tests func fatalErrHint(err error, hints ...string) { @@ -50,6 +49,36 @@ func printSuccess(msg ...interface{}) { log.Print(color.Green("Success: "), fmt.Sprint(msg...)) } +func vespaCliHome() (string, error) { + home := os.Getenv("VESPA_CLI_HOME") + if home == "" { + userHome, err := os.UserHomeDir() + if err != nil { + return "", err + } + home = filepath.Join(userHome, ".vespa") + } + if err := os.MkdirAll(home, 0700); err != nil { + return "", err + } + return home, nil +} + +func vespaCliCacheDir() (string, error) { + cacheDir := os.Getenv("VESPA_CLI_CACHE_DIR") + if cacheDir == "" { + userCacheDir, err := os.UserCacheDir() + if err != nil { + return "", err + } + cacheDir = filepath.Join(userCacheDir, "vespa") + } + if err := os.MkdirAll(cacheDir, 0755); err != nil { + return "", err + } + return cacheDir, nil +} + func deploymentFromArgs() vespa.Deployment { zone, err := vespa.ZoneFromString(zoneArg) if err != nil { @@ -102,18 +131,32 @@ func getService(service string, sessionOrRunID int64) *vespa.Service { t := getTarget() timeout := time.Duration(waitSecsArg) * time.Second if timeout > 0 { - log.Printf("Waiting up to %d %s for services to become available ...", color.Cyan(waitSecsArg), color.Cyan("seconds")) - } - if err := t.DiscoverServices(timeout, sessionOrRunID); err != nil { - fatalErr(err, "Services unavailable") + log.Printf("Waiting up to %d %s for service to become available ...", color.Cyan(waitSecsArg), color.Cyan("seconds")) } - s, err := t.Service(service) + s, err := t.Service(service, timeout, sessionOrRunID) if err != nil { - fatalErr(err, "Invalid service") + fatalErr(err, "Invalid service: ", service) } return s } +func getConsoleURL() string { + system := os.Getenv("VESPA_CLI_CLOUD_SYSTEM") + if system == "publiccd" { + return "https://console-cd.vespa.oath.cloud" + } + return "https://console.vespa.oath.cloud" + +} + +func getApiURL() string { + system := os.Getenv("VESPA_CLI_CLOUD_SYSTEM") + if system == "publiccd" { + return "https://api.vespa-external-cd.aws.oath.cloud:4443" + } + return "https://api.vespa-external.aws.oath.cloud:4443" +} + func getTarget() vespa.Target { targetType := getTargetType() if strings.HasPrefix(targetType, "http") { @@ -147,7 +190,7 @@ func getTarget() vespa.Target { if err != nil { fatalErrHint(err, "Deployment to cloud requires a certificate. Try 'vespa cert'") } - return vespa.CloudTarget(deployment, apiKey, + return vespa.CloudTarget(getApiURL(), deployment, apiKey, vespa.TLSOptions{ KeyPair: kp, CertificateFile: certificateFile, diff --git a/client/go/cmd/query_test.go b/client/go/cmd/query_test.go index bd9ae91f24d..137ffa01cd5 100644 --- a/client/go/cmd/query_test.go +++ b/client/go/cmd/query_test.go @@ -56,7 +56,6 @@ func assertQuery(t *testing.T, expectedQuery string, query ...string) { func assertQueryError(t *testing.T, status int, errorMessage string) { client := &mockHttpClient{} - convergeServices(client) client.NextResponse(status, errorMessage) assert.Equal(t, "Error: Invalid query: Status "+strconv.Itoa(status)+"\n"+errorMessage+"\n", @@ -66,7 +65,6 @@ func assertQueryError(t *testing.T, status int, errorMessage string) { func assertQueryServiceError(t *testing.T, status int, errorMessage string) { client := &mockHttpClient{} - convergeServices(client) client.NextResponse(status, errorMessage) assert.Equal(t, "Error: Status "+strconv.Itoa(status)+" from container at 127.0.0.1:8080\n"+errorMessage+"\n", @@ -75,6 +73,5 @@ func assertQueryServiceError(t *testing.T, status int, errorMessage string) { } func queryServiceURL(client *mockHttpClient) string { - convergeServices(client) return getService("query", 0).BaseURL } diff --git a/client/go/cmd/status_test.go b/client/go/cmd/status_test.go index 8ddca71a35b..0c1c8e4e3a7 100644 --- a/client/go/cmd/status_test.go +++ b/client/go/cmd/status_test.go @@ -44,7 +44,6 @@ func TestStatusErrorResponse(t *testing.T) { func assertDeployStatus(target string, args []string, t *testing.T) { client := &mockHttpClient{} - convergeServices(client) assert.Equal(t, "Deploy API at "+target+" is ready\n", executeCommand(t, client, []string{"status", "deploy"}, args), @@ -54,14 +53,12 @@ func assertDeployStatus(target string, args []string, t *testing.T) { func assertQueryStatus(target string, args []string, t *testing.T) { client := &mockHttpClient{} - convergeServices(client) assert.Equal(t, "Container (query API) at "+target+" is ready\n", executeCommand(t, client, []string{"status", "query"}, args), "vespa status container") assert.Equal(t, target+"/ApplicationStatus", client.lastRequest.URL.String()) - convergeServices(client) assert.Equal(t, "Container (query API) at "+target+" is ready\n", executeCommand(t, client, []string{"status"}, args), @@ -71,7 +68,6 @@ func assertQueryStatus(target string, args []string, t *testing.T) { func assertDocumentStatus(target string, args []string, t *testing.T) { client := &mockHttpClient{} - convergeServices(client) assert.Equal(t, "Container (document API) at "+target+" is ready\n", executeCommand(t, client, []string{"status", "document"}, args), @@ -81,7 +77,6 @@ func assertDocumentStatus(target string, args []string, t *testing.T) { func assertQueryStatusError(target string, args []string, t *testing.T) { client := &mockHttpClient{} - convergeServices(client) client.NextStatus(500) assert.Equal(t, "Container (query API) at "+target+" is not ready\nStatus 500\n", diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go index ece841617c0..19319724d18 100644 --- a/client/go/vespa/deploy.go +++ b/client/go/vespa/deploy.go @@ -71,7 +71,7 @@ func (d DeploymentOpts) String() string { func (d *DeploymentOpts) IsCloud() bool { return d.Target.Type() == cloudTargetType } func (d *DeploymentOpts) url(path string) (*url.URL, error) { - service, err := d.Target.Service("deploy") + service, err := d.Target.Service(deployService, 0, 0) if err != nil { return nil, err } diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go index 69dc876c1c8..df9144cd186 100644 --- a/client/go/vespa/target.go +++ b/client/go/vespa/target.go @@ -24,8 +24,6 @@ const ( queryService = "query" documentService = "document" - defaultCloudAPI = "https://api.vespa-external.aws.oath.cloud:4443" - waitRetryInterval = 2 * time.Second ) @@ -41,11 +39,8 @@ type Target interface { // Type returns this target's type, e.g. local or cloud. Type() string - // Service returns the service for given name. - Service(name string) (*Service, error) - - // DiscoverServices queries for services available on this target after the deployment run has completed. - DiscoverServices(timeout time.Duration, runID int64) error + // Service returns the service for given name. If timeout is non-zero, wait for the service to converge. + Service(name string, timeout time.Duration, sessionOrRunID int64) (*Service, error) } // TLSOptions configures the certificate to use for service requests. @@ -107,7 +102,12 @@ func (s *Service) Description() string { func (t *customTarget) Type() string { return t.targetType } -func (t *customTarget) Service(name string) (*Service, error) { +func (t *customTarget) Service(name string, timeout time.Duration, sessionID int64) (*Service, error) { + if timeout > 0 && name != deployService { + if err := t.waitForConvergence(timeout); err != nil { + return nil, err + } + } switch name { case deployService, queryService, documentService: url, err := t.urlWithPort(name) @@ -139,12 +139,12 @@ func (t *customTarget) urlWithPort(serviceName string) (string, error) { return u.String(), nil } -func (t *customTarget) DiscoverServices(timeout time.Duration, runID int64) error { - deployService, err := t.Service("deploy") +func (t *customTarget) waitForConvergence(timeout time.Duration) error { + deployer, err := t.Service(deployService, 0, 0) if err != nil { return err } - url := fmt.Sprintf("%s/application/v2/tenant/default/application/default/environment/prod/region/default/instance/default/serviceconverge", deployService.BaseURL) + url := fmt.Sprintf("%s/application/v2/tenant/default/application/default/environment/prod/region/default/instance/default/serviceconverge", deployer.BaseURL) req, err := http.NewRequest("GET", url, nil) if err != nil { return err @@ -171,7 +171,7 @@ func (t *customTarget) DiscoverServices(timeout time.Duration, runID int64) erro } type cloudTarget struct { - cloudAPI string + apiURL string targetType string deployment Deployment apiKey []byte @@ -184,27 +184,30 @@ type cloudTarget struct { func (t *cloudTarget) Type() string { return t.targetType } -func (t *cloudTarget) Service(name string) (*Service, error) { +func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64) (*Service, error) { + if timeout > 0 && name != deployService { + if err := t.waitForEndpoints(timeout, runID); err != nil { + return nil, err + } + } switch name { case deployService: - return &Service{Name: name, BaseURL: t.cloudAPI}, nil + return &Service{Name: name, BaseURL: t.apiURL}, nil case queryService: if t.queryURL == "" { - return nil, fmt.Errorf("service %s not discovered", name) + return nil, fmt.Errorf("service %s is not discovered", name) } return &Service{Name: name, BaseURL: t.queryURL, TLSOptions: t.tlsOptions}, nil case documentService: if t.documentURL == "" { - return nil, fmt.Errorf("service %s not discovered", name) + return nil, fmt.Errorf("service %s is not discovered", name) } return &Service{Name: name, BaseURL: t.documentURL, TLSOptions: t.tlsOptions}, nil } return nil, fmt.Errorf("unknown service: %s", name) } -// DiscoverServices waits for run identified by runID to complete and at least one endpoint is available, or timeout -// passes. -func (t *cloudTarget) DiscoverServices(timeout time.Duration, runID int64) error { +func (t *cloudTarget) waitForEndpoints(timeout time.Duration, runID int64) error { signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey) if runID > 0 { if err := t.waitForRun(signer, runID, timeout); err != nil { @@ -216,7 +219,7 @@ func (t *cloudTarget) DiscoverServices(timeout time.Duration, runID int64) error func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout time.Duration) error { runURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/job/%s-%s/run/%d", - t.cloudAPI, + t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, t.deployment.Zone.Environment, t.deployment.Zone.Region, runID) req, err := http.NewRequest("GET", runURL, nil) @@ -234,8 +237,8 @@ func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout tim return req } jobSuccessFunc := func(status int, response []byte) (bool, error) { - if status/100 != 2 { - return false, nil + if ok, err := isOK(status); !ok { + return ok, err } var resp jobResponse if err := json.Unmarshal(response, &resp); err != nil { @@ -280,7 +283,7 @@ func (t *cloudTarget) printLog(response jobResponse, last int64) int64 { func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Duration) error { deploymentURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s", - t.cloudAPI, + t.apiURL, t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance, t.deployment.Zone.Environment, t.deployment.Zone.Region) req, err := http.NewRequest("GET", deploymentURL, nil) @@ -292,8 +295,8 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura } var endpointURL string endpointFunc := func(status int, response []byte) (bool, error) { - if status/100 != 2 { - return false, nil + if ok, err := isOK(status); !ok { + return ok, err } var resp deploymentResponse if err := json.Unmarshal(response, &resp); err != nil { @@ -316,6 +319,13 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura return nil } +func isOK(status int) (bool, error) { + if status == 401 { + return false, fmt.Errorf("status %d: invalid api key", status) + } + return status/100 == 2, nil +} + // LocalTarget creates a target for a Vespa platform running locally. func LocalTarget() Target { return &customTarget{targetType: localTargetType, baseURL: "http://127.0.0.1"} @@ -327,9 +337,9 @@ func CustomTarget(baseURL string) Target { } // CloudTarget creates a Target for the Vespa Cloud platform. -func CloudTarget(deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target { +func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target { return &cloudTarget{ - cloudAPI: defaultCloudAPI, + apiURL: apiURL, targetType: cloudTargetType, deployment: deployment, apiKey: apiKey, @@ -409,7 +419,8 @@ func wait(fn responseFunc, reqFn requestFunc, certificate *tls.Certificate, time return statusCode, nil } } - if loopOnce { + timeLeft := deadline.Sub(time.Now()) + if loopOnce || timeLeft < waitRetryInterval { break } time.Sleep(waitRetryInterval) diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go index 31f145f0db3..2c90baefbbc 100644 --- a/client/go/vespa/target_test.go +++ b/client/go/vespa/target_test.go @@ -74,11 +74,11 @@ func TestCustomTargetWait(t *testing.T) { defer srv.Close() target := CustomTarget(srv.URL) - err := target.DiscoverServices(0, 42) + _, err := target.Service("query", time.Millisecond, 42) assert.NotNil(t, err) vc.deploymentConverged = true - err = target.DiscoverServices(0, 42) + _, err = target.Service("query", time.Millisecond, 42) assert.Nil(t, err) assertServiceWait(t, 200, target, "deploy") @@ -102,6 +102,7 @@ func TestCloudTargetWait(t *testing.T) { var logWriter bytes.Buffer target := CloudTarget( + "https://example.com", Deployment{ Application: ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"}, Zone: ZoneID{Environment: "dev", Region: "us-north-1"}, @@ -110,20 +111,17 @@ func TestCloudTargetWait(t *testing.T) { TLSOptions{KeyPair: x509KeyPair}, LogOptions{Writer: &logWriter}) if ct, ok := target.(*cloudTarget); ok { - ct.cloudAPI = srv.URL + ct.apiURL = srv.URL } else { t.Fatalf("Wrong target type %T", ct) } assertServiceWait(t, 200, target, "deploy") - _, err = target.Service("query") - assert.NotNil(t, err) - - err = target.DiscoverServices(0, 42) + _, err = target.Service("query", time.Millisecond, 42) assert.NotNil(t, err) vc.deploymentConverged = true - err = target.DiscoverServices(0, 42) + _, err = target.Service("query", time.Millisecond, 42) assert.Nil(t, err) assertServiceWait(t, 500, target, "query") @@ -136,13 +134,13 @@ func TestCloudTargetWait(t *testing.T) { } func assertServiceURL(t *testing.T, url string, target Target, service string) { - s, err := target.Service(service) + s, err := target.Service(service, 0, 42) assert.Nil(t, err) assert.Equal(t, url, s.BaseURL) } func assertServiceWait(t *testing.T, expectedStatus int, target Target, service string) { - s, err := target.Service(service) + s, err := target.Service(service, 0, 42) assert.Nil(t, err) status, err := s.Wait(0) |