aboutsummaryrefslogtreecommitdiffstats
path: root/client/go/internal/cli/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'client/go/internal/cli/cmd')
-rw-r--r--client/go/internal/cli/cmd/cert.go9
-rw-r--r--client/go/internal/cli/cmd/config.go91
-rw-r--r--client/go/internal/cli/cmd/config_test.go119
-rw-r--r--client/go/internal/cli/cmd/curl.go12
-rw-r--r--client/go/internal/cli/cmd/feed.go92
-rw-r--r--client/go/internal/cli/cmd/root.go98
-rw-r--r--client/go/internal/cli/cmd/test.go2
-rw-r--r--client/go/internal/cli/cmd/testutil_test.go21
8 files changed, 310 insertions, 134 deletions
diff --git a/client/go/internal/cli/cmd/cert.go b/client/go/internal/cli/cmd/cert.go
index 7f79a9db358..48bad974c3f 100644
--- a/client/go/internal/cli/cmd/cert.go
+++ b/client/go/internal/cli/cmd/cert.go
@@ -34,13 +34,18 @@ package specified as an argument to this command (default '.').
It's possible to override the private key and certificate used through
environment variables. This can be useful in continuous integration systems.
-Example of setting the certificate and key in-line:
+It's also possible override the CA certificate which can be useful when using self-signed certificates with a
+self-hosted Vespa service. See https://docs.vespa.ai/en/mtls.html for more information.
+Example of setting the CA certificate, certificate and key in-line:
+
+ export VESPA_CLI_DATA_PLANE_CA_CERT="my CA cert"
export VESPA_CLI_DATA_PLANE_CERT="my cert"
export VESPA_CLI_DATA_PLANE_KEY="my private key"
-Example of loading certificate and key from custom paths:
+Example of loading CA certificate, certificate and key from custom paths:
+ export VESPA_CLI_DATA_PLANE_CA_CERT_FILE=/path/to/cacert
export VESPA_CLI_DATA_PLANE_CERT_FILE=/path/to/cert
export VESPA_CLI_DATA_PLANE_KEY_FILE=/path/to/key
diff --git a/client/go/internal/cli/cmd/config.go b/client/go/internal/cli/cmd/config.go
index 2d32c454842..e2132814386 100644
--- a/client/go/internal/cli/cmd/config.go
+++ b/client/go/internal/cli/cmd/config.go
@@ -19,7 +19,6 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
- "github.com/vespa-engine/vespa/client/go/internal/cli/auth/auth0"
"github.com/vespa-engine/vespa/client/go/internal/cli/config"
"github.com/vespa-engine/vespa/client/go/internal/vespa"
)
@@ -250,9 +249,10 @@ type Config struct {
}
type KeyPair struct {
- KeyPair tls.Certificate
- CertificateFile string
- PrivateKeyFile string
+ KeyPair tls.Certificate
+ RootCertificates []byte
+ CertificateFile string
+ PrivateKeyFile string
}
func loadConfig(environment map[string]string, flags map[string]*pflag.Flag) (*Config, error) {
@@ -392,6 +392,10 @@ func (c *Config) deploymentIn(system vespa.System) (vespa.Deployment, error) {
return vespa.Deployment{System: system, Application: app, Zone: zone}, nil
}
+func (c *Config) caCertificatePath() string {
+ return c.environment["VESPA_CLI_DATA_PLANE_CA_CERT_FILE"]
+}
+
func (c *Config) certificatePath(app vespa.ApplicationID, targetType string) (string, error) {
if override, ok := c.environment["VESPA_CLI_DATA_PLANE_CERT_FILE"]; ok {
return override, nil
@@ -412,50 +416,68 @@ func (c *Config) privateKeyPath(app vespa.ApplicationID, targetType string) (str
return c.applicationFilePath(app, "data-plane-private-key.pem")
}
-func (c *Config) x509KeyPair(app vespa.ApplicationID, targetType string) (KeyPair, error) {
+func (c *Config) readTLSOptions(app vespa.ApplicationID, targetType string) (vespa.TLSOptions, error) {
+ _, trustAll := c.environment["VESPA_CLI_DATA_PLANE_TRUST_ALL"]
cert, certOk := c.environment["VESPA_CLI_DATA_PLANE_CERT"]
key, keyOk := c.environment["VESPA_CLI_DATA_PLANE_KEY"]
- var (
- kp tls.Certificate
- err error
- certFile string
- keyFile string
- )
+ caCertText, caCertOk := c.environment["VESPA_CLI_DATA_PLANE_CA_CERT"]
+ options := vespa.TLSOptions{TrustAll: trustAll}
+ // CA certificate
+ if caCertOk {
+ options.CACertificate = []byte(caCertText)
+ } else {
+ caCertFile := c.caCertificatePath()
+ if caCertFile != "" {
+ b, err := os.ReadFile(caCertFile)
+ if err != nil {
+ return options, err
+ }
+ options.CACertificate = b
+ options.CACertificateFile = caCertFile
+ }
+ }
+ // Certificate and private key
if certOk && keyOk {
- // Use key pair from environment
- kp, err = tls.X509KeyPair([]byte(cert), []byte(key))
+ kp, err := tls.X509KeyPair([]byte(cert), []byte(key))
+ if err != nil {
+ return vespa.TLSOptions{}, err
+ }
+ options.KeyPair = []tls.Certificate{kp}
} else {
- keyFile, err = c.privateKeyPath(app, targetType)
+ keyFile, err := c.privateKeyPath(app, targetType)
if err != nil {
- return KeyPair{}, err
+ return vespa.TLSOptions{}, err
}
- certFile, err = c.certificatePath(app, targetType)
+ certFile, err := c.certificatePath(app, targetType)
if err != nil {
- return KeyPair{}, err
+ return vespa.TLSOptions{}, err
+ }
+ kp, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err == nil {
+ options.KeyPair = []tls.Certificate{kp}
+ options.PrivateKeyFile = keyFile
+ options.CertificateFile = certFile
+ } else if err != nil && !os.IsNotExist(err) {
+ return vespa.TLSOptions{}, err
}
- kp, err = tls.LoadX509KeyPair(certFile, keyFile)
- }
- if err != nil {
- return KeyPair{}, err
}
- if targetType == vespa.TargetHosted {
- cert, err := x509.ParseCertificate(kp.Certificate[0])
+ if options.KeyPair != nil {
+ cert, err := x509.ParseCertificate(options.KeyPair[0].Certificate[0])
if err != nil {
- return KeyPair{}, err
+ return vespa.TLSOptions{}, err
}
now := time.Now()
expiredAt := cert.NotAfter
if expiredAt.Before(now) {
delta := now.Sub(expiredAt).Truncate(time.Second)
- return KeyPair{}, fmt.Errorf("certificate %s expired at %s (%s ago)", certFile, cert.NotAfter, delta)
+ source := options.CertificateFile
+ if source == "" {
+ source = "environment"
+ }
+ return vespa.TLSOptions{}, fmt.Errorf("certificate in %s expired at %s (%s ago)", source, cert.NotAfter, delta)
}
- return KeyPair{KeyPair: kp, CertificateFile: certFile, PrivateKeyFile: keyFile}, nil
}
- return KeyPair{
- KeyPair: kp,
- CertificateFile: certFile,
- PrivateKeyFile: keyFile,
- }, nil
+ return options, nil
}
func (c *Config) apiKeyFileFromEnv() (string, bool) {
@@ -490,11 +512,10 @@ func (c *Config) readAPIKey(cli *CLI, system vespa.System, tenantName string) ([
return nil, nil // Vespa Cloud CI only talks to data plane and does not have an API key
}
if !cli.isCI() {
- client, err := cli.auth0Factory(cli.httpClient, auth0.Options{ConfigPath: c.authConfigPath(), SystemName: system.Name, SystemURL: system.URL})
- if err == nil && client.HasCredentials() {
- return nil, nil // use Auth0
+ if _, err := os.Stat(c.authConfigPath()); err == nil {
+ return nil, nil // We have auth config, so we should prefer Auth0 over API key
}
- cli.printWarning("Authenticating with API key. This is discouraged in non-CI environments", "Authenticate with 'vespa auth login'")
+ cli.printWarning("Authenticating with API key. This is discouraged in non-CI environments", "Authenticate with 'vespa auth login' instead")
}
return os.ReadFile(c.apiKeyPath(tenantName))
}
diff --git a/client/go/internal/cli/cmd/config_test.go b/client/go/internal/cli/cmd/config_test.go
index 458878b4356..66b65bf402b 100644
--- a/client/go/internal/cli/cmd/config_test.go
+++ b/client/go/internal/cli/cmd/config_test.go
@@ -2,15 +2,21 @@
package cmd
import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "math/big"
"os"
"path/filepath"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/vespa-engine/vespa/client/go/internal/cli/auth/auth0"
"github.com/vespa-engine/vespa/client/go/internal/mock"
- "github.com/vespa-engine/vespa/client/go/internal/util"
"github.com/vespa-engine/vespa/client/go/internal/vespa"
)
@@ -166,7 +172,7 @@ func TestReadAPIKey(t *testing.T) {
require.Nil(t, err)
assert.Equal(t, []byte("foo"), key)
- // Cloud CI does not read key from disk as it's not expected to have any
+ // Cloud CI never reads key from disk as it's not expected to have any
cli, _, _ = newTestCLI(t, "VESPA_CLI_CLOUD_CI=true")
key, err = cli.config.readAPIKey(cli, vespa.PublicSystem, "t1")
require.Nil(t, err)
@@ -186,12 +192,111 @@ func TestReadAPIKey(t *testing.T) {
require.Nil(t, err)
assert.Equal(t, []byte("baz"), key)
- // Auth0 is preferred when configured
+ // Prefer Auth0 if we have auth config
cli, _, _ = newTestCLI(t)
- cli.auth0Factory = func(httpClient util.HTTPClient, options auth0.Options) (auth0Client, error) {
- return &mockAuth0{hasCredentials: true}, nil
- }
+ require.Nil(t, os.WriteFile(filepath.Join(cli.config.homeDir, "auth.json"), []byte("foo"), 0600))
key, err = cli.config.readAPIKey(cli, vespa.PublicSystem, "t1")
require.Nil(t, err)
assert.Nil(t, key)
}
+
+func TestConfigReadTLSOptions(t *testing.T) {
+ app := vespa.ApplicationID{Tenant: "t1", Application: "a1", Instance: "i1"}
+ homeDir := t.TempDir()
+
+ // No environment variables, and no files on disk
+ assertTLSOptions(t, homeDir, app, vespa.TargetLocal, vespa.TLSOptions{})
+
+ // A single environment variable is set
+ assertTLSOptions(t, homeDir, app, vespa.TargetLocal, vespa.TLSOptions{TrustAll: true}, "VESPA_CLI_DATA_PLANE_TRUST_ALL=true")
+
+ // Key pair is provided in-line in environment variables
+ pemCert, pemKey, keyPair := createKeyPair(t)
+ assertTLSOptions(t, homeDir, app,
+ vespa.TargetLocal,
+ vespa.TLSOptions{
+ TrustAll: true,
+ CACertificate: []byte("cacert"),
+ KeyPair: []tls.Certificate{keyPair},
+ },
+ "VESPA_CLI_DATA_PLANE_TRUST_ALL=true",
+ "VESPA_CLI_DATA_PLANE_CA_CERT=cacert",
+ "VESPA_CLI_DATA_PLANE_CERT="+string(pemCert),
+ "VESPA_CLI_DATA_PLANE_KEY="+string(pemKey),
+ )
+
+ // Key pair is provided as file paths through environment variables
+ certFile := filepath.Join(homeDir, "cert")
+ keyFile := filepath.Join(homeDir, "key")
+ caCertFile := filepath.Join(homeDir, "cacert")
+ require.Nil(t, os.WriteFile(certFile, pemCert, 0600))
+ require.Nil(t, os.WriteFile(keyFile, pemKey, 0600))
+ require.Nil(t, os.WriteFile(caCertFile, []byte("cacert"), 0600))
+ assertTLSOptions(t, homeDir, app,
+ vespa.TargetLocal,
+ vespa.TLSOptions{
+ KeyPair: []tls.Certificate{keyPair},
+ CACertificate: []byte("cacert"),
+ CACertificateFile: caCertFile,
+ CertificateFile: certFile,
+ PrivateKeyFile: keyFile,
+ },
+ "VESPA_CLI_DATA_PLANE_CERT_FILE="+certFile,
+ "VESPA_CLI_DATA_PLANE_KEY_FILE="+keyFile,
+ "VESPA_CLI_DATA_PLANE_CA_CERT_FILE="+caCertFile,
+ )
+
+ // Key pair resides in default paths
+ defaultCertFile := filepath.Join(homeDir, app.String(), "data-plane-public-cert.pem")
+ defaultKeyFile := filepath.Join(homeDir, app.String(), "data-plane-private-key.pem")
+ require.Nil(t, os.WriteFile(defaultCertFile, pemCert, 0600))
+ require.Nil(t, os.WriteFile(defaultKeyFile, pemKey, 0600))
+ assertTLSOptions(t, homeDir, app,
+ vespa.TargetLocal,
+ vespa.TLSOptions{
+ KeyPair: []tls.Certificate{keyPair},
+ CertificateFile: defaultCertFile,
+ PrivateKeyFile: defaultKeyFile,
+ },
+ )
+}
+
+func assertTLSOptions(t *testing.T, homeDir string, app vespa.ApplicationID, target string, want vespa.TLSOptions, envVars ...string) {
+ t.Helper()
+ envVars = append(envVars, "VESPA_CLI_HOME="+homeDir)
+ cli, _, _ := newTestCLI(t, envVars...)
+ require.Nil(t, cli.Run("config", "set", "application", app.String()))
+ config, err := cli.config.readTLSOptions(app, vespa.TargetLocal)
+ require.Nil(t, err)
+ assert.Equal(t, want, config)
+}
+
+func createKeyPair(t *testing.T) ([]byte, []byte, tls.Certificate) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatal(err)
+ }
+ notBefore := time.Now()
+ notAfter := notBefore.Add(24 * time.Hour)
+ template := x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{CommonName: "example.com"},
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+ }
+ certificateDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateDER})
+ pemKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyDER})
+ kp, err := tls.X509KeyPair(pemCert, pemKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return pemCert, pemKey, kp
+}
diff --git a/client/go/internal/cli/cmd/curl.go b/client/go/internal/cli/cmd/curl.go
index 8fcd1fa6ef7..3d5aaff24dc 100644
--- a/client/go/internal/cli/cmd/curl.go
+++ b/client/go/internal/cli/cmd/curl.go
@@ -4,7 +4,6 @@ package cmd
import (
"fmt"
"log"
- "net/http"
"os"
"strings"
@@ -54,6 +53,7 @@ $ vespa curl -- -v --data-urlencode "yql=select * from music where album contain
return err
}
case vespa.DocumentService, vespa.QueryService:
+ c.CaCertificate = service.TLSOptions.CACertificateFile
c.PrivateKey = service.TLSOptions.PrivateKeyFile
c.Certificate = service.TLSOptions.CertificateFile
default:
@@ -79,15 +79,7 @@ func addAccessToken(cmd *curl.Command, target vespa.Target) error {
if target.Type() != vespa.TargetCloud {
return nil
}
- req := http.Request{}
- if err := target.SignRequest(&req, ""); err != nil {
- return err
- }
- headerValue := req.Header.Get("Authorization")
- if headerValue == "" {
- return fmt.Errorf("no authorization header added when signing request")
- }
- cmd.Header("Authorization", headerValue)
+ cmd.Header("Authorization", "secret")
return nil
}
diff --git a/client/go/internal/cli/cmd/feed.go b/client/go/internal/cli/cmd/feed.go
index 895a22d2be5..06568dd35c3 100644
--- a/client/go/internal/cli/cmd/feed.go
+++ b/client/go/internal/cli/cmd/feed.go
@@ -6,6 +6,7 @@ import (
"io"
"math"
"os"
+ "runtime/pprof"
"time"
"github.com/spf13/cobra"
@@ -14,16 +15,35 @@ import (
"github.com/vespa-engine/vespa/client/go/internal/vespa/document"
)
-func addFeedFlags(cmd *cobra.Command, verbose *bool, connections *int) {
- cmd.PersistentFlags().IntVarP(connections, "connections", "N", 8, "The number of connections to use")
- cmd.PersistentFlags().BoolVarP(verbose, "verbose", "v", false, "Verbose mode. Print errors as they happen")
+func addFeedFlags(cmd *cobra.Command, options *feedOptions) {
+ cmd.PersistentFlags().IntVar(&options.connections, "connections", 8, "The number of connections to use")
+ cmd.PersistentFlags().StringVar(&options.compression, "compression", "auto", `Compression mode to use. Default is "auto" which compresses large documents. Must be "auto", "gzip" or "none"`)
+ cmd.PersistentFlags().StringVar(&options.route, "route", "", "Target Vespa route for feed operations")
+ cmd.PersistentFlags().IntVar(&options.traceLevel, "trace", 0, "The trace level of network traffic. 0 to disable")
+ cmd.PersistentFlags().IntVar(&options.timeoutSecs, "timeout", 0, "Feed operation timeout in seconds. 0 to disable")
+ cmd.PersistentFlags().BoolVar(&options.verbose, "verbose", false, "Verbose mode. Print successful operations in addition to errors")
+ memprofile := "memprofile"
+ cpuprofile := "cpuprofile"
+ cmd.PersistentFlags().StringVar(&options.memprofile, memprofile, "", "Write a heap profile to given file")
+ cmd.PersistentFlags().StringVar(&options.cpuprofile, cpuprofile, "", "Write a CPU profile to given file")
+ // Hide these flags as they are intended for internal use
+ cmd.PersistentFlags().MarkHidden(memprofile)
+ cmd.PersistentFlags().MarkHidden(cpuprofile)
+}
+
+type feedOptions struct {
+ connections int
+ compression string
+ route string
+ verbose bool
+ traceLevel int
+ timeoutSecs int
+ memprofile string
+ cpuprofile string
}
func newFeedCmd(cli *CLI) *cobra.Command {
- var (
- verbose bool
- connections int
- )
+ var options feedOptions
cmd := &cobra.Command{
Use: "feed FILE",
Short: "Feed documents to a Vespa cluster",
@@ -56,10 +76,27 @@ $ cat documents.jsonl | vespa feed -
defer f.Close()
r = f
}
- return feed(r, cli, verbose, connections)
+ if options.cpuprofile != "" {
+ f, err := os.Create(options.cpuprofile)
+ if err != nil {
+ return err
+ }
+ pprof.StartCPUProfile(f)
+ defer pprof.StopCPUProfile()
+ }
+ err := feed(r, cli, options)
+ if options.memprofile != "" {
+ f, err := os.Create(options.memprofile)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ pprof.WriteHeapProfile(f)
+ }
+ return err
},
}
- addFeedFlags(cmd, &verbose, &connections)
+ addFeedFlags(cmd, &options)
return cmd
}
@@ -67,29 +104,46 @@ func createServiceClients(service *vespa.Service, n int) []util.HTTPClient {
clients := make([]util.HTTPClient, 0, n)
for i := 0; i < n; i++ {
client := service.Client().Clone()
- util.ForceHTTP2(client, service.TLSOptions.KeyPair) // Feeding should always use HTTP/2
+ // Feeding should always use HTTP/2
+ util.ForceHTTP2(client, service.TLSOptions.KeyPair, service.TLSOptions.CACertificate, service.TLSOptions.TrustAll)
clients = append(clients, client)
}
return clients
}
-func feed(r io.Reader, cli *CLI, verbose bool, connections int) error {
+func (opts feedOptions) compressionMode() (document.Compression, error) {
+ switch opts.compression {
+ case "auto":
+ return document.CompressionAuto, nil
+ case "none":
+ return document.CompressionNone, nil
+ case "gzip":
+ return document.CompressionGzip, nil
+ }
+ return 0, errHint(fmt.Errorf("invalid compression mode: %s", opts.compression), `Must be "auto", "gzip" or "none"`)
+}
+
+func feed(r io.Reader, cli *CLI, options feedOptions) error {
service, err := documentService(cli)
if err != nil {
return err
}
- clients := createServiceClients(service, connections)
+ clients := createServiceClients(service, options.connections)
+ compression, err := options.compressionMode()
+ if err != nil {
+ return err
+ }
client := document.NewClient(document.ClientOptions{
- BaseURL: service.BaseURL,
+ Compression: compression,
+ Timeout: time.Duration(options.timeoutSecs) * time.Second,
+ Route: options.route,
+ TraceLevel: options.traceLevel,
+ BaseURL: service.BaseURL,
}, clients)
- throttler := document.NewThrottler(connections)
+ throttler := document.NewThrottler(options.connections)
// TODO(mpolden): Make doom duration configurable
circuitBreaker := document.NewCircuitBreaker(10*time.Second, 0)
- errWriter := io.Discard
- if verbose {
- errWriter = cli.Stderr
- }
- dispatcher := document.NewDispatcher(client, throttler, circuitBreaker, errWriter)
+ dispatcher := document.NewDispatcher(client, throttler, circuitBreaker, cli.Stderr, options.verbose)
dec := document.NewDecoder(r)
start := cli.now()
diff --git a/client/go/internal/cli/cmd/root.go b/client/go/internal/cli/cmd/root.go
index 360af9d0dcf..88d43411983 100644
--- a/client/go/internal/cli/cmd/root.go
+++ b/client/go/internal/cli/cmd/root.go
@@ -2,7 +2,6 @@
package cmd
import (
- "crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -88,18 +87,9 @@ func (c *execSubprocess) Run(name string, args ...string) ([]byte, error) {
return exec.Command(name, args...).Output()
}
-type ztsClient interface {
- AccessToken(domain string, certficiate tls.Certificate) (string, error)
-}
-
-type auth0Client interface {
- AccessToken() (string, error)
- HasCredentials() bool
-}
-
-type auth0Factory func(httpClient util.HTTPClient, options auth0.Options) (auth0Client, error)
+type auth0Factory func(httpClient util.HTTPClient, options auth0.Options) (vespa.Authenticator, error)
-type ztsFactory func(httpClient util.HTTPClient, url string) (ztsClient, error)
+type ztsFactory func(httpClient util.HTTPClient, domain, url string) (vespa.Authenticator, error)
// New creates the Vespa CLI, writing output to stdout and stderr, and reading environment variables from environment.
func New(stdout, stderr io.Writer, environment []string) (*CLI, error) {
@@ -143,11 +133,11 @@ For detailed description of flags and configuration, see 'vespa help config'.
httpClient: util.CreateClient(time.Second * 10),
exec: &execSubprocess{},
now: time.Now,
- auth0Factory: func(httpClient util.HTTPClient, options auth0.Options) (auth0Client, error) {
+ auth0Factory: func(httpClient util.HTTPClient, options auth0.Options) (vespa.Authenticator, error) {
return auth0.NewClient(httpClient, options)
},
- ztsFactory: func(httpClient util.HTTPClient, url string) (ztsClient, error) {
- return zts.NewClient(httpClient, url)
+ ztsFactory: func(httpClient util.HTTPClient, domain, url string) (vespa.Authenticator, error) {
+ return zts.NewClient(httpClient, domain, url)
},
}
cli.isTerminal = func() bool { return isTerminal(cli.Stdout) && isTerminal(cli.Stderr) }
@@ -321,16 +311,34 @@ func (c *CLI) createTarget(opts targetOptions) (vespa.Target, error) {
if err != nil {
return nil, err
}
+ customURL := ""
if strings.HasPrefix(targetType, "http") {
- return vespa.CustomTarget(c.httpClient, targetType), nil
+ customURL = targetType
+ targetType = vespa.TargetCustom
}
switch targetType {
- case vespa.TargetLocal:
- return vespa.LocalTarget(c.httpClient), nil
+ case vespa.TargetLocal, vespa.TargetCustom:
+ return c.createCustomTarget(targetType, customURL)
case vespa.TargetCloud, vespa.TargetHosted:
return c.createCloudTarget(targetType, opts)
+ default:
+ return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud', 'hosted' or an URL")
+ }
+}
+
+func (c *CLI) createCustomTarget(targetType, customURL string) (vespa.Target, error) {
+ tlsOptions, err := c.config.readTLSOptions(vespa.DefaultApplication, targetType)
+ if err != nil {
+ return nil, err
+ }
+ switch targetType {
+ case vespa.TargetLocal:
+ return vespa.LocalTarget(c.httpClient, tlsOptions), nil
+ case vespa.TargetCustom:
+ return vespa.CustomTarget(c.httpClient, customURL, tlsOptions), nil
+ default:
+ return nil, fmt.Errorf("invalid custom target: %s", targetType)
}
- return nil, errHint(fmt.Errorf("invalid target: %s", targetType), "Valid targets are 'local', 'cloud', 'hosted' or an URL")
}
func (c *CLI) createCloudTarget(targetType string, opts targetOptions) (vespa.Target, error) {
@@ -347,48 +355,53 @@ func (c *CLI) createCloudTarget(targetType string, opts targetOptions) (vespa.Ta
return nil, err
}
var (
- apiKey []byte
- authConfigPath string
+ apiAuth vespa.Authenticator
+ deploymentAuth vespa.Authenticator
apiTLSOptions vespa.TLSOptions
deploymentTLSOptions vespa.TLSOptions
)
switch targetType {
case vespa.TargetCloud:
- apiKey, err = c.config.readAPIKey(c, system, deployment.Application.Tenant)
+ apiKey, err := c.config.readAPIKey(c, system, deployment.Application.Tenant)
if err != nil {
return nil, err
}
- authConfigPath = c.config.authConfigPath()
+ if apiKey == nil {
+ authConfigPath := c.config.authConfigPath()
+ auth0, err := c.auth0Factory(c.httpClient, auth0.Options{ConfigPath: authConfigPath, SystemName: system.Name, SystemURL: system.URL})
+ if err != nil {
+ return nil, err
+ }
+ apiAuth = auth0
+ } else {
+ apiAuth = vespa.NewRequestSigner(deployment.Application.SerializedForm(), apiKey)
+ }
deploymentTLSOptions = vespa.TLSOptions{}
if !opts.noCertificate {
- kp, err := c.config.x509KeyPair(deployment.Application, targetType)
+ kp, err := c.config.readTLSOptions(deployment.Application, targetType)
if err != nil {
- return nil, errHint(err, "Deployment to cloud requires a certificate. Try 'vespa auth cert'")
- }
- deploymentTLSOptions = vespa.TLSOptions{
- KeyPair: []tls.Certificate{kp.KeyPair},
- CertificateFile: kp.CertificateFile,
- PrivateKeyFile: kp.PrivateKeyFile,
+ return nil, errHint(err, "Deployment to cloud requires a certificate", "Try 'vespa auth cert' to create a self-signed certificate")
}
+ deploymentTLSOptions = kp
}
case vespa.TargetHosted:
- kp, err := c.config.x509KeyPair(deployment.Application, targetType)
+ kp, err := c.config.readTLSOptions(deployment.Application, targetType)
if err != nil {
return nil, errHint(err, "Deployment to hosted requires an Athenz certificate", "Try renewing certificate with 'athenz-user-cert'")
}
- apiTLSOptions = vespa.TLSOptions{
- KeyPair: []tls.Certificate{kp.KeyPair},
- CertificateFile: kp.CertificateFile,
- PrivateKeyFile: kp.PrivateKeyFile,
+ zts, err := c.ztsFactory(c.httpClient, system.AthenzDomain, zts.DefaultURL)
+ if err != nil {
+ return nil, err
}
- deploymentTLSOptions = apiTLSOptions
+ deploymentAuth = zts
+ apiTLSOptions = kp
+ deploymentTLSOptions = kp
default:
return nil, fmt.Errorf("invalid cloud target: %s", targetType)
}
apiOptions := vespa.APIOptions{
System: system,
TLSOptions: apiTLSOptions,
- APIKey: apiKey,
}
deploymentOptions := vespa.CloudDeploymentOptions{
Deployment: deployment,
@@ -403,15 +416,7 @@ func (c *CLI) createCloudTarget(targetType string, opts targetOptions) (vespa.Ta
Writer: c.Stdout,
Level: vespa.LogLevel(logLevel),
}
- auth0, err := c.auth0Factory(c.httpClient, auth0.Options{ConfigPath: authConfigPath, SystemName: apiOptions.System.Name, SystemURL: apiOptions.System.URL})
- if err != nil {
- return nil, err
- }
- zts, err := c.ztsFactory(c.httpClient, zts.DefaultURL)
- if err != nil {
- return nil, err
- }
- return vespa.CloudTarget(c.httpClient, zts, auth0, apiOptions, deploymentOptions, logOptions)
+ return vespa.CloudTarget(c.httpClient, apiAuth, deploymentAuth, apiOptions, deploymentOptions, logOptions)
}
// system returns the appropiate system for the target configured in this CLI.
@@ -460,7 +465,6 @@ func (c *CLI) createDeploymentOptions(pkg vespa.ApplicationPackage, target vespa
ApplicationPackage: pkg,
Target: target,
Timeout: timeout,
- HTTPClient: c.httpClient,
}, nil
}
diff --git a/client/go/internal/cli/cmd/test.go b/client/go/internal/cli/cmd/test.go
index 05633b1135e..8c4501e2870 100644
--- a/client/go/internal/cli/cmd/test.go
+++ b/client/go/internal/cli/cmd/test.go
@@ -263,7 +263,7 @@ func verify(step step, defaultCluster string, defaultParameters map[string]strin
var response *http.Response
if externalEndpoint {
- util.SetCertificates(context.cli.httpClient, []tls.Certificate{})
+ util.ConfigureTLS(context.cli.httpClient, []tls.Certificate{}, nil, false)
response, err = context.cli.httpClient.Do(request, 60*time.Second)
} else {
response, err = service.Do(request, 600*time.Second) // Vespa should provide a response within the given request timeout
diff --git a/client/go/internal/cli/cmd/testutil_test.go b/client/go/internal/cli/cmd/testutil_test.go
index 61f8dab2264..492e40d8855 100644
--- a/client/go/internal/cli/cmd/testutil_test.go
+++ b/client/go/internal/cli/cmd/testutil_test.go
@@ -3,13 +3,14 @@ package cmd
import (
"bytes"
- "crypto/tls"
+ "net/http"
"path/filepath"
"testing"
"github.com/vespa-engine/vespa/client/go/internal/cli/auth/auth0"
"github.com/vespa-engine/vespa/client/go/internal/mock"
"github.com/vespa-engine/vespa/client/go/internal/util"
+ "github.com/vespa-engine/vespa/client/go/internal/vespa"
)
func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Buffer) {
@@ -29,21 +30,15 @@ func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Bu
httpClient := &mock.HTTPClient{}
cli.httpClient = httpClient
cli.exec = &mock.Exec{}
- cli.auth0Factory = func(httpClient util.HTTPClient, options auth0.Options) (auth0Client, error) {
- return &mockAuth0{}, nil
+ cli.auth0Factory = func(httpClient util.HTTPClient, options auth0.Options) (vespa.Authenticator, error) {
+ return &mockAuthenticator{}, nil
}
- cli.ztsFactory = func(httpClient util.HTTPClient, url string) (ztsClient, error) {
- return &mockZTS{}, nil
+ cli.ztsFactory = func(httpClient util.HTTPClient, domain, url string) (vespa.Authenticator, error) {
+ return &mockAuthenticator{}, nil
}
return cli, &stdout, &stderr
}
-type mockZTS struct{}
+type mockAuthenticator struct{}
-func (z *mockZTS) AccessToken(domain string, cert tls.Certificate) (string, error) { return "", nil }
-
-type mockAuth0 struct{ hasCredentials bool }
-
-func (a *mockAuth0) AccessToken() (string, error) { return "", nil }
-
-func (a *mockAuth0) HasCredentials() bool { return a.hasCredentials }
+func (a *mockAuthenticator) Authenticate(request *http.Request) error { return nil }