diff options
author | Eirik Nygaard <eirik.nygaard@yahooinc.com> | 2022-02-24 14:41:05 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-24 14:41:05 +0100 |
commit | c23bc766b78769f2fbfb09452d3bbdde2b55e4e9 (patch) | |
tree | 381ffe59d528cceead34085f6ab30689b4b2d8c4 | |
parent | 88cfafdd9e56d97f021061ad3f460689fda62567 (diff) | |
parent | 831453b8c393bcdf31fb3d72faccc8a3035837b5 (diff) |
Merge pull request #21335 from vespa-engine/ean/device-flow-as-default
Ean/device flow as default
-rw-r--r-- | client/go/auth0/auth0.go | 11 | ||||
-rw-r--r-- | client/go/cmd/api_key.go | 21 | ||||
-rw-r--r-- | client/go/cmd/api_key_test.go | 14 | ||||
-rw-r--r-- | client/go/cmd/auth.go | 20 | ||||
-rw-r--r-- | client/go/cmd/cert.go | 15 | ||||
-rw-r--r-- | client/go/cmd/cert_test.go | 28 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 18 | ||||
-rw-r--r-- | client/go/cmd/config.go | 49 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 70 | ||||
-rw-r--r-- | client/go/cmd/curl.go | 5 | ||||
-rw-r--r-- | client/go/cmd/curl_test.go | 6 | ||||
-rw-r--r-- | client/go/cmd/helpers.go | 41 | ||||
-rw-r--r-- | client/go/cmd/log_test.go | 9 | ||||
-rw-r--r-- | client/go/cmd/login.go | 11 | ||||
-rw-r--r-- | client/go/cmd/prod_test.go | 4 | ||||
-rw-r--r-- | client/go/cmd/root.go | 11 | ||||
-rw-r--r-- | client/go/cmd/test_test.go | 11 | ||||
-rw-r--r-- | client/go/vespa/deploy.go | 5 | ||||
-rw-r--r-- | client/go/vespa/target.go | 41 | ||||
-rw-r--r-- | client/go/vespa/target_test.go | 7 |
20 files changed, 242 insertions, 155 deletions
diff --git a/client/go/auth0/auth0.go b/client/go/auth0/auth0.go index 5acd4354274..bcd70c4f54a 100644 --- a/client/go/auth0/auth0.go +++ b/client/go/auth0/auth0.go @@ -129,6 +129,7 @@ func (a *Auth0) IsLoggedIn() bool { } // Check if token is valid. + // TODO: Choose issuer based on system if err = jwt.Validate(token, jwt.WithIssuer("https://vespa-cd.auth0.com/")); err != nil { return false } @@ -233,6 +234,16 @@ func (a *Auth0) getSystem() (*System, error) { return s, nil } +// HasSystem checks if the system is configured +// TODO: Used to print deprecation warning if we fall back to use tenant API key. +// Remove when this is not longer needed. +func (a *Auth0) HasSystem() bool { + if _, err := a.getSystem(); err != nil { + return false + } + return true +} + // AddSystem assigns an existing, or new System. This is expected to be called // after a login has completed. func (a *Auth0) AddSystem(s *System) error { diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go index faab67781d1..5cc1dab8a35 100644 --- a/client/go/cmd/api_key.go +++ b/client/go/cmd/api_key.go @@ -38,14 +38,12 @@ always be used. It's not possible to specify a tenant-specific key.` func init() { apiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key") apiKeyCmd.MarkPersistentFlagRequired(applicationFlag) + deprecatedApiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key") + deprecatedApiKeyCmd.MarkPersistentFlagRequired(applicationFlag) } func apiKeyExample() string { - if vespa.Auth0AccessTokenEnabled() { - return "$ vespa auth api-key -a my-tenant.my-app.my-instance" - } else { - return "$ vespa api-key -a my-tenant.my-app.my-instance" - } + return "$ vespa auth api-key -a my-tenant.my-app.my-instance" } var apiKeyCmd = &cobra.Command{ @@ -94,18 +92,7 @@ func doApiKey(_ *cobra.Command, _ []string) error { } if err := ioutil.WriteFile(apiKeyFile, apiKey, 0600); err == nil { printSuccess("API private key written to ", apiKeyFile) - printPublicKey(apiKeyFile, app.Tenant) - if vespa.Auth0AccessTokenEnabled() { - if err == nil { - if err := cfg.Set(cloudAuthFlag, "api-key"); err != nil { - return fmt.Errorf("could not write config: %w", err) - } - if err := cfg.Write(); err != nil { - return err - } - } - } - return nil + return printPublicKey(apiKeyFile, app.Tenant) } else { return fmt.Errorf("failed to write: %s: %w", apiKeyFile, err) } diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go index df3cf144180..935b8676c09 100644 --- a/client/go/cmd/api_key_test.go +++ b/client/go/cmd/api_key_test.go @@ -11,13 +11,23 @@ import ( ) func TestAPIKey(t *testing.T) { + t.Run("auth api-key", func(t *testing.T) { + testAPIKey(t, []string{"auth", "api-key"}) + }) + t.Run("api-key (deprecated)", func(t *testing.T) { + testAPIKey(t, []string{"api-key"}) + }) +} + +func testAPIKey(t *testing.T, subcommand []string) { homeDir := filepath.Join(t.TempDir(), ".vespa") keyFile := filepath.Join(homeDir, "t1.api-key.pem") - out, _ := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) + 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") - out, outErr := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, homeDir: homeDir}, t, nil) + out, outErr := execute(command{args: args, homeDir: homeDir}, t, nil) assert.Contains(t, outErr, "Error: refusing to overwrite "+keyFile+"\nHint: Use -f to overwrite it\n") assert.Contains(t, out, "This is your public key") } diff --git a/client/go/cmd/auth.go b/client/go/cmd/auth.go index 68d7fa00fdf..592c18c8b22 100644 --- a/client/go/cmd/auth.go +++ b/client/go/cmd/auth.go @@ -4,22 +4,16 @@ import ( "fmt" "github.com/spf13/cobra" - "github.com/vespa-engine/vespa/client/go/vespa" ) func init() { - if vespa.Auth0AccessTokenEnabled() { - rootCmd.AddCommand(authCmd) - rootCmd.AddCommand(deprecatedCertCmd) - rootCmd.AddCommand(deprecatedApiKeyCmd) - authCmd.AddCommand(certCmd) - authCmd.AddCommand(apiKeyCmd) - authCmd.AddCommand(loginCmd) - authCmd.AddCommand(logoutCmd) - } else { - rootCmd.AddCommand(certCmd) - rootCmd.AddCommand(apiKeyCmd) - } + rootCmd.AddCommand(authCmd) + rootCmd.AddCommand(deprecatedCertCmd) + rootCmd.AddCommand(deprecatedApiKeyCmd) + authCmd.AddCommand(certCmd) + authCmd.AddCommand(apiKeyCmd) + authCmd.AddCommand(loginCmd) + authCmd.AddCommand(logoutCmd) } var authCmd = &cobra.Command{ diff --git a/client/go/cmd/cert.go b/client/go/cmd/cert.go index f338a1ba81a..97147d1eec0 100644 --- a/client/go/cmd/cert.go +++ b/client/go/cmd/cert.go @@ -43,14 +43,12 @@ application-specific key.` func init() { certCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key") certCmd.MarkPersistentFlagRequired(applicationFlag) + deprecatedCertCmd.Flags().BoolVarP(&overwriteCertificate, "force", "f", false, "Force overwrite of existing certificate and private key") + deprecatedCertCmd.MarkPersistentFlagRequired(applicationFlag) } func certExample() string { - if vespa.Auth0AccessTokenEnabled() { - return "$ vespa auth cert -a my-tenant.my-app.my-instance" - } else { - return "$ vespa cert -a my-tenant.my-app.my-instance" - } + return "$ vespa auth cert -a my-tenant.my-app.my-instance" } var certCmd = &cobra.Command{ @@ -112,12 +110,7 @@ func doCert(_ *cobra.Command, args []string) error { } } if pkg.IsZip() { - var hint string - if vespa.Auth0AccessTokenEnabled() { - hint = "Try running 'mvn clean' before 'vespa auth cert', and then 'mvn package'" - } else { - hint = "Try running 'mvn clean' before 'vespa cert', and then 'mvn package'" - } + hint := "Try running 'mvn clean' before 'vespa auth cert', and then 'mvn package'" return errHint(fmt.Errorf("cannot add certificate to compressed application package %s", pkg.Path), hint) } diff --git a/client/go/cmd/cert_test.go b/client/go/cmd/cert_test.go index aae40db43a6..4a115f54c65 100644 --- a/client/go/cmd/cert_test.go +++ b/client/go/cmd/cert_test.go @@ -14,9 +14,19 @@ import ( ) func TestCert(t *testing.T) { + t.Run("auth cert", func(t *testing.T) { + testCert(t, []string{"auth", "cert"}) + }) + t.Run("cert (deprecated)", func(t *testing.T) { + testCert(t, []string{"cert"}) + }) +} + +func testCert(t *testing.T, subcommand []string) { homeDir := filepath.Join(t.TempDir(), ".vespa") pkgDir := mockApplicationPackage(t, false) - out, _ := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + args := append(subcommand, "-a", "t1.a1.i1", pkgDir) + out, _ := execute(command{args: args, homeDir: homeDir}, t, nil) app, err := vespa.ApplicationFromString("t1.a1.i1") assert.Nil(t, err) @@ -28,11 +38,21 @@ func TestCert(t *testing.T) { assert.Equal(t, fmt.Sprintf("Success: Certificate written to %s\nSuccess: Certificate written to %s\nSuccess: Private key written to %s\n", pkgCertificate, certificate, privateKey), out) + args = append(subcommand, "-a", "t1.a1.i1", pkgDir) _, outErr := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) assert.Contains(t, outErr, fmt.Sprintf("Error: application package %s already contains a certificate", appDir)) } func TestCertCompressedPackage(t *testing.T) { + t.Run("auth cert", func(t *testing.T) { + testCertCompressedPackage(t, []string{"auth", "cert"}) + }) + t.Run("cert (deprecated)", func(t *testing.T) { + testCertCompressedPackage(t, []string{"cert"}) + }) +} + +func testCertCompressedPackage(t *testing.T, subcommand []string) { homeDir := filepath.Join(t.TempDir(), ".vespa") pkgDir := mockApplicationPackage(t, true) zipFile := filepath.Join(pkgDir, "target", "application.zip") @@ -41,13 +61,15 @@ func TestCertCompressedPackage(t *testing.T) { _, err = os.Create(zipFile) assert.Nil(t, err) - _, outErr := execute(command{args: []string{"cert", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + args := append(subcommand, "-a", "t1.a1.i1", pkgDir) + _, outErr := execute(command{args: args, homeDir: homeDir}, t, nil) assert.Contains(t, outErr, "Error: cannot add certificate to compressed application package") err = os.Remove(zipFile) assert.Nil(t, err) - out, _ := execute(command{args: []string{"cert", "-f", "-a", "t1.a1.i1", pkgDir}, homeDir: homeDir}, t, nil) + args = append(subcommand, "-f", "-a", "t1.a1.i1", pkgDir) + out, _ := execute(command{args: args, homeDir: homeDir}, t, nil) assert.Contains(t, out, "Success: Certificate written to") assert.Contains(t, out, "Success: Private key written to") } diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go index 82682b3b355..aaa1ba5d4ea 100644 --- a/client/go/cmd/command_tester.go +++ b/client/go/cmd/command_tester.go @@ -28,6 +28,7 @@ type command struct { stdin io.ReadWriter args []string moreArgs []string + env map[string]string failTestOnError bool } @@ -47,6 +48,23 @@ func execute(cmd command, t *testing.T, client *mockHttpClient) (string, string) if client != nil { util.ActiveHttpClient = client } + originalEnv := map[string]string{} + for k, v := range cmd.env { + value, ok := os.LookupEnv(k) + if ok { + originalEnv[k] = value + } + os.Setenv(k, v) + } + defer func() { + for k, _ := range cmd.env { + if v, ok := originalEnv[k]; ok { + os.Setenv(k, v) + } else { + os.Unsetenv(k) + } + } + }() // Set Vespa CLI directories. Use a separate one per test if none is specified if cmd.homeDir == "" { diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go index a195a3e1975..9d42f0683fd 100644 --- a/client/go/cmd/config.go +++ b/client/go/cmd/config.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/vespa-engine/vespa/client/go/auth0" "github.com/vespa-engine/vespa/client/go/util" "github.com/vespa-engine/vespa/client/go/vespa" ) @@ -26,7 +27,10 @@ const ( configType = "yaml" ) -var flagToConfigBindings map[string]*cobra.Command = make(map[string]*cobra.Command) +var ( + flagToConfigBindings map[string]*cobra.Command = make(map[string]*cobra.Command) + envToConfigBindings map[string]string = make(map[string]string) +) func init() { rootCmd.AddCommand(configCmd) @@ -179,19 +183,42 @@ func (c *Config) X509KeyPair(app vespa.ApplicationID) (KeyPair, error) { } func (c *Config) APIKeyPath(tenantName string) string { - if override, ok := os.LookupEnv("VESPA_CLI_API_KEY_FILE"); ok { + if override, err := c.Get(apiKeyFileFlag); err == nil && override != "" { return override } return filepath.Join(c.Home, tenantName+".api-key.pem") } func (c *Config) ReadAPIKey(tenantName string) ([]byte, error) { - if override, ok := os.LookupEnv("VESPA_CLI_API_KEY"); ok { + if override, err := c.Get(apiKeyFlag); err == nil && override != "" { return []byte(override), nil } return ioutil.ReadFile(c.APIKeyPath(tenantName)) } +// 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 { + if _, err := c.Get(apiKeyFlag); err == nil { + return true + } + if _, err := c.Get(apiKeyFileFlag); err == nil { + return true + } + + // 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()) + 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 false +} + func (c *Config) AuthConfigPath() string { return filepath.Join(c.Home, "auth.json") } @@ -234,6 +261,9 @@ func (c *Config) load() error { for option, command := range flagToConfigBindings { viper.BindPFlag(option, command.PersistentFlags().Lookup(option)) } + for option, env := range envToConfigBindings { + viper.BindEnv(option, env) + } err := viper.ReadInConfig() if _, ok := err.(viper.ConfigFileNotFoundError); ok { return nil @@ -279,12 +309,9 @@ func (c *Config) Set(option, value string) error { viper.Set(option, value) return nil } - case cloudAuthFlag: - switch value { - case "access-token", "api-key": - viper.Set(option, value) - return nil - } + case apiKeyFileFlag: + viper.Set(option, value) + return nil } return fmt.Errorf("invalid option or value: %q: %q", option, value) } @@ -302,3 +329,7 @@ func printOption(cfg *Config, option string) { func bindFlagToConfig(option string, command *cobra.Command) { flagToConfigBindings[option] = command } + +func bindEnvToConfig(option string, env string) { + envToConfigBindings[option] = env +} diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go index ba0df781715..1d8fc95d765 100644 --- a/client/go/cmd/config_test.go +++ b/client/go/cmd/config_test.go @@ -2,10 +2,14 @@ package cmd import ( + "io/ioutil" + "os" "path/filepath" "testing" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfig(t *testing.T) { @@ -18,21 +22,31 @@ func TestConfig(t *testing.T) { assertConfigCommand(t, "", homeDir, "config", "set", "target", "http://127.0.0.1:8080") assertConfigCommand(t, "", homeDir, "config", "set", "target", "https://127.0.0.1") assertConfigCommand(t, "target = https://127.0.0.1\n", homeDir, "config", "get", "target") + assertEnvConfigCommand(t, "api-key-file = /tmp/private.key\n", homeDir, map[string]string{"VESPA_CLI_API_KEY_FILE": "/tmp/private.key"}, "config", "get", "api-key-file") + assertConfigCommand(t, "", homeDir, "config", "set", "api-key-file", "/tmp/private.key") + assertConfigCommand(t, "api-key-file = /tmp/private.key\n", homeDir, "config", "get", "api-key-file") assertConfigCommandErr(t, "Error: invalid application: \"foo\"\n", homeDir, "config", "set", "application", "foo") assertConfigCommand(t, "application = <unset>\n", homeDir, "config", "get", "application") assertConfigCommand(t, "", homeDir, "config", "set", "application", "t1.a1.i1") assertConfigCommand(t, "application = t1.a1.i1\n", homeDir, "config", "get", "application") - assertConfigCommand(t, "application = t1.a1.i1\ncolor = auto\nquiet = false\ntarget = https://127.0.0.1\nwait = 0\n", homeDir, "config", "get") + assertConfigCommand(t, "api-key-file = /tmp/private.key\napplication = t1.a1.i1\ncolor = auto\nquiet = false\ntarget = https://127.0.0.1\nwait = 0\n", homeDir, "config", "get") assertConfigCommand(t, "", homeDir, "config", "set", "wait", "60") assertConfigCommandErr(t, "Error: wait option must be an integer >= 0, got \"foo\"\n", homeDir, "config", "set", "wait", "foo") assertConfigCommand(t, "wait = 60\n", homeDir, "config", "get", "wait") + + // Avoid leaking config settings to other tests + viper.Reset() } func assertConfigCommand(t *testing.T, expected, homeDir string, args ...string) { - out, _ := execute(command{homeDir: homeDir, args: args}, t, nil) + assertEnvConfigCommand(t, expected, homeDir, nil, args...) +} + +func assertEnvConfigCommand(t *testing.T, expected, homeDir string, env map[string]string, args ...string) { + out, _ := execute(command{homeDir: homeDir, env: env, args: args}, t, nil) assert.Equal(t, expected, out) } @@ -40,3 +54,55 @@ func assertConfigCommandErr(t *testing.T, expected, homeDir string, args ...stri _, outErr := execute(command{homeDir: homeDir, args: args}, t, nil) assert.Equal(t, expected, outErr) } + +func withEnv(key, value string, fn func()) { + orig, ok := os.LookupEnv(key) + os.Setenv(key, value) + fn() + if ok { + os.Setenv(key, orig) + } else { + os.Unsetenv(key) + } +} + +func TestUseAPIKey(t *testing.T) { + homeDir := t.TempDir() + c := Config{Home: homeDir} + + assert.False(t, c.UseAPIKey("t1")) + + c.Set(apiKeyFileFlag, "/tmp/foo") + assert.True(t, c.UseAPIKey("t1")) + c.Set(apiKeyFileFlag, "") + + withEnv("VESPA_CLI_API_KEY", "...", func() { + require.Nil(t, c.load()) + assert.True(t, c.UseAPIKey("t1")) + }) + + // Test deprecated functionality + authContent := ` +{ + "version": 1, + "providers": { + "auth0": { + "version": 1, + "systems": { + "public": { + "access_token": "...", + "scopes": ["openid", "offline_access"], + "expires_at": "2030-01-01T01:01:01.000001+01:00" + } + } + } + } +}` + 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")) + require.Nil(t, ioutil.WriteFile(filepath.Join(homeDir, "auth.json"), []byte(authContent), 0600)) + assert.False(t, c.UseAPIKey("t2")) + }) +} diff --git a/client/go/cmd/curl.go b/client/go/cmd/curl.go index 47d0dcde95b..b66780780ed 100644 --- a/client/go/cmd/curl.go +++ b/client/go/cmd/curl.go @@ -2,7 +2,6 @@ package cmd import ( - "errors" "fmt" "log" "os" @@ -11,7 +10,6 @@ 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 @@ -64,9 +62,6 @@ $ vespa curl -- -v --data-urlencode "yql=select * from music where album contain return err } if t.Type() == "cloud" { - if !vespa.Auth0AccessTokenEnabled() { - return errors.New("accessing control plane using curl subcommand is only supported for Auth0 device flow") - } if err := addCloudAuth0Authentication(cfg, c); err != nil { return err } diff --git a/client/go/cmd/curl_test.go b/client/go/cmd/curl_test.go index e593f48a390..5709096fe37 100644 --- a/client/go/cmd/curl_test.go +++ b/client/go/cmd/curl_test.go @@ -14,12 +14,12 @@ func TestCurl(t *testing.T) { httpClient := &mockHttpClient{} out, _ := execute(command{homeDir: homeDir, args: []string{"curl", "-n", "-a", "t1.a1.i1", "--", "-v", "--data-urlencode", "arg=with space", "/search"}}, t, httpClient) - expected := fmt.Sprintf("curl --key %s --cert %s -v --data-urlencode 'arg=with space' https://127.0.0.1:8080/search\n", + expected := fmt.Sprintf("curl --key %s --cert %s -v --data-urlencode 'arg=with space' http://127.0.0.1:8080/search\n", filepath.Join(homeDir, "t1.a1.i1", "data-plane-private-key.pem"), filepath.Join(homeDir, "t1.a1.i1", "data-plane-public-cert.pem")) assert.Equal(t, expected, out) - out, _ = execute(command{homeDir: homeDir, args: []string{"curl", "-s", "deploy", "-n", "/application/v4/tenant/foo"}}, t, httpClient) - expected = "curl https://127.0.0.1:19071/application/v4/tenant/foo\n" + out, _ = execute(command{homeDir: homeDir, args: []string{"curl", "-a", "t1.a1.i1", "-s", "deploy", "-n", "/application/v4/tenant/foo"}}, t, httpClient) + expected = "curl http://127.0.0.1:19071/application/v4/tenant/foo\n" assert.Equal(t, expected, out) } diff --git a/client/go/cmd/helpers.go b/client/go/cmd/helpers.go index 905e51dda4f..547126a8156 100644 --- a/client/go/cmd/helpers.go +++ b/client/go/cmd/helpers.go @@ -189,34 +189,15 @@ func createTarget() (vespa.Target, error) { } var apiKey []byte = nil - apiKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) - if !vespa.Auth0AccessTokenEnabled() && endpoints == nil { + if cfg.UseAPIKey(deployment.Application.Tenant) { + apiKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) if err != nil { - return nil, errHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + return nil, err } } kp, err := cfg.X509KeyPair(deployment.Application) if err != nil { - var hint string - if vespa.Auth0AccessTokenEnabled() { - hint = "Deployment to cloud requires a certificate. Try 'vespa auth cert'" - } else { - hint = "Deployment to cloud requires a certificate. Try 'vespa cert'" - } - return nil, errHint(err, hint) - } - var cloudAuth string - if vespa.Auth0AccessTokenEnabled() { - cloudAuth, err = cfg.Get(cloudAuthFlag) - if err != nil { - if apiKey != nil { - cloudAuth = "api-key" - } else { - cloudAuth = "access-token" - } - } - } else { - cloudAuth = "" + return nil, errHint(err, "Deployment to cloud requires a certificate. Try 'vespa auth cert'") } return vespa.CloudTarget( @@ -234,7 +215,6 @@ func createTarget() (vespa.Target, error) { }, cfg.AuthConfigPath(), getSystemName(), - cloudAuth, endpoints, ), nil } @@ -270,18 +250,13 @@ func getDeploymentOpts(cfg *Config, pkg vespa.ApplicationPackage, target vespa.T return vespa.DeploymentOpts{}, err } if !opts.ApplicationPackage.HasCertificate() { - var hint string - if vespa.Auth0AccessTokenEnabled() { - hint = "Try 'vespa auth cert'" - } else { - hint = "Try 'vespa cert'" - } + hint := "Try 'vespa auth cert'" return vespa.DeploymentOpts{}, errHint(fmt.Errorf("missing certificate in application package"), "Applications in Vespa Cloud require a certificate", hint) } - opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) - if !vespa.Auth0AccessTokenEnabled() { + if cfg.UseAPIKey(deployment.Application.Tenant) { + opts.APIKey, err = cfg.ReadAPIKey(deployment.Application.Tenant) if err != nil { - return vespa.DeploymentOpts{}, errHint(err, "Deployment to cloud requires an API key. Try 'vespa api-key'") + return vespa.DeploymentOpts{}, err } } opts.Deployment = deployment diff --git a/client/go/cmd/log_test.go b/client/go/cmd/log_test.go index 9d895d1f244..24ccd9e266d 100644 --- a/client/go/cmd/log_test.go +++ b/client/go/cmd/log_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/vespa-engine/vespa/client/go/build" ) @@ -16,7 +17,8 @@ func TestLog(t *testing.T) { httpClient.NextResponse(200, `1632738690.905535 host1a.dev.aws-us-east-1c 806/53 logserver-container Container.com.yahoo.container.jdisc.ConfiguredApplication info Switching to the latest deployed set of configurations and components. Application config generation: 52532`) 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{"auth", "api-key"}}, 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) out, _ := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T10:00:00Z", "--to", "2021-09-27T11:00:00Z"}}, t, httpClient) @@ -26,6 +28,7 @@ func TestLog(t *testing.T) { _, errOut := execute(command{homeDir: homeDir, args: []string{"log", "--from", "2021-09-27T13:12:49Z", "--to", "2021-09-27T13:15:00", "1h"}}, t, httpClient) assert.Equal(t, "Error: invalid period: cannot combine --from/--to with relative value: 1h\n", errOut) + viper.Reset() } func TestLogOldClient(t *testing.T) { @@ -37,11 +40,13 @@ func TestLogOldClient(t *testing.T) { httpClient.NextResponse(200, `{"minVersion": "8.0.0"}`) 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{"auth", "api-key"}}, 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) 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" assert.Equal(t, expected, errOut) build.Version = buildVersion + viper.Reset() } diff --git a/client/go/cmd/login.go b/client/go/cmd/login.go index f4438cdbb24..8787f1f80f5 100644 --- a/client/go/cmd/login.go +++ b/client/go/cmd/login.go @@ -3,7 +3,6 @@ package cmd import ( "github.com/spf13/cobra" "github.com/vespa-engine/vespa/client/go/auth0" - "github.com/vespa-engine/vespa/client/go/vespa" ) var loginCmd = &cobra.Command{ @@ -24,16 +23,6 @@ var loginCmd = &cobra.Command{ return err } _, err = auth0.RunLogin(ctx, a, false) - if vespa.Auth0AccessTokenEnabled() { - if err == nil { - if err := cfg.Set(cloudAuthFlag, "access-token"); err != nil { - return err - } - if err := cfg.Write(); err != nil { - return err - } - } - } return err }, } diff --git a/client/go/cmd/prod_test.go b/client/go/cmd/prod_test.go index 4e635f87a75..8fa9ef401b5 100644 --- a/client/go/cmd/prod_test.go +++ b/client/go/cmd/prod_test.go @@ -166,7 +166,7 @@ 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"}}, t, httpClient) + out, err := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem")}}, t, httpClient) assert.Equal(t, "", err) 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") @@ -191,7 +191,7 @@ 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", pkgDir}}, t, httpClient) + out, _ := execute(command{homeDir: homeDir, args: []string{"prod", "submit", "-k", filepath.Join(homeDir, "t1.api-key.pem"), pkgDir}}, t, httpClient) 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 0f4bef5595f..f5a846536c5 100644 --- a/client/go/cmd/root.go +++ b/client/go/cmd/root.go @@ -53,6 +53,8 @@ Vespa documentation: https://docs.vespa.ai`, waitSecsArg int colorArg string quietArg bool + apiKeyFileArg string + apiKeyArg string stdin io.ReadWriter = os.Stdin color = aurora.NewAurora(false) @@ -66,7 +68,8 @@ const ( waitFlag = "wait" colorFlag = "color" quietFlag = "quiet" - cloudAuthFlag = "cloudAuth" + apiKeyFileFlag = "api-key-file" + apiKeyFlag = "api-key" ) func isTerminal() bool { @@ -115,11 +118,17 @@ func init() { rootCmd.PersistentFlags().IntVarP(&waitSecsArg, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready") rootCmd.PersistentFlags().StringVarP(&colorArg, colorFlag, "c", "auto", "Whether to use colors in output. Can be \"auto\", \"never\" or \"always\"") rootCmd.PersistentFlags().BoolVarP(&quietArg, quietFlag, "q", false, "Quiet mode. Only errors are printed.") + rootCmd.PersistentFlags().StringVarP(&apiKeyFileArg, apiKeyFileFlag, "k", "", "Path to API key used for deployment authentication") + bindFlagToConfig(targetFlag, rootCmd) bindFlagToConfig(applicationFlag, rootCmd) bindFlagToConfig(waitFlag, rootCmd) bindFlagToConfig(colorFlag, rootCmd) bindFlagToConfig(quietFlag, rootCmd) + bindFlagToConfig(apiKeyFileFlag, rootCmd) + + bindEnvToConfig(apiKeyFlag, "VESPA_CLI_API_KEY") + bindEnvToConfig(apiKeyFileFlag, "VESPA_CLI_API_KEY_FILE") } // errHint creates a new CLI error, with optional hints that will be printed after the error diff --git a/client/go/cmd/test_test.go b/client/go/cmd/test_test.go index 2c59bbbc030..9c161c091ec 100644 --- a/client/go/cmd/test_test.go +++ b/client/go/cmd/test_test.go @@ -13,10 +13,10 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vespa-engine/vespa/client/go/util" "github.com/vespa-engine/vespa/client/go/vespa" - - "github.com/stretchr/testify/assert" ) func TestSuite(t *testing.T) { @@ -93,7 +93,12 @@ func TestSingleTest(t *testing.T) { } func TestSingleTestWithCloudAndEndpoints(t *testing.T) { - cmd := command{args: []string{"test", "testdata/tests/system-test/test.json", "-t", "cloud", "-a", "t.a.i"}} + apiKey, err := vespa.CreateAPIKey() + require.Nil(t, err) + cmd := command{ + args: []string{"test", "testdata/tests/system-test/test.json", "-t", "cloud", "-a", "t.a.i"}, + env: map[string]string{"VESPA_CLI_API_KEY": string(apiKey)}, + } cmd.homeDir = filepath.Join(t.TempDir(), ".vespa") os.MkdirAll(cmd.homeDir, 0700) keyFile := filepath.Join(cmd.homeDir, "key") diff --git a/client/go/vespa/deploy.go b/client/go/vespa/deploy.go index cac03067a69..aed430399a0 100644 --- a/client/go/vespa/deploy.go +++ b/client/go/vespa/deploy.go @@ -348,11 +348,6 @@ func checkDeploymentOpts(opts DeploymentOpts) error { if !opts.ApplicationPackage.HasCertificate() { return fmt.Errorf("%s: missing certificate in package", opts) } - if !Auth0AccessTokenEnabled() { - if opts.APIKey == nil { - return fmt.Errorf("%s: missing api key", opts.String()) - } - } return nil } diff --git a/client/go/vespa/target.go b/client/go/vespa/target.go index ae4934d788d..204dbc143c6 100644 --- a/client/go/vespa/target.go +++ b/client/go/vespa/target.go @@ -12,10 +12,8 @@ import ( "math" "net/http" "net/url" - "os" "sort" "strconv" - "strings" "time" "github.com/vespa-engine/vespa/client/go/auth0" @@ -35,6 +33,11 @@ const ( retryInterval = 2 * time.Second ) +const ( + CloudAuthApiKey = "api-key" + CloudAuthAccessToken = "access-token" +) + // Service represents a Vespa service. type Service struct { BaseURL string @@ -77,14 +80,6 @@ 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 @@ -218,7 +213,6 @@ type cloudTarget struct { urlsByCluster map[string]string authConfigPath string systemName string - cloudAuth string } func (t *cloudTarget) resolveEndpoint(cluster string) (string, error) { @@ -272,26 +266,18 @@ func (t *cloudTarget) Service(name string, timeout time.Duration, runID int64, c 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 Auth0AccessTokenEnabled() { - if t.cloudAuth == "access-token" { - if err := t.addAuth0AccessToken(req); err != nil { - return err - } - } else { - if t.apiKey == nil { - return fmt.Errorf("Deployment to cloud requires an API key. Try 'vespa api-key'") - } - signer := NewRequestSigner(sigKeyId, t.apiKey) - if err := signer.SignRequest(req); err != nil { - return err - } - } - } else { + if t.apiKey != nil { signer := NewRequestSigner(sigKeyId, t.apiKey) if err := signer.SignRequest(req); err != nil { return err } + } else { + if err := t.addAuth0AccessToken(req); err != nil { + return err + } } return nil } @@ -528,7 +514,7 @@ 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, - authConfigPath string, systemName string, cloudAuth string, urlsByCluster map[string]string) Target { + authConfigPath string, systemName string, urlsByCluster map[string]string) Target { return &cloudTarget{ apiURL: apiURL, targetType: cloudTargetType, @@ -538,7 +524,6 @@ func CloudTarget(apiURL string, deployment Deployment, apiKey []byte, tlsOptions logOptions: logOptions, authConfigPath: authConfigPath, systemName: systemName, - cloudAuth: cloudAuth, urlsByCluster: urlsByCluster, } } diff --git a/client/go/vespa/target_test.go b/client/go/vespa/target_test.go index 5aa20b22465..8391655eaf7 100644 --- a/client/go/vespa/target_test.go +++ b/client/go/vespa/target_test.go @@ -166,16 +166,13 @@ func createCloudTarget(t *testing.T, url string, logWriter io.Writer) Target { x509KeyPair, err := tls.X509KeyPair(kp.Certificate, kp.PrivateKey) assert.Nil(t, err) - var apiKey []byte = nil - if !Auth0AccessTokenEnabled() { - apiKey, err = CreateAPIKey() - } + 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) + }, apiKey, TLSOptions{KeyPair: x509KeyPair}, LogOptions{Writer: logWriter}, "", "", nil) if ct, ok := target.(*cloudTarget); ok { ct.apiURL = url } else { |