summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/go/cmd/api_key.go2
-rw-r--r--client/go/cmd/api_key_test.go12
-rw-r--r--client/go/cmd/cert_test.go8
-rw-r--r--client/go/cmd/clone.go99
-rw-r--r--client/go/cmd/clone_test.go11
-rw-r--r--client/go/cmd/command_tester.go13
-rw-r--r--client/go/cmd/config.go24
-rw-r--r--client/go/cmd/config_test.go3
-rw-r--r--client/go/cmd/curl_test.go7
-rw-r--r--client/go/cmd/deploy.go8
-rw-r--r--client/go/cmd/deploy_test.go2
-rw-r--r--client/go/cmd/document_test.go5
-rw-r--r--client/go/cmd/helpers.go61
-rw-r--r--client/go/cmd/query_test.go3
-rw-r--r--client/go/cmd/status_test.go5
-rw-r--r--client/go/vespa/deploy.go2
-rw-r--r--client/go/vespa/target.go67
-rw-r--r--client/go/vespa/target_test.go18
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)