summaryrefslogtreecommitdiffstats
path: root/client/go
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-02-28 12:58:08 +0100
committerMartin Polden <mpolden@mpolden.no>2022-02-28 14:36:58 +0100
commitac3b7c8ec070e36b958ac38bdc275e80e1e81cd7 (patch)
treed226c3fc4c9412a8dd881862d659c0bbac637cbf /client/go
parente2a943aabd29424df63daaef4121e6f8a0d4a96c (diff)
Support hosted Vespa in Vespa CLI
Diffstat (limited to 'client/go')
-rw-r--r--client/go/cmd/api_key.go16
-rw-r--r--client/go/cmd/api_key_test.go2
-rw-r--r--client/go/cmd/config.go12
-rw-r--r--client/go/cmd/config_test.go13
-rw-r--r--client/go/cmd/curl.go13
-rw-r--r--client/go/cmd/deploy.go10
-rw-r--r--client/go/cmd/deploy_test.go6
-rw-r--r--client/go/cmd/helpers.go193
-rw-r--r--client/go/cmd/log_test.go9
-rw-r--r--client/go/cmd/login.go10
-rw-r--r--client/go/cmd/logout.go10
-rw-r--r--client/go/cmd/prod.go21
-rw-r--r--client/go/cmd/prod_test.go15
-rw-r--r--client/go/cmd/root.go1
-rw-r--r--client/go/cmd/test.go2
-rw-r--r--client/go/vespa/deploy.go46
-rw-r--r--client/go/vespa/system.go23
-rw-r--r--client/go/vespa/target.go195
-rw-r--r--client/go/vespa/target_test.go29
-rw-r--r--client/go/vespa/xml/config.go7
20 files changed, 392 insertions, 241 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go
index 5cc1dab8a35..ae3f5346f4c 100644
--- a/client/go/cmd/api_key.go
+++ b/client/go/cmd/api_key.go
@@ -79,11 +79,19 @@ func doApiKey(_ *cobra.Command, _ []string) error {
if err != nil {
return err
}
+ targetType, err := getTargetType()
+ if err != nil {
+ return err
+ }
+ system, err := getSystem(targetType)
+ if err != nil {
+ return err
+ }
apiKeyFile := cfg.APIKeyPath(app.Tenant)
if util.PathExists(apiKeyFile) && !overwriteKey {
err := fmt.Errorf("refusing to overwrite %s", apiKeyFile)
printErrHint(err, "Use -f to overwrite it")
- printPublicKey(apiKeyFile, app.Tenant)
+ printPublicKey(system, apiKeyFile, app.Tenant)
return ErrCLI{error: err, quiet: true}
}
apiKey, err := vespa.CreateAPIKey()
@@ -92,13 +100,13 @@ func doApiKey(_ *cobra.Command, _ []string) error {
}
if err := ioutil.WriteFile(apiKeyFile, apiKey, 0600); err == nil {
printSuccess("API private key written to ", apiKeyFile)
- return printPublicKey(apiKeyFile, app.Tenant)
+ return printPublicKey(system, apiKeyFile, app.Tenant)
} else {
return fmt.Errorf("failed to write: %s: %w", apiKeyFile, err)
}
}
-func printPublicKey(apiKeyFile, tenant string) error {
+func printPublicKey(system vespa.System, apiKeyFile, tenant string) error {
pemKeyData, err := ioutil.ReadFile(apiKeyFile)
if err != nil {
return fmt.Errorf("failed to read: %s: %w", apiKeyFile, err)
@@ -118,7 +126,7 @@ func printPublicKey(apiKeyFile, tenant string) error {
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(), getConsoleURL(), tenant)
+ log.Printf(color.Cyan("%s/tenant/%s/keys").String(), system.ConsoleURL, tenant)
log.Print("and paste the entire public key including the BEGIN and END lines.")
return nil
}
diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go
index 935b8676c09..ba697b69d9f 100644
--- a/client/go/cmd/api_key_test.go
+++ b/client/go/cmd/api_key_test.go
@@ -23,6 +23,8 @@ func testAPIKey(t *testing.T, subcommand []string) {
homeDir := filepath.Join(t.TempDir(), ".vespa")
keyFile := filepath.Join(homeDir, "t1.api-key.pem")
+ execute(command{args: []string{"config", "set", "target", "cloud"}, homeDir: homeDir}, t, nil)
+
args := append(subcommand, "-a", "t1.a1.i1")
out, _ := execute(command{args: args, homeDir: homeDir}, t, nil)
assert.Contains(t, out, "Success: API private key written to "+keyFile+"\n")
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index 9d42f0683fd..0997be2c899 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -197,7 +197,7 @@ func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) {
}
// UseAPIKey checks if api key should be used be checking if api-key or api-key-file has been set.
-func (c *Config) UseAPIKey(tenantName string) bool {
+func (c *Config) UseAPIKey(system vespa.System, tenantName string) bool {
if _, err := c.Get(apiKeyFlag); err == nil {
return true
}
@@ -207,15 +207,11 @@ func (c *Config) UseAPIKey(tenantName string) bool {
// If no Auth0 token is created, fall back to tenant api key, but warn that this functionality is deprecated
// TODO: Remove this when users have had time to migrate over to Auth0 device flow authentication
- a, err := auth0.GetAuth0(c.AuthConfigPath(), getSystemName(), getApiURL())
+ a, err := auth0.GetAuth0(c.AuthConfigPath(), system.Name, system.URL)
if err != nil || !a.HasSystem() {
fmt.Fprintln(stderr, "Defaulting to tenant API key is deprecated. Use Auth0 device flow: 'vespa auth login' instead")
- if !util.PathExists(c.APIKeyPath(tenantName)) {
- return false
- }
- return true
+ return util.PathExists(c.APIKeyPath(tenantName))
}
-
return false
}
@@ -283,7 +279,7 @@ func (c *Config) Set(option, value string) error {
switch option {
case targetFlag:
switch value {
- case "local", "cloud":
+ case vespa.TargetLocal, vespa.TargetCloud, vespa.TargetHosted:
viper.Set(option, value)
return nil
}
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index 16378b5f8ba..2f0ccbb29e1 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
func TestConfig(t *testing.T) {
@@ -16,6 +17,8 @@ func TestConfig(t *testing.T) {
assertConfigCommandErr(t, "Error: 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")
+ assertConfigCommand(t, "", homeDir, "config", "set", "target", "hosted")
+ assertConfigCommand(t, "target = hosted\n", homeDir, "config", "get", "target")
assertConfigCommand(t, "", homeDir, "config", "set", "target", "cloud")
assertConfigCommand(t, "target = cloud\n", homeDir, "config", "get", "target")
assertConfigCommand(t, "", homeDir, "config", "set", "target", "http://127.0.0.1:8080")
@@ -66,15 +69,15 @@ func TestUseAPIKey(t *testing.T) {
homeDir := t.TempDir()
c := Config{Home: homeDir}
- assert.False(t, c.UseAPIKey("t1"))
+ assert.False(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
c.Set(apiKeyFileFlag, "/tmp/foo")
- assert.True(t, c.UseAPIKey("t1"))
+ assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
c.Set(apiKeyFileFlag, "")
withEnv("VESPA_CLI_API_KEY", "...", func() {
require.Nil(t, c.load())
- assert.True(t, c.UseAPIKey("t1"))
+ assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t1"))
})
// Test deprecated functionality
@@ -97,8 +100,8 @@ func TestUseAPIKey(t *testing.T) {
withEnv("VESPA_CLI_CLOUD_SYSTEM", "public", func() {
_, err := os.Create(filepath.Join(homeDir, "t2.api-key.pem"))
require.Nil(t, err)
- assert.True(t, c.UseAPIKey("t2"))
+ assert.True(t, c.UseAPIKey(vespa.PublicSystem, "t2"))
require.Nil(t, ioutil.WriteFile(filepath.Join(homeDir, "auth.json"), []byte(authContent), 0600))
- assert.False(t, c.UseAPIKey("t2"))
+ assert.False(t, c.UseAPIKey(vespa.PublicSystem, "t2"))
})
}
diff --git a/client/go/cmd/curl.go b/client/go/cmd/curl.go
index b66780780ed..1ede2cccae3 100644
--- a/client/go/cmd/curl.go
+++ b/client/go/cmd/curl.go
@@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"github.com/vespa-engine/vespa/client/go/auth0"
"github.com/vespa-engine/vespa/client/go/curl"
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
var curlDryRun bool
@@ -61,8 +62,8 @@ $ vespa curl -- -v --data-urlencode "yql=select * from music where album contain
if err != nil {
return err
}
- if t.Type() == "cloud" {
- if err := addCloudAuth0Authentication(cfg, c); err != nil {
+ if t.Type() == vespa.TargetCloud {
+ if err := addCloudAuth0Authentication(t.Deployment().System, cfg, c); err != nil {
return err
}
}
@@ -92,17 +93,17 @@ $ vespa curl -- -v --data-urlencode "yql=select * from music where album contain
},
}
-func addCloudAuth0Authentication(cfg *Config, c *curl.Command) error {
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), getSystemName(), getApiURL())
+func addCloudAuth0Authentication(system vespa.System, cfg *Config, c *curl.Command) error {
+ a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
if err != nil {
return err
}
- system, err := a.PrepareSystem(auth0.ContextWithCancel())
+ authSystem, err := a.PrepareSystem(auth0.ContextWithCancel())
if err != nil {
return err
}
- c.Header("Authorization", "Bearer "+system.AccessToken)
+ c.Header("Authorization", "Bearer "+authSystem.AccessToken)
return nil
}
diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go
index aef6aece5ad..13994ae1b31 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -26,7 +26,7 @@ func init() {
rootCmd.AddCommand(deployCmd)
rootCmd.AddCommand(prepareCmd)
rootCmd.AddCommand(activateCmd)
- deployCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "dev.aws-us-east-1c", "The zone to use for deployment")
+ deployCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "", "The zone to use for deployment. This defaults to a dev zone")
deployCmd.PersistentFlags().StringVarP(&logLevelArg, logLevelFlag, "l", "error", `Log level for Vespa logs. Must be "error", "warning", "info" or "debug"`)
}
@@ -73,7 +73,7 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
return err
}
- fmt.Print("\n")
+ log.Println()
if opts.IsCloud() {
printSuccess("Triggered deployment of ", color.Cyan(pkg.Path), " with run ID ", color.Cyan(sessionOrRunID))
} else {
@@ -82,9 +82,9 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
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",
- getConsoleURL(),
- opts.Deployment.Application.Tenant, opts.Deployment.Application.Application, opts.Deployment.Application.Instance,
- opts.Deployment.Zone.Environment, opts.Deployment.Zone.Region,
+ opts.Target.Deployment().System.ConsoleURL,
+ opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application, opts.Target.Deployment().Application.Instance,
+ opts.Target.Deployment().Zone.Environment, opts.Target.Deployment().Zone.Region,
sessionOrRunID)))
}
return waitForQueryService(sessionOrRunID)
diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go
index bcb7c515dd7..f5af3751eb8 100644
--- a/client/go/cmd/deploy_test.go
+++ b/client/go/cmd/deploy_test.go
@@ -35,7 +35,7 @@ func TestDeployZipWithURLTargetArgument(t *testing.T) {
client := &mock.HTTPClient{}
assert.Equal(t,
- "Success: Deployed "+applicationPackage+"\n",
+ "\nSuccess: Deployed "+applicationPackage+"\n",
executeCommand(t, client, arguments, []string{}))
assertDeployRequestMade("http://target:19071", client, t)
}
@@ -107,7 +107,7 @@ func TestDeployError(t *testing.T) {
func assertDeploy(applicationPackage string, arguments []string, t *testing.T) {
client := &mock.HTTPClient{}
assert.Equal(t,
- "Success: Deployed "+applicationPackage+"\n",
+ "\nSuccess: Deployed "+applicationPackage+"\n",
executeCommand(t, client, arguments, []string{}))
assertDeployRequestMade("http://127.0.0.1:19071", client, t)
}
@@ -174,6 +174,6 @@ func assertDeployServerError(t *testing.T, status int, errorMessage string) {
client.NextResponse(status, errorMessage)
_, outErr := execute(command{args: []string{"deploy", "testdata/applications/withTarget/target/application.zip"}}, t, client)
assert.Equal(t,
- "Error: error from deploy service at 127.0.0.1:19071 (Status "+strconv.Itoa(status)+"):\n"+errorMessage+"\n",
+ "Error: error from deploy api at 127.0.0.1:19071 (Status "+strconv.Itoa(status)+"):\n"+errorMessage+"\n",
outErr)
}
diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go
index b5c5845e6ae..7098c241f37 100644
--- a/client/go/cmd/helpers.go
+++ b/client/go/cmd/helpers.go
@@ -5,6 +5,8 @@
package cmd
import (
+ "crypto/tls"
+ "crypto/x509"
"encoding/json"
"fmt"
"log"
@@ -29,6 +31,40 @@ func printSuccess(msg ...interface{}) {
log.Print(color.Green("Success: "), fmt.Sprint(msg...))
}
+func athenzPath(filename string) (string, error) {
+ userHome, err := os.UserHomeDir()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(userHome, ".athenz", filename), nil
+}
+
+func athenzKeyPair() (tls.Certificate, error) {
+ certFile, err := athenzPath("cert")
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ keyFile, err := athenzPath("key")
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ kp, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ cert, err := x509.ParseCertificate(kp.Certificate[0])
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ now := time.Now()
+ expiredAt := cert.NotAfter
+ if expiredAt.Before(now) {
+ delta := now.Sub(expiredAt).Truncate(time.Second)
+ return tls.Certificate{}, errHint(fmt.Errorf("certificate %s expired at %s (%s ago)", certFile, cert.NotAfter, delta), "Try renewing certificate with 'athenz-user-cert'")
+ }
+ return kp, nil
+}
+
func vespaCliHome() (string, error) {
home := os.Getenv("VESPA_CLI_HOME")
if home == "" {
@@ -59,16 +95,20 @@ func vespaCliCacheDir() (string, error) {
return cacheDir, nil
}
-func deploymentFromArgs() (vespa.Deployment, error) {
- zone, err := vespa.ZoneFromString(zoneArg)
- if err != nil {
- return vespa.Deployment{}, err
+func deploymentFromArgs(system vespa.System) (vespa.Deployment, error) {
+ zone := system.DefaultZone
+ var err error
+ if zoneArg != "" {
+ zone, err = vespa.ZoneFromString(zoneArg)
+ if err != nil {
+ return vespa.Deployment{}, err
+ }
}
app, err := getApplication()
if err != nil {
return vespa.Deployment{}, err
}
- return vespa.Deployment{Application: app, Zone: zone}, nil
+ return vespa.Deployment{System: system, Application: app, Zone: zone}, nil
}
func applicationSource(args []string) string {
@@ -124,28 +164,18 @@ func getService(service string, sessionOrRunID int64, cluster string) (*vespa.Se
func getEndpointsOverride() string { return os.Getenv("VESPA_CLI_ENDPOINTS") }
-func getSystem() string { return os.Getenv("VESPA_CLI_CLOUD_SYSTEM") }
-
-func getSystemName() string {
- if getSystem() == "publiccd" {
- return "publiccd"
+func getSystem(targetType string) (vespa.System, error) {
+ name := os.Getenv("VESPA_CLI_CLOUD_SYSTEM")
+ if name != "" {
+ return vespa.GetSystem(name)
}
- return "public"
-}
-
-func getConsoleURL() string {
- if getSystem() == "publiccd" {
- return "https://console-cd.vespa.oath.cloud"
- }
- return "https://console.vespa.oath.cloud"
-
-}
-
-func getApiURL() string {
- if getSystem() == "publiccd" {
- return "https://api.vespa-external-cd.aws.oath.cloud:4443"
+ switch targetType {
+ case vespa.TargetHosted:
+ return vespa.MainSystem, nil
+ case vespa.TargetCloud:
+ return vespa.PublicSystem, nil
}
- return "https://api.vespa-external.aws.oath.cloud:4443"
+ return vespa.System{}, fmt.Errorf("no default system found for %s target", targetType)
}
func getTarget() (vespa.Target, error) {
@@ -172,53 +202,80 @@ func createTarget() (vespa.Target, error) {
return vespa.CustomTarget(targetType), nil
}
switch targetType {
- case "local":
+ case vespa.TargetLocal:
return vespa.LocalTarget(), nil
- case "cloud":
- cfg, err := LoadConfig()
- if err != nil {
- return nil, err
- }
- deployment, err := deploymentFromArgs()
- if err != nil {
- return nil, err
- }
- endpoints, err := getEndpointsFromEnv()
- if err != nil {
- return nil, err
- }
+ case vespa.TargetCloud, vespa.TargetHosted:
+ return createCloudTarget(targetType)
+ }
+ return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud', 'hosted' or an URL")
+}
- var apiKey []byte = nil
- if cfg.UseAPIKey(deployment.Application.Tenant) {
+func createCloudTarget(targetType string) (vespa.Target, error) {
+ cfg, err := LoadConfig()
+ if err != nil {
+ return nil, err
+ }
+ system, err := getSystem(targetType)
+ if err != nil {
+ return nil, err
+ }
+ deployment, err := deploymentFromArgs(system)
+ if err != nil {
+ return nil, err
+ }
+ endpoints, err := getEndpointsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+ var (
+ apiKey []byte
+ authConfigPath string
+ apiTLSOptions vespa.TLSOptions
+ deploymentTLSOptions vespa.TLSOptions
+ )
+ if targetType == vespa.TargetCloud {
+ if cfg.UseAPIKey(system, deployment.Application.Tenant) {
apiKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
if err != nil {
return nil, err
}
}
+ authConfigPath = cfg.AuthConfigPath()
kp, err := cfg.X509KeyPair(deployment.Application)
if err != nil {
return nil, errHint(err, "Deployment to cloud requires a certificate. Try 'vespa auth cert'")
}
-
- return vespa.CloudTarget(
- getApiURL(),
- deployment,
- apiKey,
- vespa.TLSOptions{
- KeyPair: kp.KeyPair,
- CertificateFile: kp.CertificateFile,
- PrivateKeyFile: kp.PrivateKeyFile,
- },
- vespa.LogOptions{
- Writer: stdout,
- Level: vespa.LogLevel(logLevelArg),
- },
- cfg.AuthConfigPath(),
- getSystemName(),
- endpoints,
- ), nil
- }
- return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL")
+ deploymentTLSOptions = vespa.TLSOptions{
+ KeyPair: kp.KeyPair,
+ CertificateFile: kp.CertificateFile,
+ PrivateKeyFile: kp.PrivateKeyFile,
+ }
+ } else if targetType == vespa.TargetHosted {
+ kp, err := athenzKeyPair()
+ if err != nil {
+ return nil, err
+ }
+ apiTLSOptions = vespa.TLSOptions{KeyPair: kp}
+ deploymentTLSOptions = apiTLSOptions
+ } else {
+ return nil, fmt.Errorf("invalid cloud target: %s", targetType)
+ }
+ apiOptions := vespa.APIOptions{
+ System: system,
+ TLSOptions: apiTLSOptions,
+ APIKey: apiKey,
+ AuthConfigPath: authConfigPath,
+ }
+ deploymentOptions := vespa.CloudDeploymentOptions{
+ Deployment: deployment,
+ TLSOptions: deploymentTLSOptions,
+ ClusterURLs: endpoints,
+ }
+ logOptions := vespa.LogOptions{
+ Writer: stdout,
+ Level: vespa.LogLevel(logLevelArg),
+ }
+ return vespa.CloudTarget(apiOptions, deploymentOptions, logOptions)
}
func waitForService(service string, sessionOrRunID int64) error {
@@ -245,22 +302,12 @@ func waitForService(service string, sessionOrRunID int64) error {
func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.Target) (vespa.DeploymentOptions, error) {
opts := vespa.DeploymentOptions{ApplicationPackage: pkg, Target: target}
if opts.IsCloud() {
- deployment, err := deploymentFromArgs()
- if err != nil {
- return vespa.DeploymentOptions{}, err
- }
- if !opts.ApplicationPackage.HasCertificate() {
+ if target.Type() == vespa.TargetCloud && !opts.ApplicationPackage.HasCertificate() {
hint := "Try 'vespa auth cert'"
return vespa.DeploymentOptions{}, errHint(fmt.Errorf("missing certificate in application package"), "Applications in Vespa Cloud require a certificate", hint)
}
- if cfg.UseAPIKey(deployment.Application.Tenant) {
- opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
- if err != nil {
- return vespa.DeploymentOptions{}, err
- }
- }
- opts.Deployment = deployment
}
+ opts.Timeout = time.Duration(waitSecsArg) * time.Second
return opts, nil
}
diff --git a/client/go/cmd/log_test.go b/client/go/cmd/log_test.go
index e67f00db096..3f6714b0d3c 100644
--- a/client/go/cmd/log_test.go
+++ b/client/go/cmd/log_test.go
@@ -18,10 +18,12 @@ func TestLog(t *testing.T) {
execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "api-key-file", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"cert", pkgDir}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
- out, _ := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T10:00:00Z", "--to", "2021-09-27T11:00:00Z"}}, t, httpClient)
+ out, outErr := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T10:00:00Z", "--to", "2021-09-27T11:00:00Z"}}, t, httpClient)
+ assert.Equal(t, "", outErr)
expected := "[2021-09-27 10:31:30.905535] host1a.dev.aws-us-east-1c info logserver-container Container.com.yahoo.container.jdisc.ConfiguredApplication Switching to the latest deployed set of configurations and components. Application config generation: 52532\n"
assert.Equal(t, expected, out)
@@ -40,8 +42,9 @@ func TestLogOldClient(t *testing.T) {
execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "api-key-file", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"cert", pkgDir}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
out, errOut := execute(command{homeDir: homeDir, args: []string{"log"}}, t, httpClient)
assert.Equal(t, "", out)
expected := "Error: client version 7.0.0 is less than the minimum supported version: 8.0.0\nHint: This is not a fatal error, but this version may not work as expected\nHint: Try 'vespa version' to check for a new version\n"
diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go
index 8787f1f80f5..2ac480d05f5 100644
--- a/client/go/cmd/login.go
+++ b/client/go/cmd/login.go
@@ -18,7 +18,15 @@ var loginCmd = &cobra.Command{
if err != nil {
return err
}
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), getSystemName(), getApiURL())
+ targetType, err := getTargetType()
+ if err != nil {
+ return err
+ }
+ system, err := getSystem(targetType)
+ if err != nil {
+ return err
+ }
+ a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
if err != nil {
return err
}
diff --git a/client/go/cmd/logout.go b/client/go/cmd/logout.go
index ddc1d36d5e1..b1f2477aba4 100644
--- a/client/go/cmd/logout.go
+++ b/client/go/cmd/logout.go
@@ -17,7 +17,15 @@ var logoutCmd = &cobra.Command{
if err != nil {
return err
}
- a, err := auth0.GetAuth0(cfg.AuthConfigPath(), getSystemName(), getApiURL())
+ targetType, err := getTargetType()
+ if err != nil {
+ return err
+ }
+ system, err := getSystem(targetType)
+ if err != nil {
+ return err
+ }
+ a, err := auth0.GetAuth0(cfg.AuthConfigPath(), system.Name, system.URL)
if err != nil {
return err
}
diff --git a/client/go/cmd/prod.go b/client/go/cmd/prod.go
index 8c40eb969bf..7f20b889345 100644
--- a/client/go/cmd/prod.go
+++ b/client/go/cmd/prod.go
@@ -73,6 +73,10 @@ https://cloud.vespa.ai/en/reference/deployment`,
if err != nil {
return fmt.Errorf("a services.xml declaring your cluster(s) must exist: %w", err)
}
+ target, err := getTarget()
+ if err != nil {
+ return err
+ }
fmt.Fprint(stdout, "This will modify any existing ", color.Yellow("deployment.xml"), " and ", color.Yellow("services.xml"),
"!\nBefore modification a backup of the original file will be created.\n\n")
@@ -80,7 +84,7 @@ https://cloud.vespa.ai/en/reference/deployment`,
fmt.Fprint(stdout, "Abort the configuration at any time by pressing Ctrl-C. The\nfiles will remain untouched.\n\n")
fmt.Fprint(stdout, "See this guide for sizing a Vespa deployment:\n", color.Green("https://docs.vespa.ai/en/performance/sizing-search.html\n\n"))
r := bufio.NewReader(stdin)
- deploymentXML, err = updateRegions(r, deploymentXML)
+ deploymentXML, err = updateRegions(r, deploymentXML, target.Deployment().System)
if err != nil {
return err
}
@@ -127,8 +131,9 @@ $ vespa prod submit`,
if err != nil {
return err
}
- if target.Type() != "cloud" {
- return fmt.Errorf("%s target cannot deploy to Vespa Cloud", target.Type())
+ if target.Type() != vespa.TargetCloud {
+ // TODO: Add support for hosted
+ return fmt.Errorf("prod submit does not support %s target", target.Type())
}
appSource := applicationSource(args)
pkg, err := vespa.FindApplicationPackage(appSource, true)
@@ -165,7 +170,7 @@ $ vespa prod submit`,
} else {
printSuccess("Submitted ", color.Cyan(pkg.Path), " for deployment")
log.Printf("See %s for deployment progress\n", color.Cyan(fmt.Sprintf("%s/tenant/%s/application/%s/prod/deployment",
- getConsoleURL(), opts.Deployment.Application.Tenant, opts.Deployment.Application.Application)))
+ opts.Target.Deployment().System.ConsoleURL, opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application)))
}
return nil
},
@@ -202,8 +207,8 @@ func writeWithBackup(pkg vespa.ApplicationPackage, filename, contents string) er
return ioutil.WriteFile(dst, []byte(contents), 0644)
}
-func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment) (xml.Deployment, error) {
- regions, err := promptRegions(r, deploymentXML)
+func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (xml.Deployment, error) {
+ regions, err := promptRegions(r, deploymentXML, system)
if err != nil {
return xml.Deployment{}, err
}
@@ -222,7 +227,7 @@ func updateRegions(r *bufio.Reader, deploymentXML xml.Deployment) (xml.Deploymen
return deploymentXML, nil
}
-func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment) (string, error) {
+func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment, system vespa.System) (string, error) {
fmt.Fprintln(stdout, color.Cyan("> Deployment regions"))
fmt.Fprintf(stdout, "Documentation: %s\n", color.Green("https://cloud.vespa.ai/en/reference/zones"))
fmt.Fprintf(stdout, "Example: %s\n\n", color.Yellow("aws-us-east-1c,aws-us-west-2a"))
@@ -238,7 +243,7 @@ func promptRegions(r *bufio.Reader, deploymentXML xml.Deployment) (string, error
validator := func(input string) error {
regions := strings.Split(input, ",")
for _, r := range regions {
- if !xml.IsProdRegion(r, getSystem()) {
+ if !xml.IsProdRegion(r, system) {
return fmt.Errorf("invalid region %s", r)
}
}
diff --git a/client/go/cmd/prod_test.go b/client/go/cmd/prod_test.go
index dc246e1b469..90b67af8669 100644
--- a/client/go/cmd/prod_test.go
+++ b/client/go/cmd/prod_test.go
@@ -152,8 +152,8 @@ func TestProdSubmit(t *testing.T) {
httpClient.NextResponse(200, `ok`)
execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"cert", pkgDir}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
// Zipping requires relative paths, so much let command run from pkgDir, then reset cwd for subsequent tests.
if cwd, err := os.Getwd(); err != nil {
@@ -167,8 +167,8 @@ func TestProdSubmit(t *testing.T) {
if err := os.Setenv("CI", "true"); err != nil {
t.Fatal(err)
}
- out, err := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
- assert.Equal(t, "", err)
+ out, outErr := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient)
+ assert.Equal(t, "", outErr)
assert.Contains(t, out, "Success: Submitted")
assert.Contains(t, out, "See https://console.vespa.oath.cloud/tenant/t1/application/a1/prod/deployment for deployment progress")
}
@@ -182,8 +182,8 @@ func TestProdSubmitWithJava(t *testing.T) {
httpClient.NextResponse(200, `ok`)
execute(command{homeDir: homeDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, httpClient)
execute(command{homeDir: homeDir, args: []string{"config", "set", "target", "cloud"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"api-key"}}, t, httpClient)
- execute(command{homeDir: homeDir, args: []string{"cert", pkgDir}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "api-key"}}, t, httpClient)
+ execute(command{homeDir: homeDir, args: []string{"auth", "cert", pkgDir}}, t, httpClient)
// Copy an application package pre-assembled with mvn package
testAppDir := filepath.Join("testdata", "applications", "withDeployment", "target")
@@ -192,7 +192,8 @@ func TestProdSubmitWithJava(t *testing.T) {
testZipFile := filepath.Join(testAppDir, "application-test.zip")
copyFile(t, filepath.Join(pkgDir, "target", "application-test.zip"), testZipFile)
- out, _ := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem"), pkgDir}}, t, httpClient)
+ out, outErr := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem"), pkgDir}}, t, httpClient)
+ assert.Equal(t, "", outErr)
assert.Contains(t, out, "Success: Submitted")
assert.Contains(t, out, "See https://console.vespa.oath.cloud/tenant/t1/application/a1/prod/deployment for deployment progress")
}
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go
index f5a846536c5..cbcbb6e5d12 100644
--- a/client/go/cmd/root.go
+++ b/client/go/cmd/root.go
@@ -54,7 +54,6 @@ Vespa documentation: https://docs.vespa.ai`,
colorArg string
quietArg bool
apiKeyFileArg string
- apiKeyArg string
stdin io.ReadWriter = os.Stdin
color = aurora.NewAurora(false)
diff --git a/client/go/cmd/test.go b/client/go/cmd/test.go
index 294f98c0f91..d12059a8d12 100644
--- a/client/go/cmd/test.go
+++ b/client/go/cmd/test.go
@@ -25,7 +25,7 @@ import (
func init() {
rootCmd.AddCommand(testCmd)
- testCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "dev.aws-us-east-1c", "The zone to use for deployment")
+ testCmd.PersistentFlags().StringVarP(&zoneArg, zoneFlag, "z", "", "The zone to use for deployment. This defaults to a dev zone")
}
var testCmd = &cobra.Command{
diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go
index dd3df238d3f..3316dcac924 100644
--- a/client/go/vespa/deploy.go
+++ b/client/go/vespa/deploy.go
@@ -38,15 +38,15 @@ type ZoneID struct {
}
type Deployment struct {
+ System System
Application ApplicationID
Zone ZoneID
}
type DeploymentOptions struct {
- ApplicationPackage ApplicationPackage
Target Target
- Deployment Deployment
- APIKey []byte
+ ApplicationPackage ApplicationPackage
+ Timeout time.Duration
}
type ApplicationPackage struct {
@@ -67,10 +67,13 @@ func (d Deployment) String() string {
}
func (d DeploymentOptions) String() string {
- return fmt.Sprintf("%s to %s", d.Deployment, d.Target.Type())
+ return fmt.Sprintf("%s to %s", d.Target.Deployment(), d.Target.Type())
}
-func (d *DeploymentOptions) IsCloud() bool { return d.Target.Type() == cloudTargetType }
+// IsCloud returns whether this is a deployment to Vespa Cloud or hosted Vespa
+func (d *DeploymentOptions) IsCloud() bool {
+ return d.Target.Type() == TargetCloud || d.Target.Type() == TargetHosted
+}
func (d *DeploymentOptions) url(path string) (*url.URL, error) {
service, err := d.Target.Service(deployService, 0, 0, "")
@@ -256,15 +259,15 @@ func Deploy(opts DeploymentOptions) (int64, error) {
if err := checkDeploymentOpts(opts); err != nil {
return 0, err
}
- if opts.Deployment.Zone.Environment == "" || opts.Deployment.Zone.Region == "" {
+ if opts.Target.Deployment().Zone.Environment == "" || opts.Target.Deployment().Zone.Region == "" {
return 0, fmt.Errorf("%s: missing zone", opts)
}
path = fmt.Sprintf("/application/v4/tenant/%s/application/%s/instance/%s/deploy/%s-%s",
- opts.Deployment.Application.Tenant,
- opts.Deployment.Application.Application,
- opts.Deployment.Application.Instance,
- opts.Deployment.Zone.Environment,
- opts.Deployment.Zone.Region)
+ opts.Target.Deployment().Application.Tenant,
+ opts.Target.Deployment().Application.Application,
+ opts.Target.Deployment().Application.Instance,
+ opts.Target.Deployment().Zone.Environment,
+ opts.Target.Deployment().Zone.Region)
}
u, err := opts.url(path)
if err != nil {
@@ -292,12 +295,12 @@ func copyToPart(dst *multipart.Writer, src io.Reader, fieldname, filename string
func Submit(opts DeploymentOptions) error {
if !opts.IsCloud() {
- return fmt.Errorf("%s: submit is unsupported", opts)
+ return fmt.Errorf("%s: submit is unsupported by %s target", opts, opts.Target.Type())
}
if err := checkDeploymentOpts(opts); err != nil {
return err
}
- path := fmt.Sprintf("/application/v4/tenant/%s/application/%s/submit", opts.Deployment.Application.Tenant, opts.Deployment.Application.Application)
+ path := fmt.Sprintf("/application/v4/tenant/%s/application/%s/submit", opts.Target.Deployment().Application.Tenant, opts.Target.Deployment().Application.Application)
u, err := opts.url(path)
if err != nil {
return err
@@ -332,7 +335,7 @@ func Submit(opts DeploymentOptions) error {
}
request.Header.Set("Content-Type", writer.FormDataContentType())
serviceDescription := "Submit service"
- sigKeyId := opts.Deployment.Application.SerializedForm()
+ sigKeyId := opts.Target.Deployment().Application.SerializedForm()
if err := opts.Target.SignRequest(request, sigKeyId); err != nil {
return err
}
@@ -345,7 +348,7 @@ func Submit(opts DeploymentOptions) error {
}
func checkDeploymentOpts(opts DeploymentOptions) error {
- if !opts.ApplicationPackage.HasCertificate() {
+ if opts.Target.Type() == TargetCloud && !opts.ApplicationPackage.HasCertificate() {
return fmt.Errorf("%s: missing certificate in package", opts)
}
return nil
@@ -364,15 +367,18 @@ func uploadApplicationPackage(url *url.URL, opts DeploymentOptions) (int64, erro
Header: header,
Body: ioutil.NopCloser(zipReader),
}
- serviceDescription := "Deploy service"
- sigKeyId := opts.Deployment.Application.SerializedForm()
- if err := opts.Target.SignRequest(request, sigKeyId); err != nil {
+ service, err := opts.Target.Service(deployService, opts.Timeout, 0, "")
+ if err != nil {
return 0, err
}
+ keyID := opts.Target.Deployment().Application.SerializedForm()
+ if err := opts.Target.SignRequest(request, keyID); err != nil {
+ return 0, err
+ }
var response *http.Response
err = util.Spinner("Uploading application package ...", func() error {
- response, err = util.HttpDo(request, time.Minute*10, serviceDescription)
+ response, err = service.Do(request, time.Minute*10)
return err
})
if err != nil {
@@ -385,7 +391,7 @@ func uploadApplicationPackage(url *url.URL, opts DeploymentOptions) (int64, erro
RunID int64 `json:"run"` // Controller
}
jsonResponse.SessionID = "0" // Set a default session ID for responses that don't contain int (e.g. cloud deployment)
- if err := checkResponse(request, response, serviceDescription); err != nil {
+ if err := checkResponse(request, response, service.Description()); err != nil {
return 0, err
}
jsonDec := json.NewDecoder(response.Body)
diff --git a/client/go/vespa/system.go b/client/go/vespa/system.go
index c1bb4fcec0b..03a1de0cf28 100644
--- a/client/go/vespa/system.go
+++ b/client/go/vespa/system.go
@@ -20,18 +20,20 @@ var PublicCDSystem = System{
// MainSystem represents the main hosted Vespa system.
var MainSystem = System{
- Name: "main",
- URL: "https://api.vespa.ouryahoo.com:4443",
- ConsoleURL: "https://console.vespa.ouryahoo.com",
- DefaultZone: ZoneID{Environment: "dev", Region: "us-east-1"},
+ Name: "main",
+ URL: "https://api.vespa.ouryahoo.com:4443",
+ ConsoleURL: "https://console.vespa.ouryahoo.com",
+ DefaultZone: ZoneID{Environment: "dev", Region: "us-east-1"},
+ AthenzDomain: "vespa.vespa",
}
// CDSystem represents the CD variant of the hosted Vespa system.
var CDSystem = System{
- Name: "cd",
- URL: "https://api-cd.vespa.ouryahoo.com:4443",
- ConsoleURL: "https://console-cd.vespa.ouryahoo.com",
- DefaultZone: ZoneID{Environment: "dev", Region: "cd-us-west-1"},
+ Name: "cd",
+ URL: "https://api-cd.vespa.ouryahoo.com:4443",
+ ConsoleURL: "https://console-cd.vespa.ouryahoo.com",
+ DefaultZone: ZoneID{Environment: "dev", Region: "cd-us-west-1"},
+ AthenzDomain: "vespa.vespa.cd",
}
// System represents a Vespa system.
@@ -40,8 +42,11 @@ type System struct {
// URL is the API URL for this system.
URL string
ConsoleURL string
- // DefaultZone declares the default zone for manual deployments to this system.
+ // DefaultZone is default zone to use in manual deployments to this system.
DefaultZone ZoneID
+ // AthenzDomain is the Athenz domain used by this system. This is empty for systems not using Athenz for tenant
+ // authentication.
+ AthenzDomain string
}
// GetSystem returns the system of given name.
diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go
index 204dbc143c6..985e32a9420 100644
--- a/client/go/vespa/target.go
+++ b/client/go/vespa/target.go
@@ -19,12 +19,21 @@ import (
"github.com/vespa-engine/vespa/client/go/auth0"
"github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
+ "github.com/vespa-engine/vespa/client/go/zts"
)
const (
- localTargetType = "local"
- customTargetType = "custom"
- cloudTargetType = "cloud"
+ // A target for a local Vespa service
+ TargetLocal = "local"
+
+ // A target for a custom URL
+ TargetCustom = "custom"
+
+ // A Vespa Cloud target
+ TargetCloud = "cloud"
+
+ // A hosted Vespa target
+ TargetHosted = "hosted"
deployService = "deploy"
queryService = "query"
@@ -33,16 +42,12 @@ const (
retryInterval = 2 * time.Second
)
-const (
- CloudAuthApiKey = "api-key"
- CloudAuthAccessToken = "access-token"
-)
-
// Service represents a Vespa service.
type Service struct {
BaseURL string
Name string
TLSOptions TLSOptions
+ ztsClient ztsClient
}
// Target represents a Vespa platform, running named Vespa services.
@@ -50,6 +55,9 @@ type Target interface {
// Type returns this target's type, e.g. local or cloud.
Type() string
+ // Deployment returns the deployment managed by this target.
+ Deployment() Deployment
+
// 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, cluster string) (*Service, error)
@@ -63,11 +71,12 @@ type Target interface {
CheckVersion(clientVersion version.Version) error
}
-// TLSOptions configures the certificate to use for service requests.
+// TLSOptions configures the client certificate to use for cloud API or service requests.
type TLSOptions struct {
KeyPair tls.Certificate
CertificateFile string
PrivateKeyFile string
+ AthenzDomain string
}
// LogOptions configures the log output to produce when writing log messages.
@@ -80,20 +89,41 @@ type LogOptions struct {
Level int
}
+// CloudOptions configures URL and authentication for a cloud target.
+type APIOptions struct {
+ System System
+ TLSOptions TLSOptions
+ APIKey []byte
+ AuthConfigPath string
+}
+
+// CloudDeploymentOptions configures the deployment to manage through a cloud target.
+type CloudDeploymentOptions struct {
+ Deployment Deployment
+ TLSOptions TLSOptions
+ ClusterURLs map[string]string // Endpoints keyed on cluster name
+}
+
type customTarget struct {
targetType string
baseURL string
}
-func (t *customTarget) SignRequest(req *http.Request, sigKeyId string) error { return nil }
-
-func (t *customTarget) CheckVersion(version version.Version) error { return nil }
-
// Do sends request to this service. Any required authentication happens automatically.
func (s *Service) Do(request *http.Request, timeout time.Duration) (*http.Response, error) {
if s.TLSOptions.KeyPair.Certificate != nil {
util.ActiveHttpClient.UseCertificate([]tls.Certificate{s.TLSOptions.KeyPair})
}
+ if s.TLSOptions.AthenzDomain != "" {
+ accessToken, err := s.ztsClient.AccessToken(s.TLSOptions.AthenzDomain, s.TLSOptions.KeyPair)
+ if err != nil {
+ return nil, err
+ }
+ if request.Header == nil {
+ request.Header = make(http.Header)
+ }
+ request.Header.Add("Authorization", "Bearer "+accessToken)
+ }
return util.HttpDo(request, timeout, s.Description())
}
@@ -130,6 +160,8 @@ func (s *Service) Description() string {
func (t *customTarget) Type() string { return t.targetType }
+func (t *customTarget) Deployment() Deployment { return Deployment{} }
+
func (t *customTarget) Service(name string, timeout time.Duration, sessionOrRunID int64, cluster string) (*Service, error) {
if timeout > 0 && name != deployService {
if err := t.waitForConvergence(timeout); err != nil {
@@ -148,9 +180,13 @@ func (t *customTarget) Service(name string, timeout time.Duration, sessionOrRunI
}
func (t *customTarget) PrintLog(options LogOptions) error {
- return fmt.Errorf("reading logs from non-cloud deployment is currently unsupported")
+ return fmt.Errorf("reading logs from non-cloud deployment is unsupported")
}
+func (t *customTarget) SignRequest(req *http.Request, sigKeyId string) error { return nil }
+
+func (t *customTarget) CheckVersion(version version.Version) error { return nil }
+
func (t *customTarget) urlWithPort(serviceName string) (string, error) {
u, err := url.Parse(t.baseURL)
if err != nil {
@@ -203,32 +239,30 @@ func (t *customTarget) waitForConvergence(timeout time.Duration) error {
}
type cloudTarget struct {
- apiURL string
- targetType string
- deployment Deployment
- apiKey []byte
- tlsOptions TLSOptions
- logOptions LogOptions
+ apiOptions APIOptions
+ deploymentOptions CloudDeploymentOptions
+ logOptions LogOptions
+ ztsClient ztsClient
+}
- urlsByCluster map[string]string
- authConfigPath string
- systemName string
+type ztsClient interface {
+ AccessToken(domain string, certficiate tls.Certificate) (string, error)
}
func (t *cloudTarget) resolveEndpoint(cluster string) (string, error) {
if cluster == "" {
- for _, u := range t.urlsByCluster {
- if len(t.urlsByCluster) == 1 {
+ for _, u := range t.deploymentOptions.ClusterURLs {
+ if len(t.deploymentOptions.ClusterURLs) == 1 {
return u, nil
} else {
- return "", fmt.Errorf("multiple clusters, none chosen: %v", t.urlsByCluster)
+ return "", fmt.Errorf("multiple clusters, none chosen: %v", t.deploymentOptions.ClusterURLs)
}
}
} else {
- u := t.urlsByCluster[cluster]
+ u := t.deploymentOptions.ClusterURLs[cluster]
if u == "" {
- clusters := make([]string, len(t.urlsByCluster))
- for c := range t.urlsByCluster {
+ clusters := make([]string, len(t.deploymentOptions.ClusterURLs))
+ for c := range t.deploymentOptions.ClusterURLs {
clusters = append(clusters, c)
}
return "", fmt.Errorf("unknown cluster '%s': must be one of %v", cluster, clusters)
@@ -239,38 +273,42 @@ func (t *cloudTarget) resolveEndpoint(cluster string) (string, error) {
return "", fmt.Errorf("no endpoints")
}
-func (t *cloudTarget) Type() string { return t.targetType }
+func (t *cloudTarget) Type() string {
+ switch t.apiOptions.System.Name {
+ case MainSystem.Name, CDSystem.Name:
+ return TargetHosted
+ }
+ return TargetCloud
+}
+
+func (t *cloudTarget) Deployment() Deployment { return t.deploymentOptions.Deployment }
func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64, cluster string) (*Service, error) {
- if name != deployService && t.urlsByCluster == nil {
+ if name != deployService && t.deploymentOptions.ClusterURLs == nil {
if err := t.waitForEndpoints(timeout, runID); err != nil {
return nil, err
}
}
switch name {
case deployService:
- return &Service{Name: name, BaseURL: t.apiURL}, nil
- case queryService:
- queryURL, err := t.resolveEndpoint(cluster)
- if err != nil {
- return nil, err
- }
- return &Service{Name: name, BaseURL: queryURL, TLSOptions: t.tlsOptions}, nil
- case documentService:
- documentURL, err := t.resolveEndpoint(cluster)
+ return &Service{Name: name, BaseURL: t.apiOptions.System.URL, TLSOptions: t.apiOptions.TLSOptions, ztsClient: t.ztsClient}, nil
+ case queryService, documentService:
+ url, err := t.resolveEndpoint(cluster)
if err != nil {
return nil, err
}
- return &Service{Name: name, BaseURL: documentURL, TLSOptions: t.tlsOptions}, nil
+ t.deploymentOptions.TLSOptions.AthenzDomain = t.apiOptions.System.AthenzDomain
+ return &Service{Name: name, BaseURL: url, TLSOptions: t.deploymentOptions.TLSOptions, ztsClient: t.ztsClient}, nil
}
return nil, fmt.Errorf("unknown service: %s", name)
}
-// SignRequest adds authentication data to a http.Request.
-// The api key is used if set on cloudTarget, if not the Auth0 device flow is used.
func (t *cloudTarget) SignRequest(req *http.Request, sigKeyId string) error {
- if t.apiKey != nil {
- signer := NewRequestSigner(sigKeyId, t.apiKey)
+ if t.apiOptions.TLSOptions.KeyPair.Certificate != nil {
+ return nil // using mTLS
+ }
+ if t.apiOptions.APIKey != nil {
+ signer := NewRequestSigner(sigKeyId, t.apiOptions.APIKey)
if err := signer.SignRequest(req); err != nil {
return err
}
@@ -286,7 +324,7 @@ func (t *cloudTarget) CheckVersion(clientVersion version.Version) error {
if clientVersion.IsZero() { // development version is always fine
return nil
}
- req, err := http.NewRequest("GET", fmt.Sprintf("%s/cli/v1/", t.apiURL), nil)
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/cli/v1/", t.apiOptions.System.URL), nil)
if err != nil {
return err
}
@@ -313,7 +351,7 @@ func (t *cloudTarget) CheckVersion(clientVersion version.Version) error {
}
func (t *cloudTarget) addAuth0AccessToken(request *http.Request) error {
- a, err := auth0.GetAuth0(t.authConfigPath, t.systemName, t.apiURL)
+ a, err := auth0.GetAuth0(t.apiOptions.AuthConfigPath, t.apiOptions.System.Name, t.apiOptions.System.URL)
if err != nil {
return err
}
@@ -327,9 +365,9 @@ func (t *cloudTarget) addAuth0AccessToken(request *http.Request) error {
func (t *cloudTarget) logsURL() string {
return fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s/logs",
- t.apiURL,
- t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance,
- t.deployment.Zone.Environment, t.deployment.Zone.Region)
+ t.apiOptions.System.URL,
+ t.deploymentOptions.Deployment.Application.Tenant, t.deploymentOptions.Deployment.Application.Application, t.deploymentOptions.Deployment.Application.Instance,
+ t.deploymentOptions.Deployment.Zone.Environment, t.deploymentOptions.Deployment.Zone.Region)
}
func (t *cloudTarget) PrintLog(options LogOptions) error {
@@ -347,7 +385,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
q.Set("to", strconv.FormatInt(toMillis, 10))
}
req.URL.RawQuery = q.Encode()
- t.SignRequest(req, t.deployment.Application.SerializedForm())
+ t.SignRequest(req, t.deploymentOptions.Deployment.Application.SerializedForm())
return req
}
logFunc := func(status int, response []byte) (bool, error) {
@@ -376,7 +414,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
if options.Follow {
timeout = math.MaxInt64 // No timeout
}
- _, err = wait(logFunc, requestFunc, &t.tlsOptions.KeyPair, timeout)
+ _, err = wait(logFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
return err
}
@@ -391,9 +429,9 @@ func (t *cloudTarget) waitForEndpoints(timeout time.Duration, runID int64) error
func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error {
runURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/job/%s-%s/run/%d",
- t.apiURL,
- t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance,
- t.deployment.Zone.Environment, t.deployment.Zone.Region, runID)
+ t.apiOptions.System.URL,
+ t.deploymentOptions.Deployment.Application.Tenant, t.deploymentOptions.Deployment.Application.Application, t.deploymentOptions.Deployment.Application.Instance,
+ t.deploymentOptions.Deployment.Zone.Environment, t.deploymentOptions.Deployment.Zone.Region, runID)
req, err := http.NewRequest("GET", runURL, nil)
if err != nil {
return err
@@ -403,7 +441,7 @@ func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error {
q := req.URL.Query()
q.Set("after", strconv.FormatInt(lastID, 10))
req.URL.RawQuery = q.Encode()
- if err := t.SignRequest(req, t.deployment.Application.SerializedForm()); err != nil {
+ if err := t.SignRequest(req, t.deploymentOptions.Deployment.Application.SerializedForm()); err != nil {
panic(err)
}
return req
@@ -427,7 +465,7 @@ func (t *cloudTarget) waitForRun(runID int64, timeout time.Duration) error {
}
return true, nil
}
- _, err = wait(jobSuccessFunc, requestFunc, &t.tlsOptions.KeyPair, timeout)
+ _, err = wait(jobSuccessFunc, requestFunc, &t.apiOptions.TLSOptions.KeyPair, timeout)
return err
}
@@ -455,14 +493,14 @@ func (t *cloudTarget) printLog(response jobResponse, last int64) int64 {
func (t *cloudTarget) discoverEndpoints(timeout time.Duration) error {
deploymentURL := fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s",
- t.apiURL,
- t.deployment.Application.Tenant, t.deployment.Application.Application, t.deployment.Application.Instance,
- t.deployment.Zone.Environment, t.deployment.Zone.Region)
+ t.apiOptions.System.URL,
+ t.deploymentOptions.Deployment.Application.Tenant, t.deploymentOptions.Deployment.Application.Application, t.deploymentOptions.Deployment.Application.Instance,
+ t.deploymentOptions.Deployment.Zone.Environment, t.deploymentOptions.Deployment.Zone.Region)
req, err := http.NewRequest("GET", deploymentURL, nil)
if err != nil {
return err
}
- if err := t.SignRequest(req, t.deployment.Application.SerializedForm()); err != nil {
+ if err := t.SignRequest(req, t.deploymentOptions.Deployment.Application.SerializedForm()); err != nil {
return err
}
urlsByCluster := make(map[string]string)
@@ -485,13 +523,13 @@ func (t *cloudTarget) discoverEndpoints(timeout time.Duration) error {
}
return true, nil
}
- if _, err = wait(endpointFunc, func() *http.Request { return req }, &t.tlsOptions.KeyPair, timeout); err != nil {
+ if _, err = wait(endpointFunc, func() *http.Request { return req }, &t.apiOptions.TLSOptions.KeyPair, timeout); err != nil {
return err
}
if len(urlsByCluster) == 0 {
return fmt.Errorf("no endpoints discovered")
}
- t.urlsByCluster = urlsByCluster
+ t.deploymentOptions.ClusterURLs = urlsByCluster
return nil
}
@@ -504,28 +542,26 @@ func isOK(status int) (bool, error) {
// LocalTarget creates a target for a Vespa platform running locally.
func LocalTarget() Target {
- return &customTarget{targetType: localTargetType, baseURL: "http://127.0.0.1"}
+ return &customTarget{targetType: TargetLocal, baseURL: "http://127.0.0.1"}
}
// CustomTarget creates a Target for a Vespa platform running at baseURL.
func CustomTarget(baseURL string) Target {
- return &customTarget{targetType: customTargetType, baseURL: baseURL}
+ return &customTarget{targetType: TargetCustom, baseURL: baseURL}
}
-// CloudTarget creates a Target for the Vespa Cloud platform.
-func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions,
- authConfigPath string, systemName string, urlsByCluster map[string]string) Target {
- return &cloudTarget{
- apiURL: apiURL,
- targetType: cloudTargetType,
- deployment: deployment,
- apiKey: apiKey,
- tlsOptions: tlsOptions,
- logOptions: logOptions,
- authConfigPath: authConfigPath,
- systemName: systemName,
- urlsByCluster: urlsByCluster,
+// CloudTarget creates a Target for the Vespa Cloud or hosted Vespa platform.
+func CloudTarget(apiOptions APIOptions, deploymentOptions CloudDeploymentOptions, logOptions LogOptions) (Target, error) {
+ ztsClient, err := zts.NewClient(zts.DefaultURL, util.ActiveHttpClient)
+ if err != nil {
+ return nil, err
}
+ return &cloudTarget{
+ apiOptions: apiOptions,
+ deploymentOptions: deploymentOptions,
+ logOptions: logOptions,
+ ztsClient: ztsClient,
+ }, nil
}
type deploymentEndpoint struct {
@@ -571,7 +607,8 @@ func wait(fn responseFunc, reqFn requestFunc, certificate *tls.Certificate, time
deadline := time.Now().Add(timeout)
loopOnce := timeout == 0
for time.Now().Before(deadline) || loopOnce {
- response, httpErr = util.HttpDo(reqFn(), 10*time.Second, "")
+ req := reqFn()
+ response, httpErr = util.HttpDo(req, 10*time.Second, "")
if httpErr == nil {
statusCode = response.StatusCode
body, err := ioutil.ReadAll(response.Body)
diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go
index 8391655eaf7..bf3e0fae7d0 100644
--- a/client/go/vespa/target_test.go
+++ b/client/go/vespa/target_test.go
@@ -169,12 +169,23 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target {
apiKey, err := CreateAPIKey()
assert.Nil(t, err)
- target := CloudTarget("https://example.com", Deployment{
- Application: ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"},
- Zone: ZoneID{Environment: "dev", Region: "us-north-1"},
- }, apiKey, TLSOptions{KeyPair: x509KeyPair}, LogOptions{Writer: logWriter}, "", "", nil)
+ target, err := CloudTarget(
+ APIOptions{APIKey: apiKey, System: PublicSystem},
+ CloudDeploymentOptions{
+ Deployment: Deployment{
+ Application: ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"},
+ Zone: ZoneID{Environment: "dev", Region: "us-north-1"},
+ },
+ TLSOptions: TLSOptions{KeyPair: x509KeyPair},
+ },
+ LogOptions{Writer: logWriter},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
if ct, ok := target.(*cloudTarget); ok {
- ct.apiURL = url
+ ct.apiOptions.System.URL = url
+ ct.ztsClient = &mockZTSClient{token: "foo bar"}
} else {
t.Fatalf("Wrong target type %T", ct)
}
@@ -195,3 +206,11 @@ func assertServiceWait(t *testing.T, expectedStatus int, target Target, service
assert.Nil(t, err)
assert.Equal(t, expectedStatus, status)
}
+
+type mockZTSClient struct {
+ token string
+}
+
+func (c *mockZTSClient) AccessToken(domain string, certificate tls.Certificate) (string, error) {
+ return c.token, nil
+}
diff --git a/client/go/vespa/xml/config.go b/client/go/vespa/xml/config.go
index c9efcb7f340..c9af92339bc 100644
--- a/client/go/vespa/xml/config.go
+++ b/client/go/vespa/xml/config.go
@@ -9,6 +9,8 @@ import (
"regexp"
"strconv"
"strings"
+
+ "github.com/vespa-engine/vespa/client/go/vespa"
)
var DefaultDeployment Deployment
@@ -218,8 +220,9 @@ func ParseNodeCount(s string) (int, int, error) {
}
// IsProdRegion returns whether string s is a valid production region.
-func IsProdRegion(s string, system string) bool {
- if system == "publiccd" {
+func IsProdRegion(s string, system vespa.System) bool {
+ // TODO: Add support for cd and main systems
+ if system.Name == vespa.PublicCDSystem.Name {
return s == "aws-us-east-1c"
}
switch s {