aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeandro Alves <ldalves@gmail.com>2021-11-12 19:59:19 +0100
committerGitHub <noreply@github.com>2021-11-12 19:59:19 +0100
commit720943c4c6c3d520e3ae140e9b814c572d21cd7f (patch)
tree8ffaea62459a6dd002e9ca5b8e375399d0ba927c
parentc1415db70208a20807f320eab16de6297e7e5286 (diff)
parent3cfbef21b4aa9ba1caba4e24529f27bb6cf2c0ab (diff)
Merge pull request #19998 from vespa-engine/ldalves/device-authorization-flow
Ldalves/device authorization flow
-rw-r--r--client/go/auth/auth.go175
-rw-r--r--client/go/auth/secrets.go24
-rw-r--r--client/go/auth/token.go68
-rw-r--r--client/go/cli/cli.go355
-rw-r--r--client/go/cmd/config.go4
-rw-r--r--client/go/cmd/helpers.go37
-rw-r--r--client/go/cmd/login.go34
-rw-r--r--client/go/go.mod15
-rw-r--r--client/go/go.sum58
-rw-r--r--client/go/util/spinner.go63
-rw-r--r--client/go/vespa/deploy.go20
-rw-r--r--client/go/vespa/target.go79
-rw-r--r--client/go/vespa/target_test.go7
13 files changed, 888 insertions, 51 deletions
diff --git a/client/go/auth/auth.go b/client/go/auth/auth.go
new file mode 100644
index 00000000000..397e410924d
--- /dev/null
+++ b/client/go/auth/auth.go
@@ -0,0 +1,175 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package auth
+
+import (
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+const (
+ audiencePath = "/api/v2/"
+ waitThresholdInSeconds = 3
+ // SecretsNamespace namespace used to set/get values from the keychain
+ SecretsNamespace = "vespa-cli"
+)
+
+var requiredScopes = []string{"openid", "offline_access"}
+
+type Authenticator struct {
+ Audience string
+ ClientID string
+ DeviceCodeEndpoint string
+ OauthTokenEndpoint string
+}
+
+// SecretStore provides access to stored sensitive data.
+type SecretStore interface {
+ // Get gets the secret
+ Get(namespace, key string) (string, error)
+ // Delete removes the secret
+ Delete(namespace, key string) error
+}
+
+type Result struct {
+ Tenant string
+ Domain string
+ RefreshToken string
+ AccessToken string
+ ExpiresIn int64
+}
+
+type State struct {
+ DeviceCode string `json:"device_code"`
+ UserCode string `json:"user_code"`
+ VerificationURI string `json:"verification_uri_complete"`
+ ExpiresIn int `json:"expires_in"`
+ Interval int `json:"interval"`
+}
+
+// RequiredScopes returns the scopes used for login.
+func RequiredScopes() []string { return requiredScopes }
+
+func (s *State) IntervalDuration() time.Duration {
+ return time.Duration(s.Interval+waitThresholdInSeconds) * time.Second
+}
+
+// Start kicks-off the device authentication flow
+// by requesting a device code from Auth0,
+// The returned state contains the URI for the next step of the flow.
+func (a *Authenticator) Start(ctx context.Context) (State, error) {
+ s, err := a.getDeviceCode(ctx)
+ if err != nil {
+ return State{}, fmt.Errorf("cannot get device code: %w", err)
+ }
+ return s, nil
+}
+
+// Wait waits until the user is logged in on the browser.
+func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) {
+ t := time.NewTicker(state.IntervalDuration())
+ for {
+ select {
+ case <-ctx.Done():
+ return Result{}, ctx.Err()
+ case <-t.C:
+ data := url.Values{
+ "client_id": {a.ClientID},
+ "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
+ "device_code": {state.DeviceCode},
+ }
+ r, err := http.PostForm(a.OauthTokenEndpoint, data)
+ if err != nil {
+ return Result{}, fmt.Errorf("cannot get device code: %w", err)
+ }
+ defer r.Body.Close()
+
+ var res struct {
+ AccessToken string `json:"access_token"`
+ IDToken string `json:"id_token"`
+ RefreshToken string `json:"refresh_token"`
+ Scope string `json:"scope"`
+ ExpiresIn int64 `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ Error *string `json:"error,omitempty"`
+ ErrorDescription string `json:"error_description,omitempty"`
+ }
+
+ err = json.NewDecoder(r.Body).Decode(&res)
+ if err != nil {
+ return Result{}, fmt.Errorf("cannot decode response: %w", err)
+ }
+
+ if res.Error != nil {
+ if *res.Error == "authorization_pending" {
+ continue
+ }
+ return Result{}, errors.New(res.ErrorDescription)
+ }
+
+ ten, domain, err := parseTenant(res.AccessToken)
+ if err != nil {
+ return Result{}, fmt.Errorf("cannot parse tenant from the given access token: %w", err)
+ }
+
+ return Result{
+ RefreshToken: res.RefreshToken,
+ AccessToken: res.AccessToken,
+ ExpiresIn: res.ExpiresIn,
+ Tenant: ten,
+ Domain: domain,
+ }, nil
+ }
+ }
+}
+
+func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) {
+ data := url.Values{
+ "client_id": {a.ClientID},
+ "scope": {strings.Join(requiredScopes, " ")},
+ "audience": {a.Audience},
+ }
+ r, err := http.PostForm(a.DeviceCodeEndpoint, data)
+ if err != nil {
+ return State{}, fmt.Errorf("cannot get device code: %w", err)
+ }
+ defer r.Body.Close()
+ var res State
+ err = json.NewDecoder(r.Body).Decode(&res)
+ if err != nil {
+ return State{}, fmt.Errorf("cannot decode response: %w", err)
+ }
+ return res, nil
+}
+
+func parseTenant(accessToken string) (tenant, domain string, err error) {
+ parts := strings.Split(accessToken, ".")
+ v, err := base64.RawURLEncoding.DecodeString(parts[1])
+ if err != nil {
+ return "", "", err
+ }
+ var payload struct {
+ AUDs []string `json:"aud"`
+ }
+ if err := json.Unmarshal(v, &payload); err != nil {
+ return "", "", err
+ }
+ for _, aud := range payload.AUDs {
+ u, err := url.Parse(aud)
+ if err != nil {
+ return "", "", err
+ }
+ if u.Path == audiencePath {
+ parts := strings.Split(u.Host, ".")
+ return parts[0], u.Host, nil
+ }
+ }
+ return "", "", fmt.Errorf("audience not found for %s", audiencePath)
+}
diff --git a/client/go/auth/secrets.go b/client/go/auth/secrets.go
new file mode 100644
index 00000000000..e38d8c56595
--- /dev/null
+++ b/client/go/auth/secrets.go
@@ -0,0 +1,24 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package auth
+
+import (
+ "github.com/zalando/go-keyring"
+)
+
+type Keyring struct{}
+
+// Set sets the given key/value pair with the given namespace.
+func (k *Keyring) Set(namespace, key, value string) error {
+ return keyring.Set(namespace, key, value)
+}
+
+// Get gets a value for the given namespace and key.
+func (k *Keyring) Get(namespace, key string) (string, error) {
+ return keyring.Get(namespace, key)
+}
+
+// Delete deletes a value for the given namespace and key.
+func (k *Keyring) Delete(namespace, key string) error {
+ return keyring.Delete(namespace, key)
+}
diff --git a/client/go/auth/token.go b/client/go/auth/token.go
new file mode 100644
index 00000000000..e9b90b8994e
--- /dev/null
+++ b/client/go/auth/token.go
@@ -0,0 +1,68 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package auth
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+type TokenResponse struct {
+ AccessToken string `json:"access_token"`
+ IDToken string `json:"id_token"`
+ TokenType string `json:"token_type"`
+ ExpiresIn int `json:"expires_in"`
+}
+
+type TokenRetriever struct {
+ Authenticator *Authenticator
+ Secrets SecretStore
+ Client *http.Client
+}
+
+// Delete deletes the given tenant from the secrets' storage.
+func (t *TokenRetriever) Delete(tenant string) error {
+ return t.Secrets.Delete(SecretsNamespace, tenant)
+}
+
+// Refresh gets a new access token from the provided refresh token,
+// The request is used the default client_id and endpoint for device authentication.
+func (t *TokenRetriever) Refresh(ctx context.Context, tenant string) (TokenResponse, error) {
+ // get stored refresh token:
+ refreshToken, err := t.Secrets.Get(SecretsNamespace, tenant)
+ if err != nil {
+ return TokenResponse{}, fmt.Errorf("cannot get the stored refresh token: %w", err)
+ }
+ if refreshToken == "" {
+ return TokenResponse{}, errors.New("cannot use the stored refresh token: the token is empty")
+ }
+ // get access token:
+ r, err := t.Client.PostForm(t.Authenticator.OauthTokenEndpoint, url.Values{
+ "grant_type": {"refresh_token"},
+ "client_id": {t.Authenticator.ClientID},
+ "refresh_token": {refreshToken},
+ })
+ if err != nil {
+ return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %w", err)
+ }
+
+ defer r.Body.Close()
+ if r.StatusCode != http.StatusOK {
+ b, _ := ioutil.ReadAll(r.Body)
+ bodyStr := string(b)
+ return TokenResponse{}, fmt.Errorf("cannot get a new access token from the refresh token: %s", bodyStr)
+ }
+
+ var res TokenResponse
+ err = json.NewDecoder(r.Body).Decode(&res)
+ if err != nil {
+ return TokenResponse{}, fmt.Errorf("cannot decode response: %w", err)
+ }
+
+ return res, nil
+}
diff --git a/client/go/cli/cli.go b/client/go/cli/cli.go
new file mode 100644
index 00000000000..e1dde387b89
--- /dev/null
+++ b/client/go/cli/cli.go
@@ -0,0 +1,355 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/joeshaw/envdecode"
+ "github.com/pkg/browser"
+ "github.com/vespa-engine/vespa/client/go/util"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/lestrrat-go/jwx/jwt"
+ "github.com/vespa-engine/vespa/client/go/auth"
+)
+
+const accessTokenExpThreshold = 5 * time.Minute
+
+var errUnauthenticated = errors.New("not logged in. Try 'vespa login'")
+
+type config struct {
+ InstallID string `json:"install_id,omitempty"`
+ DefaultTenant string `json:"default_tenant"`
+ Tenants map[string]Tenant `json:"tenants"`
+}
+
+// Tenant is an auth0 Tenant.
+type Tenant struct {
+ Name string `json:"name"`
+ Domain string `json:"domain"`
+ AccessToken string `json:"access_token,omitempty"`
+ Scopes []string `json:"scopes,omitempty"`
+ ExpiresAt time.Time `json:"expires_at"`
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+}
+
+type Cli struct {
+ Authenticator *auth.Authenticator
+ tenant string
+ initOnce sync.Once
+ errOnce error
+ Path string
+ config config
+}
+
+// IsLoggedIn encodes the domain logic for determining whether we're
+// logged in. This might check our config storage, or just in memory.
+func (c *Cli) IsLoggedIn() bool {
+ // No need to check errors for initializing context.
+ _ = c.init()
+
+ if c.tenant == "" {
+ return false
+ }
+
+ // Parse the access token for the tenant.
+ t, err := jwt.ParseString(c.config.Tenants[c.tenant].AccessToken)
+ if err != nil {
+ return false
+ }
+
+ // Check if token is valid.
+ if err = jwt.Validate(t, jwt.WithIssuer("https://vespa-cd.auth0.com/")); err != nil {
+ return false
+ }
+
+ return true
+}
+
+// default to vespa-cd.auth0.com
+var (
+ authCfg struct {
+ Audience string `env:"AUTH0_AUDIENCE,default=https://vespa-cd.auth0.com/api/v2/"`
+ ClientID string `env:"AUTH0_CLIENT_ID,default=4wYWA496zBP28SLiz0PuvCt8ltL11DZX"`
+ DeviceCodeEndpoint string `env:"AUTH0_DEVICE_CODE_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/device/code"`
+ OauthTokenEndpoint string `env:"AUTH0_OAUTH_TOKEN_ENDPOINT,default=https://vespa-cd.auth0.com/oauth/token"`
+ }
+)
+
+func ContextWithCancel() context.Context {
+ ctx, cancel := context.WithCancel(context.Background())
+ ch := make(chan os.Signal, 1)
+ signal.Notify(ch, os.Interrupt)
+ go func() {
+ <-ch
+ defer cancel()
+ os.Exit(0)
+ }()
+ return ctx
+}
+
+// Setup will try to initialize the config context, as well as figure out if
+// there's a readily available tenant.
+func GetCli(configPath string) (*Cli, error) {
+ c := Cli{}
+ c.Path = configPath
+ if err := envdecode.StrictDecode(&authCfg); err != nil {
+ return nil, fmt.Errorf("could not decode env: %w", err)
+ }
+ c.Authenticator = &auth.Authenticator{
+ Audience: authCfg.Audience,
+ ClientID: authCfg.ClientID,
+ DeviceCodeEndpoint: authCfg.DeviceCodeEndpoint,
+ OauthTokenEndpoint: authCfg.OauthTokenEndpoint,
+ }
+ return &c, nil
+}
+
+// prepareTenant loads the Tenant, refreshing its token if necessary.
+// The Tenant access token needs a refresh if:
+// 1. the Tenant scopes are different from the currently required scopes.
+// 2. the access token is expired.
+func (c *Cli) PrepareTenant(ctx context.Context) (Tenant, error) {
+ if err := c.init(); err != nil {
+ return Tenant{}, err
+ }
+ t, err := c.getTenant()
+ if err != nil {
+ return Tenant{}, err
+ }
+
+ if t.ClientID != "" && t.ClientSecret != "" {
+ return t, nil
+ }
+
+ if t.AccessToken == "" || scopesChanged(t) {
+ t, err = RunLogin(ctx, c, true)
+ if err != nil {
+ return Tenant{}, err
+ }
+ } else if isExpired(t.ExpiresAt, accessTokenExpThreshold) {
+ // check if the stored access token is expired:
+ // use the refresh token to get a new access token:
+ tr := &auth.TokenRetriever{
+ Authenticator: c.Authenticator,
+ Secrets: &auth.Keyring{},
+ Client: http.DefaultClient,
+ }
+
+ res, err := tr.Refresh(ctx, t.Domain)
+ if err != nil {
+ // ask and guide the user through the login process:
+ fmt.Println(fmt.Errorf("failed to renew access token, %s", err))
+ t, err = RunLogin(ctx, c, true)
+ if err != nil {
+ return Tenant{}, err
+ }
+ } else {
+ // persist the updated tenant with renewed access token
+ t.AccessToken = res.AccessToken
+ t.ExpiresAt = time.Now().Add(
+ time.Duration(res.ExpiresIn) * time.Second,
+ )
+
+ err = c.AddTenant(t)
+ if err != nil {
+ return Tenant{}, err
+ }
+ }
+ }
+
+ return t, nil
+}
+
+// isExpired is true if now() + a threshold is after the given date
+func isExpired(t time.Time, threshold time.Duration) bool {
+ return time.Now().Add(threshold).After(t)
+}
+
+// scopesChanged compare the Tenant scopes
+// with the currently required scopes.
+func scopesChanged(t Tenant) bool {
+ want := auth.RequiredScopes()
+ got := t.Scopes
+
+ sort.Strings(want)
+ sort.Strings(got)
+
+ if (want == nil) != (got == nil) {
+ return true
+ }
+
+ if len(want) != len(got) {
+ return true
+ }
+
+ for i := range t.Scopes {
+ if want[i] != got[i] {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *Cli) getTenant() (Tenant, error) {
+ if err := c.init(); err != nil {
+ return Tenant{}, err
+ }
+
+ t, ok := c.config.Tenants[c.tenant]
+ if !ok {
+ return Tenant{}, fmt.Errorf("unable to find tenant: %s; run 'vespa login' to configure a new tenant", c.tenant)
+ }
+
+ return t, nil
+}
+
+// AddTenant assigns an existing, or new Tenant. This is expected to be called
+// after a login has completed.
+func (c *Cli) AddTenant(ten Tenant) error {
+ _ = c.init()
+
+ if c.config.DefaultTenant == "" {
+ c.config.DefaultTenant = ten.Domain
+ }
+
+ // If we're dealing with an empty file, we'll need to initialize this map.
+ if c.config.Tenants == nil {
+ c.config.Tenants = map[string]Tenant{}
+ }
+
+ c.config.Tenants[ten.Domain] = ten
+
+ if err := c.persistConfig(); err != nil {
+ return fmt.Errorf("unexpected error persisting config: %w", err)
+ }
+
+ return nil
+}
+
+func (c *Cli) persistConfig() error {
+ dir := filepath.Dir(c.Path)
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ if err := os.MkdirAll(dir, 0700); err != nil {
+ return err
+ }
+ }
+
+ buf, err := json.MarshalIndent(c.config, "", " ")
+ if err != nil {
+ return err
+ }
+
+ if err := ioutil.WriteFile(c.Path, buf, 0600); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Cli) init() error {
+ c.initOnce.Do(func() {
+ if c.errOnce = c.initContext(); c.errOnce != nil {
+ return
+ }
+ })
+ return c.errOnce
+}
+
+func (c *Cli) initContext() (err error) {
+ if _, err := os.Stat(c.Path); os.IsNotExist(err) {
+ return errUnauthenticated
+ }
+
+ var buf []byte
+ if buf, err = ioutil.ReadFile(c.Path); err != nil {
+ return err
+ }
+
+ if err := json.Unmarshal(buf, &c.config); err != nil {
+ return err
+ }
+
+ if c.tenant == "" && c.config.DefaultTenant == "" {
+ return errUnauthenticated
+ }
+
+ if c.tenant == "" {
+ c.tenant = c.config.DefaultTenant
+ }
+
+ return nil
+}
+
+// RunLogin runs the login flow guiding the user through the process
+// by showing the login instructions, opening the browser.
+// Use `expired` to run the login from other commands setup:
+// this will only affect the messages.
+func RunLogin(ctx context.Context, cli *Cli, expired bool) (Tenant, error) {
+ if expired {
+ fmt.Println("Please sign in to re-authorize the CLI.")
+ }
+
+ state, err := cli.Authenticator.Start(ctx)
+ if err != nil {
+ return Tenant{}, fmt.Errorf("could not start the authentication process: %w", err)
+ }
+
+ fmt.Printf("Your Device Confirmation code is: %s\n\n", state.UserCode)
+ fmt.Println("Press Enter to open the browser to log in or ^C to quit...")
+ fmt.Scanln()
+
+ err = browser.OpenURL(state.VerificationURI)
+
+ if err != nil {
+ fmt.Printf("Couldn't open the URL, please do it manually: %s.", state.VerificationURI)
+ }
+
+ var res auth.Result
+ err = util.Spinner("Waiting for login to complete in browser", func() error {
+ res, err = cli.Authenticator.Wait(ctx, state)
+ return err
+ })
+
+ if err != nil {
+ return Tenant{}, fmt.Errorf("login error: %w", err)
+ }
+
+ fmt.Print("\n")
+ fmt.Println("Successfully logged in.")
+ fmt.Print("\n")
+
+ // store the refresh token
+ secretsStore := &auth.Keyring{}
+ err = secretsStore.Set(auth.SecretsNamespace, res.Domain, res.RefreshToken)
+ if err != nil {
+ // log the error but move on
+ fmt.Println("Could not store the refresh token locally, please expect to login again once your access token expired.")
+ }
+
+ t := Tenant{
+ Name: res.Tenant,
+ Domain: res.Domain,
+ AccessToken: res.AccessToken,
+ ExpiresAt: time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
+ Scopes: auth.RequiredScopes(),
+ }
+ err = cli.AddTenant(t)
+ if err != nil {
+ return Tenant{}, fmt.Errorf("could not add tenant to config: %w", err)
+ }
+
+ return t, nil
+}
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index d10f66c83c6..0b08a2dc28d 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -148,6 +148,10 @@ func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) {
return ioutil.ReadFile(c.APIKeyPath(tenantName))
}
+func (c *Config) AuthConfigPath() string {
+ return filepath.Join(c.Home, "auth", "config.json")
+}
+
func (c *Config) ReadSessionID(app vespa.ApplicationID) (int64, error) {
sessionPath, err := c.applicationFilePath(app, "session_id")
if err != nil {
diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go
index 4768290e33e..54d8798b71d 100644
--- a/client/go/cmd/helpers.go
+++ b/client/go/cmd/helpers.go
@@ -153,10 +153,17 @@ func getConsoleURL() string {
}
func getApiURL() string {
- if getSystem() == "publiccd" {
- return "https://api.vespa-external-cd.aws.oath.cloud:4443"
+ if vespa.Auth0AccessTokenEnabled() {
+ if getSystem() == "publiccd" {
+ return "https://api.vespa-external-cd.aws.oath.cloud:443"
+ }
+ return "https://api.vespa-external.aws.oath.cloud:443"
+ } else {
+ if getSystem() == "publiccd" {
+ return "https://api.vespa-external-cd.aws.oath.cloud:4443"
+ }
+ return "https://api.vespa-external.aws.oath.cloud:4443"
}
- return "https://api.vespa-external.aws.oath.cloud:4443"
}
func getTarget() vespa.Target {
@@ -174,9 +181,12 @@ func getTarget() vespa.Target {
fatalErr(err, "Could not load config")
return nil
}
- apiKey, err := ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant))
- if err != nil {
- fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ var apiKey []byte = nil
+ if !vespa.Auth0AccessTokenEnabled() {
+ apiKey, err = ioutil.ReadFile(cfg.APIKeyPath(deployment.Application.Tenant))
+ if err != nil {
+ fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ }
}
privateKeyFile, err := cfg.PrivateKeyPath(deployment.Application)
if err != nil {
@@ -201,7 +211,8 @@ func getTarget() vespa.Target {
vespa.LogOptions{
Writer: stdout,
Level: vespa.LogLevel(logLevelArg),
- })
+ },
+ cfg.AuthConfigPath())
}
fatalErrHint(fmt.Errorf("Invalid target: %s", targetType), "Valid targets are 'local', 'cloud' or an URL")
return nil
@@ -232,11 +243,13 @@ func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.T
fatalErrHint(fmt.Errorf("Missing certificate in application package"), "Applications in Vespa Cloud require a certificate", "Try 'vespa cert'")
return opts
}
- var err error
- opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
- if err != nil {
- fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
- return opts
+ if !vespa.Auth0AccessTokenEnabled() {
+ var err error
+ opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant)
+ if err != nil {
+ fatalErrHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'")
+ return opts
+ }
}
opts.Deployment = deployment
}
diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go
new file mode 100644
index 00000000000..767d462b0be
--- /dev/null
+++ b/client/go/cmd/login.go
@@ -0,0 +1,34 @@
+package cmd
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/vespa-engine/vespa/client/go/cli"
+ "github.com/vespa-engine/vespa/client/go/vespa"
+)
+
+func init() {
+ if vespa.Auth0AccessTokenEnabled() {
+ rootCmd.AddCommand(loginCmd)
+ }
+}
+
+var loginCmd = &cobra.Command{
+ Use: "login",
+ Args: cobra.NoArgs,
+ Short: "Authenticate the Vespa CLI",
+ Example: "$ vespa login",
+ DisableAutoGenTag: true,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ cfg, err := LoadConfig()
+ if err != nil {
+ return err
+ }
+ c, err := cli.GetCli(cfg.AuthConfigPath())
+ if err != nil {
+ return err
+ }
+ _, err = cli.RunLogin(ctx, c, false)
+ return err
+ },
+}
diff --git a/client/go/go.mod b/client/go/go.mod
index 27faff3fd0b..70eea958933 100644
--- a/client/go/go.mod
+++ b/client/go/go.mod
@@ -3,12 +3,23 @@ module github.com/vespa-engine/vespa/client/go
go 1.15
require (
+ github.com/briandowns/spinner v1.16.0
+ github.com/fatih/color v1.10.0 // indirect
+ github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
+ github.com/lestrrat-go/jwx v1.2.9
github.com/logrusorgru/aurora/v3 v3.0.0
- github.com/mattn/go-colorable v0.0.9
- github.com/mattn/go-isatty v0.0.3
+ github.com/mattn/go-colorable v0.1.8
+ github.com/mattn/go-isatty v0.0.13
+ github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2
+ github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
+ github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.7.0
+ github.com/zalando/go-keyring v0.1.1
+ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
+ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
diff --git a/client/go/go.sum b/client/go/go.sum
index 0462d0575a1..59656af2b35 100644
--- a/client/go/go.sum
+++ b/client/go/go.sum
@@ -45,6 +45,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs=
+github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@@ -57,9 +59,14 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
+github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -68,12 +75,18 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec=
+github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -165,6 +178,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
+github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
@@ -180,14 +195,31 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
+github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
+github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
+github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
+github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
+github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
+github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
+github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
+github.com/lestrrat-go/jwx v1.2.9 h1:kS8kLI4oaBYJJ6u6rpbPI0tDYVCqo0P5u8vv1zoQ49U=
+github.com/lestrrat-go/jwx v1.2.9/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw=
+github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
+github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4=
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -204,7 +236,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 h1:acNfDZXmm28D2Yg/c3ALnZStzNaZMSagpbr96vY6Zjc=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -235,6 +271,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -249,6 +287,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/zalando/go-keyring v0.1.1 h1:w2V9lcx/Uj4l+dzAf1m9s+DJ1O8ROkEHnynonHjTcYE=
+github.com/zalando/go-keyring v0.1.1/go.mod h1:OIC+OZ28XbmwFxU/Rp9V7eKzZjamBJwRzC8UFJH9+L8=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
@@ -269,6 +309,9 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
+golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -367,6 +410,7 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -376,9 +420,11 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -403,8 +449,11 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -575,8 +624,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
diff --git a/client/go/util/spinner.go b/client/go/util/spinner.go
new file mode 100644
index 00000000000..1deb4296d28
--- /dev/null
+++ b/client/go/util/spinner.go
@@ -0,0 +1,63 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package util
+
+import (
+ "os"
+ "time"
+
+ "github.com/briandowns/spinner"
+ "github.com/pkg/errors"
+)
+
+const (
+ spinnerTextEllipsis = "..."
+ spinnerTextDone = "done"
+ spinnerTextFailed = "failed"
+ spinnerColor = "blue"
+)
+
+var messages = os.Stderr
+
+func Spinner(text string, fn func() error) error {
+ initialMsg := text + spinnerTextEllipsis + " "
+ doneMsg := initialMsg + spinnerTextDone + "\n"
+ failMsg := initialMsg + spinnerTextFailed + "\n"
+
+ return loading(initialMsg, doneMsg, failMsg, fn)
+}
+
+func loading(initialMsg, doneMsg, failMsg string, fn func() error) error {
+ done := make(chan struct{})
+ errc := make(chan error)
+ go func() {
+ defer close(done)
+
+ s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(messages))
+ s.Prefix = initialMsg
+ s.FinalMSG = doneMsg
+ s.HideCursor = true
+ s.Writer = messages
+
+ if err := s.Color(spinnerColor); err != nil {
+ panic(Error(err, "failed setting spinner color"))
+ }
+
+ s.Start()
+ err := <-errc
+ if err != nil {
+ s.FinalMSG = failMsg
+ }
+
+ s.Stop()
+ }()
+
+ err := fn()
+ errc <- err
+ <-done
+ return err
+}
+
+func Error(e error, message string) error {
+ return errors.Wrap(e, message)
+}
diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go
index 3718c7d813a..9c5fb3a12ac 100644
--- a/client/go/vespa/deploy.go
+++ b/client/go/vespa/deploy.go
@@ -327,12 +327,12 @@ func Submit(opts DeploymentOpts) error {
Header: make(http.Header),
}
request.Header.Set("Content-Type", writer.FormDataContentType())
- signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey)
- if err := signer.SignRequest(request); err != nil {
+ serviceDescription := "Submit service"
+ sigKeyId := opts.Deployment.Application.SerializedForm()
+ if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil {
return err
}
- serviceDescription := "Submit service"
- response, err := util.HttpDo(request, time.Minute*10, serviceDescription)
+ response, err := util.HttpDo(request, time.Minute*10, sigKeyId)
if err != nil {
return err
}
@@ -344,7 +344,7 @@ func checkDeploymentOpts(opts DeploymentOpts) error {
if !opts.ApplicationPackage.HasCertificate() {
return fmt.Errorf("%s: missing certificate in package", opts)
}
- if opts.APIKey == nil {
+ if !Auth0AccessTokenEnabled() && opts.APIKey == nil {
return fmt.Errorf("%s: missing api key", opts.String())
}
return nil
@@ -363,13 +363,11 @@ func uploadApplicationPackage(url *url.URL, opts DeploymentOpts) (int64, error)
Header: header,
Body: ioutil.NopCloser(zipReader),
}
- if opts.APIKey != nil {
- signer := NewRequestSigner(opts.Deployment.Application.SerializedForm(), opts.APIKey)
- if err := signer.SignRequest(request); err != nil {
- return 0, err
- }
- }
serviceDescription := "Deploy service"
+ sigKeyId := opts.Deployment.Application.SerializedForm()
+ if err := opts.Target.PrepareApiRequest(request, sigKeyId); err != nil {
+ return 0, err
+ }
response, err := util.HttpDo(request, time.Minute*10, serviceDescription)
if err != nil {
return 0, err
diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go
index 8a09440f5cc..f497bf5b3cd 100644
--- a/client/go/vespa/target.go
+++ b/client/go/vespa/target.go
@@ -6,13 +6,16 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
+ "github.com/vespa-engine/vespa/client/go/cli"
"io"
"io/ioutil"
"math"
"net/http"
"net/url"
+ "os"
"sort"
"strconv"
+ "strings"
"time"
"github.com/vespa-engine/vespa/client/go/util"
@@ -35,6 +38,7 @@ type Service struct {
BaseURL string
Name string
TLSOptions TLSOptions
+ Target *Target
}
// Target represents a Vespa platform, running named Vespa services.
@@ -47,6 +51,8 @@ type Target interface {
// PrintLog writes the logs of this deployment using given options to control output.
PrintLog(options LogOptions) error
+
+ PrepareApiRequest(req *http.Request, sigKeyId string) error
}
// TLSOptions configures the certificate to use for service requests.
@@ -66,11 +72,21 @@ type LogOptions struct {
Level int
}
+func Auth0AccessTokenEnabled() bool {
+ v, present := os.LookupEnv("VESPA_CLI_OAUTH2_DEVICE_FLOW")
+ if !present {
+ return false
+ }
+ return strings.ToLower(v) == "true" || v == "1" || v == ""
+}
+
type customTarget struct {
targetType string
baseURL string
}
+func (t *customTarget) PrepareApiRequest(req *http.Request, sigKeyId string) 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 {
@@ -192,8 +208,9 @@ type cloudTarget struct {
tlsOptions TLSOptions
logOptions LogOptions
- queryURL string
- documentURL string
+ queryURL string
+ documentURL string
+ authConfigPath string
}
func (t *cloudTarget) Type() string { return t.targetType }
@@ -221,6 +238,30 @@ func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64) (
return nil, fmt.Errorf("unknown service: %s", name)
}
+func (t *cloudTarget) PrepareApiRequest(req *http.Request, sigKeyId string) error {
+ if Auth0AccessTokenEnabled() {
+ if err := t.addAuth0AccessToken(req); err != nil {
+ return err
+ }
+ } else if t.apiKey != nil {
+ signer := NewRequestSigner(sigKeyId, t.apiKey)
+ if err := signer.SignRequest(req); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (t *cloudTarget) addAuth0AccessToken(request *http.Request) error {
+ c, err := cli.GetCli(t.authConfigPath)
+ tenant, err := c.PrepareTenant(cli.ContextWithCancel())
+ if err != nil {
+ return err
+ }
+ request.Header.Set("Authorization", "Bearer "+tenant.AccessToken)
+ return nil
+}
+
func (t *cloudTarget) logsURL() string {
return fmt.Sprintf("%s/application/v4/tenant/%s/application/%s/instance/%s/environment/%s/region/%s/logs",
t.apiURL,
@@ -233,7 +274,6 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
if err != nil {
return err
}
- signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey)
lastFrom := options.From
requestFunc := func() *http.Request {
fromMillis := lastFrom.Unix() * 1000
@@ -244,9 +284,7 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
q.Set("to", strconv.FormatInt(toMillis, 10))
}
req.URL.RawQuery = q.Encode()
- if err := signer.SignRequest(req); err != nil {
- panic(err)
- }
+ t.PrepareApiRequest(req, t.deployment.Application.SerializedForm())
return req
}
logFunc := func(status int, response []byte) (bool, error) {
@@ -280,16 +318,15 @@ func (t *cloudTarget) PrintLog(options LogOptions) error {
}
func (t *cloudTarget) waitForEndpoints(timeout time.Duration, runID int64) error {
- signer := NewRequestSigner(t.deployment.Application.SerializedForm(), t.apiKey)
if runID > 0 {
- if err := t.waitForRun(signer, runID, timeout); err != nil {
+ if err := t.waitForRun(runID, timeout); err != nil {
return err
}
}
- return t.discoverEndpoints(signer, timeout)
+ return t.discoverEndpoints(timeout)
}
-func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout time.Duration) 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,
@@ -303,7 +340,7 @@ func (t *cloudTarget) waitForRun(signer *RequestSigner, runID int64, timeout tim
q := req.URL.Query()
q.Set("after", strconv.FormatInt(lastID, 10))
req.URL.RawQuery = q.Encode()
- if err := signer.SignRequest(req); err != nil {
+ if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil {
panic(err)
}
return req
@@ -353,7 +390,7 @@ func (t *cloudTarget) printLog(response jobResponse, last int64) int64 {
return response.LastID
}
-func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Duration) error {
+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,
@@ -362,7 +399,7 @@ func (t *cloudTarget) discoverEndpoints(signer *RequestSigner, timeout time.Dura
if err != nil {
return err
}
- if err := signer.SignRequest(req); err != nil {
+ if err := t.PrepareApiRequest(req, t.deployment.Application.SerializedForm()); err != nil {
return err
}
var endpointURL string
@@ -409,14 +446,16 @@ func CustomTarget(baseURL string) Target {
}
// CloudTarget creates a Target for the Vespa Cloud platform.
-func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions) Target {
+func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions TLSOptions, logOptions LogOptions,
+ authConfigPath string) Target {
return &cloudTarget{
- apiURL: apiURL,
- targetType: cloudTargetType,
- deployment: deployment,
- apiKey: apiKey,
- tlsOptions: tlsOptions,
- logOptions: logOptions,
+ apiURL: apiURL,
+ targetType: cloudTargetType,
+ deployment: deployment,
+ apiKey: apiKey,
+ tlsOptions: tlsOptions,
+ logOptions: logOptions,
+ authConfigPath: authConfigPath,
}
}
diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go
index ed924059297..d4d23901513 100644
--- a/client/go/vespa/target_test.go
+++ b/client/go/vespa/target_test.go
@@ -143,7 +143,10 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target {
x509KeyPair, err := tls.X509KeyPair(kp.Certificate, kp.PrivateKey)
assert.Nil(t, err)
- apiKey, err := CreateAPIKey()
+ var apiKey []byte = nil
+ if !Auth0AccessTokenEnabled() {
+ apiKey, err = CreateAPIKey()
+ }
assert.Nil(t, err)
target := CloudTarget(
@@ -154,7 +157,7 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target {
},
apiKey,
TLSOptions{KeyPair: x509KeyPair},
- LogOptions{Writer: logWriter})
+ LogOptions{Writer: logWriter}, "")
if ct, ok := target.(*cloudTarget); ok {
ct.apiURL = url
} else {