diff options
author | Martin Polden <mpolden@mpolden.no> | 2022-02-28 12:58:08 +0100 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2022-02-28 14:36:58 +0100 |
commit | ac3b7c8ec070e36b958ac38bdc275e80e1e81cd7 (patch) | |
tree | d226c3fc4c9412a8dd881862d659c0bbac637cbf /client/go | |
parent | e2a943aabd29424df63daaef4121e6f8a0d4a96c (diff) |
Support hosted Vespa in Vespa CLI
Diffstat (limited to 'client/go')
-rw-r--r-- | client/go/cmd/api_key.go | 16 | ||||
-rw-r--r-- | client/go/cmd/api_key_test.go | 2 | ||||
-rw-r--r-- | client/go/cmd/config.go | 12 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 13 | ||||
-rw-r--r-- | client/go/cmd/curl.go | 13 | ||||
-rw-r--r-- | client/go/cmd/deploy.go | 10 | ||||
-rw-r--r-- | client/go/cmd/deploy_test.go | 6 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 193 | ||||
-rw-r--r-- | client/go/cmd/log_test.go | 9 | ||||
-rw-r--r-- | client/go/cmd/login.go | 10 | ||||
-rw-r--r-- | client/go/cmd/logout.go | 10 | ||||
-rw-r--r-- | client/go/cmd/prod.go | 21 | ||||
-rw-r--r-- | client/go/cmd/prod_test.go | 15 | ||||
-rw-r--r-- | client/go/cmd/root.go | 1 | ||||
-rw-r--r-- | client/go/cmd/test.go | 2 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 46 | ||||
-rw-r--r-- | client/go/vespa/system.go | 23 | ||||
-rw-r--r-- | client/go/vespa/target.go | 195 | ||||
-rw-r--r-- | client/go/vespa/target_test.go | 29 | ||||
-rw-r--r-- | client/go/vespa/xml/config.go | 7 |
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 { |