summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-04-07 14:19:56 +0200
committerMartin Polden <mpolden@mpolden.no>2022-04-08 08:47:43 +0200
commit0673055c460f0530335d9d43e550cce562bae108 (patch)
treec502fac64cd3013cc87fd6f963affc16a55c041c /client
parent8c0e721a14c6eece88a72b20edcc7104766aa65c (diff)
Add support for local configuration
Diffstat (limited to 'client')
-rw-r--r--client/go/cmd/cert.go4
-rw-r--r--client/go/cmd/config.go234
-rw-r--r--client/go/cmd/config_test.go99
-rw-r--r--client/go/cmd/deploy.go27
-rw-r--r--client/go/cmd/deploy_test.go2
-rw-r--r--client/go/cmd/prod.go11
-rw-r--r--client/go/cmd/root.go102
-rw-r--r--client/go/cmd/status.go9
-rw-r--r--client/go/cmd/test.go3
-rw-r--r--client/go/cmd/testutil_test.go2
-rw-r--r--client/go/vespa/application.go5
11 files changed, 320 insertions, 178 deletions
diff --git a/client/go/cmd/cert.go b/client/go/cmd/cert.go
index 11b20603ce8..581c76b8721 100644
--- a/client/go/cmd/cert.go
+++ b/client/go/cmd/cert.go
@@ -100,7 +100,7 @@ func doCert(cli *CLI, overwriteCertificate, noApplicationPackage bool, args []st
}
var pkg vespa.ApplicationPackage
if !noApplicationPackage {
- pkg, err = vespa.FindApplicationPackage(applicationSource(args), false)
+ pkg, err = cli.applicationPackageFrom(args, false)
if err != nil {
return err
}
@@ -168,7 +168,7 @@ func doCertAdd(cli *CLI, overwriteCertificate bool, args []string) error {
if err != nil {
return err
}
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), false)
+ pkg, err := cli.applicationPackageFrom(args, false)
if err != nil {
return err
}
diff --git a/client/go/cmd/config.go b/client/go/cmd/config.go
index 417dfd77198..e64353fd91d 100644
--- a/client/go/cmd/config.go
+++ b/client/go/cmd/config.go
@@ -13,9 +13,11 @@ import (
"sort"
"strconv"
"strings"
+ "time"
"github.com/fatih/color"
"github.com/spf13/cobra"
+ "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/vespa-engine/vespa/client/go/auth/auth0"
"github.com/vespa-engine/vespa/client/go/util"
@@ -38,7 +40,21 @@ future invocations the flag can then be omitted as it is read from the config
file instead.
Configuration is written to $HOME/.vespa by default. This path can be
-overridden by setting the VESPA_CLI_HOME environment variable.`,
+overridden by setting the VESPA_CLI_HOME environment variable.
+
+When configuring a local option, the configuration is written to
+[working-directory]/.vespa, where working directory is assumed to be Vespa
+application directory. This allows you have separate configuration options per
+application.
+
+Vespa CLI chooses the value for a given option in the following order, from
+most to least preferred:
+
+1. flag value specified on the command line
+2. local config value
+3. global config value
+4. default value
+`,
DisableAutoGenTag: true,
SilenceUsage: false,
Args: cobra.MinimumNArgs(1),
@@ -49,7 +65,8 @@ overridden by setting the VESPA_CLI_HOME environment variable.`,
}
func newConfigSetCmd(cli *CLI) *cobra.Command {
- return &cobra.Command{
+ var localArg bool
+ cmd := &cobra.Command{
Use: "set option-name value",
Short: "Set a configuration option.",
Example: `# Set the target to Vespa Cloud
@@ -62,57 +79,88 @@ $ vespa config set application my-tenant.my-application
$ vespa config set application my-tenant.my-application.my-instance
# Set the instance explicitly. This will take precedence over an instance specified as part of the application option.
-$ vespa config set instance other-instance`,
+$ vespa config set instance other-instance
+
+# Set an option in local configuration, for the current application only
+$ vespa config set --local wait 600
+`,
DisableAutoGenTag: true,
SilenceUsage: true,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
- if err := cli.config.set(args[0], args[1]); err != nil {
+ config := cli.config
+ if localArg {
+ // Need an application package in working directory to allow local configuration
+ if _, err := cli.applicationPackageFrom(nil, false); err != nil {
+ return fmt.Errorf("failed to write local configuration: %w", err)
+ }
+ if err := cli.config.loadLocalConfigFrom(".", true, true); err != nil {
+ return fmt.Errorf("failed to create local configuration: %w", err)
+ }
+ config = cli.config.local
+ }
+ if err := config.set(args[0], args[1]); err != nil {
return err
}
- return cli.config.write()
+ return config.write()
},
}
+ cmd.Flags().BoolVarP(&localArg, "local", "l", false, "Write option to local configuration, i.e. for the current application")
+ return cmd
}
func newConfigGetCmd(cli *CLI) *cobra.Command {
- return &cobra.Command{
+ var localArg bool
+ cmd := &cobra.Command{
Use: "get [option-name]",
Short: "Show given configuration option, or all configuration options",
+ Long: `Show given configuration option, or all configuration options.
+
+By default this command prints the effective configuration for the current
+application, i.e. it takes into account any local configuration located in
+[working-directory]/.vespa.
+`,
Example: `$ vespa config get
-$ vespa config get target`,
+$ vespa config get target
+$ vespa config get --local
+`,
Args: cobra.MaximumNArgs(1),
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 { // Print all values
- var flags []string
- for flag := range cli.config.bindings.flag {
- flags = append(flags, flag)
+ config := cli.config
+ if localArg {
+ if err := cli.config.loadLocalConfigFrom(".", false, true); err != nil {
+ return fmt.Errorf("failed to load local configuration: %w", err)
}
- sort.Strings(flags)
- for _, flag := range flags {
- cli.config.printOption(flag)
+ config = cli.config.local
+ if config == nil {
+ cli.printWarning("no local configuration present")
+ return nil
+ }
+ }
+ if len(args) == 0 { // Print all values
+ for _, option := range config.list() {
+ config.printOption(option)
}
} else {
- cli.config.printOption(args[0])
+ config.printOption(args[0])
}
return nil
},
}
+ cmd.Flags().BoolVarP(&localArg, "local", "l", false, "Show only local configuration, if any")
+ return cmd
}
type Config struct {
homeDir string
cacheDir string
environment map[string]string
- bindings ConfigBindings
- createDirs bool
-}
+ local *Config
-type ConfigBindings struct {
- flag map[string]*cobra.Command
- environment map[string]string
+ flags *pflag.FlagSet
+ viper *viper.Viper
}
type KeyPair struct {
@@ -121,43 +169,70 @@ type KeyPair struct {
PrivateKeyFile string
}
-func NewConfigBindings() ConfigBindings {
- return ConfigBindings{
- flag: make(map[string]*cobra.Command),
- environment: make(map[string]string),
- }
-}
-
-func (b *ConfigBindings) bindFlag(name string, command *cobra.Command) {
- b.flag[name] = command
-}
-
-func (b *ConfigBindings) bindEnvironment(flagName string, variable string) {
- b.environment[flagName] = variable
-}
-
-func loadConfig(environment map[string]string, bindings ConfigBindings) (*Config, error) {
+func loadConfig(environment map[string]string, globalFlags *pflag.FlagSet) (*Config, error) {
home, err := vespaCliHome(environment)
if err != nil {
return nil, fmt.Errorf("could not detect config directory: %w", err)
}
+ config, err := loadConfigFrom(home, environment, globalFlags)
+ if err != nil {
+ return nil, err
+ }
+ // Load local config from working directory by default, if any
+ if err := config.loadLocalConfigFrom(".", false, false); err != nil {
+ return nil, err
+ }
+ return config, nil
+}
+
+func loadConfigFrom(dir string, environment map[string]string, globalFlags *pflag.FlagSet) (*Config, error) {
cacheDir, err := vespaCliCacheDir(environment)
if err != nil {
return nil, fmt.Errorf("could not detect cache directory: %w", err)
}
c := &Config{
- homeDir: home,
+ homeDir: dir,
cacheDir: cacheDir,
environment: environment,
- bindings: bindings,
- createDirs: true,
- }
- if err := c.load(); err != nil {
- return nil, fmt.Errorf("could not load config: %w", err)
+ flags: globalFlags,
+ }
+ v := viper.New()
+ v.SetConfigName(configName)
+ v.SetConfigType(configType)
+ v.AddConfigPath(c.homeDir)
+ v.BindPFlags(globalFlags)
+ c.viper = v
+ if err := v.ReadInConfig(); err != nil {
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+ return nil, err
+ }
}
return c, nil
}
+func (c *Config) loadLocalConfigFrom(parent string, create, noFlags bool) error {
+ home := filepath.Join(parent, ".vespa")
+ _, err := os.Stat(home)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ if !create {
+ return nil
+ }
+ }
+ flags := c.flags
+ if noFlags {
+ flags = &pflag.FlagSet{}
+ }
+ config, err := loadConfigFrom(home, c.environment, flags)
+ if err != nil {
+ return err
+ }
+ c.local = config
+ return nil
+}
+
func (c *Config) write() error {
if err := os.MkdirAll(c.homeDir, 0700); err != nil {
return err
@@ -168,7 +243,8 @@ func (c *Config) write() error {
return err
}
}
- return viper.WriteConfig()
+ err := c.viper.WriteConfig()
+ return err
}
func (c *Config) targetType() (string, error) {
@@ -179,6 +255,23 @@ func (c *Config) targetType() (string, error) {
return targetType, nil
}
+func (c *Config) timeout() (time.Duration, error) {
+ wait, ok := c.get(waitFlag)
+ if !ok {
+ return 0, nil
+ }
+ secs, err := strconv.Atoi(wait)
+ if err != nil {
+ return 0, err
+ }
+ return time.Duration(secs) * time.Second, nil
+}
+
+func (c *Config) isQuiet() bool {
+ quiet, _ := c.get(quietFlag)
+ return quiet == "true"
+}
+
func (c *Config) application() (vespa.ApplicationID, error) {
app, ok := c.get(applicationFlag)
if !ok {
@@ -195,10 +288,11 @@ func (c *Config) application() (vespa.ApplicationID, error) {
return application, nil
}
-func (c *Config) deploymentIn(zoneName string, system vespa.System) (vespa.Deployment, error) {
+func (c *Config) deploymentIn(system vespa.System) (vespa.Deployment, error) {
zone := system.DefaultZone
- var err error
- if zoneName != "" {
+ zoneName, ok := c.get(zoneFlag)
+ if ok {
+ var err error
zone, err = vespa.ZoneFromString(zoneName)
if err != nil {
return vespa.Deployment{}, err
@@ -321,35 +415,25 @@ func (c *Config) writeSessionID(app vespa.ApplicationID, sessionID int64) error
func (c *Config) applicationFilePath(app vespa.ApplicationID, name string) (string, error) {
appDir := filepath.Join(c.homeDir, app.String())
- if c.createDirs {
- if err := os.MkdirAll(appDir, 0700); err != nil {
- return "", err
- }
+ if err := os.MkdirAll(appDir, 0700); err != nil {
+ return "", err
}
return filepath.Join(appDir, name), nil
}
-func (c *Config) load() error {
- viper.SetConfigName(configName)
- viper.SetConfigType(configType)
- viper.AddConfigPath(c.homeDir)
- for option, command := range c.bindings.flag {
- viper.BindPFlag(option, command.PersistentFlags().Lookup(option))
- }
- err := viper.ReadInConfig()
- if _, ok := err.(viper.ConfigFileNotFoundError); ok {
- return nil
- }
- return err
+func (c *Config) list() []string {
+ options := c.viper.AllKeys()
+ sort.Strings(options)
+ return options
}
func (c *Config) get(option string) (string, bool) {
- if envVar, ok := c.bindings.environment[option]; ok {
- if value, ok := c.environment[envVar]; ok {
- return value, true
+ if c.local != nil {
+ if value, ok := c.local.get(option); ok {
+ return value, ok
}
}
- value := viper.GetString(option)
+ value := c.viper.GetString(option)
if value == "" {
return "", false
}
@@ -361,11 +445,11 @@ func (c *Config) set(option, value string) error {
case targetFlag:
switch value {
case vespa.TargetLocal, vespa.TargetCloud, vespa.TargetHosted:
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
}
if strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") {
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
}
case applicationFlag:
@@ -373,31 +457,31 @@ func (c *Config) set(option, value string) error {
if err != nil {
return err
}
- viper.Set(option, app.String())
+ c.viper.Set(option, app.String())
return nil
case instanceFlag:
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
case waitFlag:
if n, err := strconv.Atoi(value); err != nil || n < 0 {
return fmt.Errorf("%s option must be an integer >= 0, got %q", option, value)
}
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
case colorFlag:
switch value {
case "auto", "never", "always":
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
}
case quietFlag:
switch value {
case "true", "false":
- viper.Set(option, value)
+ c.viper.Set(option, value)
return nil
}
}
- return fmt.Errorf("invalid option or value: %q: %q", option, value)
+ return fmt.Errorf("invalid option or value: %s = %s", option, value)
}
func (c *Config) printOption(option string) {
diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go
index 4fb9ac606cc..7059bfa78cc 100644
--- a/client/go/cmd/config_test.go
+++ b/client/go/cmd/config_test.go
@@ -8,52 +8,87 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/vespa-engine/vespa/client/go/mock"
"github.com/vespa-engine/vespa/client/go/vespa"
)
func TestConfig(t *testing.T) {
- assertConfigCommandErr(t, "Error: invalid option or value: \"foo\": \"bar\"\n", "config", "set", "foo", "bar")
- assertConfigCommand(t, "foo = <unset>\n", "config", "get", "foo")
- assertConfigCommand(t, "target = local\n", "config", "get", "target")
- assertConfigCommand(t, "", "config", "set", "target", "hosted")
- assertConfigCommand(t, "target = hosted\n", "config", "get", "target")
- assertConfigCommand(t, "", "config", "set", "target", "cloud")
- assertConfigCommand(t, "target = cloud\n", "config", "get", "target")
- assertConfigCommand(t, "", "config", "set", "target", "http://127.0.0.1:8080")
- assertConfigCommand(t, "", "config", "set", "target", "https://127.0.0.1")
- assertConfigCommand(t, "target = https://127.0.0.1\n", "config", "get", "target")
-
- assertConfigCommandErr(t, "Error: invalid application: \"foo\"\n", "config", "set", "application", "foo")
- assertConfigCommand(t, "application = <unset>\n", "config", "get", "application")
- assertConfigCommand(t, "", "config", "set", "application", "t1.a1.i1")
- assertConfigCommand(t, "application = t1.a1.i1\n", "config", "get", "application")
-
- assertConfigCommand(t, "", "config", "set", "wait", "60")
- assertConfigCommandErr(t, "Error: wait option must be an integer >= 0, got \"foo\"\n", "config", "set", "wait", "foo")
- assertConfigCommand(t, "wait = 60\n", "config", "get", "wait")
-
- assertConfigCommand(t, "", "config", "set", "quiet", "true")
- assertConfigCommand(t, "", "config", "set", "quiet", "false")
-
- assertConfigCommand(t, "", "config", "set", "instance", "i2")
- assertConfigCommand(t, "instance = i2\n", "config", "get", "instance")
-
- assertConfigCommand(t, "", "config", "set", "application", "t1.a1")
- assertConfigCommand(t, "application = t1.a1.default\n", "config", "get", "application")
+ configHome := t.TempDir()
+ assertConfigCommandErr(t, configHome, "Error: invalid option or value: foo = bar\n", "config", "set", "foo", "bar")
+ assertConfigCommand(t, configHome, "foo = <unset>\n", "config", "get", "foo")
+ assertConfigCommand(t, configHome, "target = local\n", "config", "get", "target")
+ assertConfigCommand(t, configHome, "", "config", "set", "target", "hosted")
+ assertConfigCommand(t, configHome, "target = hosted\n", "config", "get", "target")
+ assertConfigCommand(t, configHome, "", "config", "set", "target", "cloud")
+ assertConfigCommand(t, configHome, "target = cloud\n", "config", "get", "target")
+ assertConfigCommand(t, configHome, "", "config", "set", "target", "http://127.0.0.1:8080")
+ assertConfigCommand(t, configHome, "", "config", "set", "target", "https://127.0.0.1")
+ assertConfigCommand(t, configHome, "target = https://127.0.0.1\n", "config", "get", "target")
+
+ assertConfigCommandErr(t, configHome, "Error: invalid application: \"foo\"\n", "config", "set", "application", "foo")
+ assertConfigCommand(t, configHome, "application = <unset>\n", "config", "get", "application")
+ assertConfigCommand(t, configHome, "", "config", "set", "application", "t1.a1.i1")
+ assertConfigCommand(t, configHome, "application = t1.a1.i1\n", "config", "get", "application")
+
+ assertConfigCommand(t, configHome, "", "config", "set", "wait", "60")
+ assertConfigCommandErr(t, configHome, "Error: wait option must be an integer >= 0, got \"foo\"\n", "config", "set", "wait", "foo")
+ assertConfigCommand(t, configHome, "wait = 60\n", "config", "get", "wait")
+ assertConfigCommand(t, configHome, "wait = 30\n", "config", "get", "--wait", "30", "wait") // flag overrides global config
+
+ assertConfigCommand(t, configHome, "", "config", "set", "quiet", "true")
+ assertConfigCommand(t, configHome, "", "config", "set", "quiet", "false")
+
+ assertConfigCommand(t, configHome, "", "config", "set", "instance", "i2")
+ assertConfigCommand(t, configHome, "instance = i2\n", "config", "get", "instance")
+
+ assertConfigCommand(t, configHome, "", "config", "set", "application", "t1.a1")
+ assertConfigCommand(t, configHome, "application = t1.a1.default\n", "config", "get", "application")
+}
+
+func TestLocalConfig(t *testing.T) {
+ configHome := t.TempDir()
+ assertConfigCommand(t, configHome, "", "config", "set", "instance", "main")
+
+ // Change directory to an application package and write local configuration
+ _, rootDir := mock.ApplicationPackageDir(t, false, false)
+ wd, err := os.Getwd()
+ require.Nil(t, err)
+ t.Cleanup(func() { os.Chdir(wd) })
+ require.Nil(t, os.Chdir(rootDir))
+ assertConfigCommand(t, configHome, "", "config", "set", "--local", "instance", "foo")
+ assertConfigCommand(t, configHome, "instance = foo\n", "config", "get", "instance")
+ assertConfigCommand(t, configHome, "instance = bar\n", "config", "get", "--instance", "bar", "instance") // flag overrides local config
+
+ // get --local prints only options set in local config
+ assertConfigCommand(t, configHome, "", "config", "set", "--local", "target", "hosted")
+ assertConfigCommand(t, configHome, "instance = foo\ntarget = hosted\n", "config", "get", "--local")
+
+ // only locally set options are written
+ localConfig, err := os.ReadFile(filepath.Join(rootDir, ".vespa", "config.yaml"))
+ require.Nil(t, err)
+ assert.Equal(t, "instance: foo\ntarget: hosted\n", string(localConfig))
+
+ // Changing back to original directory reads from global config
+ require.Nil(t, os.Chdir(wd))
+ assertConfigCommand(t, configHome, "instance = main\n", "config", "get", "instance")
}
-func assertConfigCommand(t *testing.T, expected string, args ...string) {
- assertEnvConfigCommand(t, expected, nil, args...)
+func assertConfigCommand(t *testing.T, configHome, expected string, args ...string) {
+ t.Helper()
+ assertEnvConfigCommand(t, configHome, expected, nil, args...)
}
-func assertEnvConfigCommand(t *testing.T, expected string, env []string, args ...string) {
+func assertEnvConfigCommand(t *testing.T, configHome, expected string, env []string, args ...string) {
+ t.Helper()
+ env = append(env, "VESPA_CLI_HOME="+configHome)
cli, stdout, _ := newTestCLI(t, env...)
err := cli.Run(args...)
assert.Nil(t, err)
assert.Equal(t, expected, stdout.String())
}
-func assertConfigCommandErr(t *testing.T, expected string, args ...string) {
+func assertConfigCommandErr(t *testing.T, configHome, expected string, args ...string) {
+ t.Helper()
cli, _, stderr := newTestCLI(t)
err := cli.Run(args...)
assert.NotNil(t, err)
diff --git a/client/go/cmd/deploy.go b/client/go/cmd/deploy.go
index 16da69fded6..930e25dddc7 100644
--- a/client/go/cmd/deploy.go
+++ b/client/go/cmd/deploy.go
@@ -48,7 +48,7 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ pkg, err := cli.applicationPackageFrom(args, true)
if err != nil {
return err
}
@@ -56,7 +56,10 @@ $ vespa deploy -t cloud -z perf.aws-us-east-1c`,
if err != nil {
return err
}
- opts := cli.createDeploymentOptions(pkg, target)
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
if versionArg != "" {
version, err := version.Parse(versionArg)
if err != nil {
@@ -105,7 +108,7 @@ func newPrepareCmd(cli *CLI) *cobra.Command {
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ pkg, err := cli.applicationPackageFrom(args, true)
if err != nil {
return fmt.Errorf("could not find application package: %w", err)
}
@@ -113,7 +116,10 @@ func newPrepareCmd(cli *CLI) *cobra.Command {
if err != nil {
return err
}
- opts := cli.createDeploymentOptions(pkg, target)
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
var result vespa.PrepareResult
err = cli.spinner(cli.Stderr, "Uploading application package ...", func() error {
result, err = vespa.Prepare(opts)
@@ -139,7 +145,7 @@ func newActivateCmd(cli *CLI) *cobra.Command {
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- pkg, err := vespa.FindApplicationPackage(applicationSource(args), true)
+ pkg, err := cli.applicationPackageFrom(args, true)
if err != nil {
return fmt.Errorf("could not find application package: %w", err)
}
@@ -151,7 +157,10 @@ func newActivateCmd(cli *CLI) *cobra.Command {
if err != nil {
return err
}
- opts := cli.createDeploymentOptions(pkg, target)
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
err = vespa.Activate(sessionID, opts)
if err != nil {
return err
@@ -163,7 +172,11 @@ func newActivateCmd(cli *CLI) *cobra.Command {
}
func waitForQueryService(cli *CLI, target vespa.Target, sessionOrRunID int64) error {
- if cli.flags.waitSecs > 0 {
+ timeout, err := cli.config.timeout()
+ if err != nil {
+ return err
+ }
+ if timeout > 0 {
log.Println()
_, err := cli.service(target, vespa.QueryService, sessionOrRunID, "")
return err
diff --git a/client/go/cmd/deploy_test.go b/client/go/cmd/deploy_test.go
index fedcfd604f4..fe3e0ca0467 100644
--- a/client/go/cmd/deploy_test.go
+++ b/client/go/cmd/deploy_test.go
@@ -66,7 +66,7 @@ func TestDeployApplicationDirectoryWithPomAndEmptyTarget(t *testing.T) {
cli, _, stderr := newTestCLI(t)
assert.NotNil(t, cli.Run("deploy", "testdata/applications/withEmptyTarget"))
assert.Equal(t,
- "Error: pom.xml exists but no target/application.zip. Run mvn package first\n",
+ "Error: found pom.xml, but target/application.zip does not exist: run 'mvn package' first\n",
stderr.String())
}
diff --git a/client/go/cmd/prod.go b/client/go/cmd/prod.go
index 5bc4851dfa7..a7e11d3e117 100644
--- a/client/go/cmd/prod.go
+++ b/client/go/cmd/prod.go
@@ -53,8 +53,7 @@ https://cloud.vespa.ai/en/reference/deployment`,
DisableAutoGenTag: true,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
- appSource := applicationSource(args)
- pkg, err := vespa.FindApplicationPackage(appSource, false)
+ pkg, err := cli.applicationPackageFrom(args, false)
if err != nil {
return err
}
@@ -135,8 +134,7 @@ $ vespa prod submit`,
// TODO: Add support for hosted
return fmt.Errorf("prod submit does not support %s target", target.Type())
}
- appSource := applicationSource(args)
- pkg, err := vespa.FindApplicationPackage(appSource, true)
+ pkg, err := cli.applicationPackageFrom(args, true)
if err != nil {
return err
}
@@ -154,7 +152,10 @@ $ vespa prod submit`,
if !cli.isCI() {
cli.printWarning("We recommend doing this only from a CD job", "See https://cloud.vespa.ai/en/getting-to-production")
}
- opts := cli.createDeploymentOptions(pkg, target)
+ opts, err := cli.createDeploymentOptions(pkg, target)
+ if err != nil {
+ return err
+ }
if err := vespa.Submit(opts); err != nil {
return fmt.Errorf("could not submit application for deployment: %w", err)
} else {
diff --git a/client/go/cmd/root.go b/client/go/cmd/root.go
index 92ff98d8756..996a86ec90a 100644
--- a/client/go/cmd/root.go
+++ b/client/go/cmd/root.go
@@ -11,7 +11,6 @@ import (
"os"
"os/exec"
"path/filepath"
- "strconv"
"strings"
"time"
@@ -19,6 +18,7 @@ import (
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
+ "github.com/spf13/pflag"
"github.com/vespa-engine/vespa/client/go/build"
"github.com/vespa-engine/vespa/client/go/util"
"github.com/vespa-engine/vespa/client/go/version"
@@ -44,7 +44,6 @@ type CLI struct {
Stderr io.Writer
cmd *cobra.Command
- flags *Flags
config *Config
version version.Version
@@ -54,17 +53,6 @@ type CLI struct {
spinner func(w io.Writer, message string, fn func() error) error
}
-// Flags holds the global Flags of Vespa CLI.
-type Flags struct {
- target string
- application string
- instance string
- zone string
- waitSecs int
- color string
- quiet bool
-}
-
// ErrCLI is an error returned to the user. It wraps an exit status, a regular error and optional hints for resolving
// the error.
type ErrCLI struct {
@@ -136,7 +124,6 @@ Vespa documentation: https://docs.vespa.ai`,
exec: &execSubprocess{},
}
cli.isTerminal = func() bool { return isTerminal(cli.Stdout) && isTerminal(cli.Stderr) }
- cli.configureFlags()
if err := cli.loadConfig(); err != nil {
return nil, err
}
@@ -147,15 +134,7 @@ Vespa documentation: https://docs.vespa.ai`,
}
func (c *CLI) loadConfig() error {
- bindings := NewConfigBindings()
- bindings.bindFlag(targetFlag, c.cmd)
- bindings.bindFlag(applicationFlag, c.cmd)
- bindings.bindFlag(instanceFlag, c.cmd)
- bindings.bindFlag(zoneFlag, c.cmd)
- bindings.bindFlag(waitFlag, c.cmd)
- bindings.bindFlag(colorFlag, c.cmd)
- bindings.bindFlag(quietFlag, c.cmd)
- config, err := loadConfig(c.Environment, bindings)
+ config, err := loadConfig(c.Environment, c.configureFlags())
if err != nil {
return err
}
@@ -170,7 +149,7 @@ func (c *CLI) configureOutput(cmd *cobra.Command, args []string) error {
if f, ok := c.Stderr.(*os.File); ok {
c.Stderr = colorable.NewColorable(f)
}
- if quiet, _ := c.config.get(quietFlag); quiet == "true" {
+ if c.config.isQuiet() {
c.Stdout = io.Discard
}
log.SetFlags(0) // No timestamps
@@ -185,29 +164,37 @@ func (c *CLI) configureOutput(cmd *cobra.Command, args []string) error {
colorize = true
case "never":
default:
- return errHint(fmt.Errorf("invalid value for %s option", colorFlag), "Must be \"auto\", \"never\" or \"always\"")
+ return errHint(fmt.Errorf("invalid value for option %s: %q", colorFlag, colorValue), "Must be \"auto\", \"never\" or \"always\"")
}
color.NoColor = !colorize
return nil
}
-func (c *CLI) configureFlags() {
- flags := Flags{}
- c.cmd.PersistentFlags().StringVarP(&flags.target, targetFlag, "t", "local", "The name or URL of the recipient of this command")
- c.cmd.PersistentFlags().StringVarP(&flags.application, applicationFlag, "a", "", "The application to manage")
- c.cmd.PersistentFlags().StringVarP(&flags.instance, instanceFlag, "i", "", "The instance of the application to manage")
- c.cmd.PersistentFlags().StringVarP(&flags.zone, zoneFlag, "z", "", "The zone to use. This defaults to a dev zone")
- c.cmd.PersistentFlags().IntVarP(&flags.waitSecs, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready")
- c.cmd.PersistentFlags().StringVarP(&flags.color, colorFlag, "c", "auto", "Whether to use colors in output.")
- c.cmd.PersistentFlags().BoolVarP(&flags.quiet, quietFlag, "q", false, "Quiet mode. Only errors will be printed")
- c.flags = &flags
+func (c *CLI) configureFlags() *pflag.FlagSet {
+ var (
+ target string
+ application string
+ instance string
+ zone string
+ waitSecs int
+ color string
+ quiet bool
+ )
+ c.cmd.PersistentFlags().StringVarP(&target, targetFlag, "t", "local", "The name or URL of the recipient of this command")
+ c.cmd.PersistentFlags().StringVarP(&application, applicationFlag, "a", "", "The application to manage")
+ c.cmd.PersistentFlags().StringVarP(&instance, instanceFlag, "i", "", "The instance of the application to manage")
+ c.cmd.PersistentFlags().StringVarP(&zone, zoneFlag, "z", "", "The zone to use. This defaults to a dev zone")
+ c.cmd.PersistentFlags().IntVarP(&waitSecs, waitFlag, "w", 0, "Number of seconds to wait for a service to become ready")
+ c.cmd.PersistentFlags().StringVarP(&color, colorFlag, "c", "auto", "Whether to use colors in output.")
+ c.cmd.PersistentFlags().BoolVarP(&quiet, quietFlag, "q", false, "Quiet mode. Only errors will be printed")
+ return c.cmd.PersistentFlags()
}
func (c *CLI) configureSpinner() {
// Explicitly disable spinner for Screwdriver. It emulates a tty but
// \r result in a newline, and output gets truncated.
_, screwdriver := c.Environment["SCREWDRIVER"]
- if c.flags.quiet || !c.isTerminal() || screwdriver {
+ if c.config.isQuiet() || !c.isTerminal() || screwdriver {
c.spinner = func(w io.Writer, message string, fn func() error) error {
return fn()
}
@@ -313,7 +300,7 @@ func (c *CLI) createCloudTarget(targetType string, opts targetOptions) (vespa.Ta
if err != nil {
return nil, err
}
- deployment, err := c.config.deploymentIn(c.flags.zone, system)
+ deployment, err := c.config.deploymentIn(system)
if err != nil {
return nil, err
}
@@ -403,9 +390,12 @@ func (c *CLI) system(targetType string) (vespa.System, error) {
// function blocks according to the wait period configured in this CLI. The parameter sessionOrRunID specifies either
// the session ID (local target) or run ID (cloud target) to wait for.
func (c *CLI) service(target vespa.Target, name string, sessionOrRunID int64, cluster string) (*vespa.Service, error) {
- timeout := time.Duration(c.flags.waitSecs) * time.Second
+ timeout, err := c.config.timeout()
+ if err != nil {
+ return nil, err
+ }
if timeout > 0 {
- log.Printf("Waiting up to %s %s for %s service to become available ...", color.CyanString(strconv.Itoa(c.flags.waitSecs)), color.CyanString("seconds"), color.CyanString(name))
+ log.Printf("Waiting up to %s for %s service to become available ...", color.CyanString(timeout.String()), color.CyanString(name))
}
s, err := target.Service(name, timeout, sessionOrRunID, cluster)
if err != nil {
@@ -414,13 +404,17 @@ func (c *CLI) service(target vespa.Target, name string, sessionOrRunID int64, cl
return s, nil
}
-func (c *CLI) createDeploymentOptions(pkg vespa.ApplicationPackage, target vespa.Target) vespa.DeploymentOptions {
+func (c *CLI) createDeploymentOptions(pkg vespa.ApplicationPackage, target vespa.Target) (vespa.DeploymentOptions, error) {
+ timeout, err := c.config.timeout()
+ if err != nil {
+ return vespa.DeploymentOptions{}, err
+ }
return vespa.DeploymentOptions{
ApplicationPackage: pkg,
Target: target,
- Timeout: time.Duration(c.flags.waitSecs) * time.Second,
+ Timeout: timeout,
HTTPClient: c.httpClient,
- }
+ }, nil
}
// isCI returns true if running inside a continuous integration environment.
@@ -520,9 +514,25 @@ func athenzKeyPair() (KeyPair, error) {
return KeyPair{KeyPair: kp, CertificateFile: certFile, PrivateKeyFile: keyFile}, nil
}
-func applicationSource(args []string) string {
- if len(args) > 0 {
- return args[0]
+// applicationPackageFrom returns an application loaded from args. If args is empty, the application package is loaded
+// from the working directory. If requirePackaging is true, the application package is required to be packaged with mvn
+// package.
+func (c *CLI) applicationPackageFrom(args []string, requirePackaging bool) (vespa.ApplicationPackage, error) {
+ path := "."
+ if len(args) == 1 {
+ path = args[0]
+ stat, err := os.Stat(path)
+ if err != nil {
+ return vespa.ApplicationPackage{}, err
+ }
+ if stat.IsDir() {
+ // Using an explicit application directory, look for local config in that directory too
+ if err := c.config.loadLocalConfigFrom(path, false, false); err != nil {
+ return vespa.ApplicationPackage{}, err
+ }
+ }
+ } else if len(args) > 1 {
+ return vespa.ApplicationPackage{}, fmt.Errorf("expected 0 or 1 arguments, got %d", len(args))
}
- return "."
+ return vespa.FindApplicationPackage(path, requirePackaging)
}
diff --git a/client/go/cmd/status.go b/client/go/cmd/status.go
index 1eacff56354..65da8f8f1c0 100644
--- a/client/go/cmd/status.go
+++ b/client/go/cmd/status.go
@@ -7,8 +7,6 @@ package cmd
import (
"fmt"
"log"
- "strconv"
- "time"
"github.com/fatih/color"
"github.com/spf13/cobra"
@@ -76,9 +74,12 @@ func printServiceStatus(cli *CLI, name string) error {
if err != nil {
return err
}
- timeout := time.Duration(cli.flags.waitSecs) * time.Second
+ timeout, err := cli.config.timeout()
+ if err != nil {
+ return err
+ }
if timeout > 0 {
- log.Printf("Waiting up to %s %s for service to become ready ...", color.CyanString(strconv.Itoa(cli.flags.waitSecs)), color.CyanString("seconds"))
+ log.Printf("Waiting up to %s for service to become ready ...", color.CyanString(timeout.String()))
}
s, err := t.Service(name, timeout, 0, "")
if err != nil {
diff --git a/client/go/cmd/test.go b/client/go/cmd/test.go
index acb6231623e..1c452acd7f6 100644
--- a/client/go/cmd/test.go
+++ b/client/go/cmd/test.go
@@ -77,7 +77,7 @@ func runTests(cli *CLI, rootPath string, dryRun bool) (int, []string, error) {
if err != nil {
return 0, nil, errHint(err, "See https://docs.vespa.ai/en/reference/testing")
}
- context := testContext{testsPath: rootPath, dryRun: dryRun, cli: cli, zone: cli.flags.zone}
+ context := testContext{testsPath: rootPath, dryRun: dryRun, cli: cli}
previousFailed := false
for _, test := range tests {
if !test.IsDir() && filepath.Ext(test.Name()) == ".json" {
@@ -469,7 +469,6 @@ type response struct {
type testContext struct {
cli *CLI
- zone string
lazyTarget vespa.Target
testsPath string
dryRun bool
diff --git a/client/go/cmd/testutil_test.go b/client/go/cmd/testutil_test.go
index e5c69e38e93..26d0369a215 100644
--- a/client/go/cmd/testutil_test.go
+++ b/client/go/cmd/testutil_test.go
@@ -6,12 +6,10 @@ import (
"path/filepath"
"testing"
- "github.com/spf13/viper"
"github.com/vespa-engine/vespa/client/go/mock"
)
func newTestCLI(t *testing.T, envVars ...string) (*CLI, *bytes.Buffer, *bytes.Buffer) {
- t.Cleanup(viper.Reset)
homeDir := filepath.Join(t.TempDir(), ".vespa")
cacheDir := filepath.Join(t.TempDir(), ".cache", "vespa")
env := []string{"VESPA_CLI_HOME=" + homeDir, "VESPA_CLI_CACHE_DIR=" + cacheDir}
diff --git a/client/go/vespa/application.go b/client/go/vespa/application.go
index 7bace69ec52..c03f4d6f644 100644
--- a/client/go/vespa/application.go
+++ b/client/go/vespa/application.go
@@ -196,7 +196,8 @@ func copyFile(src *zip.File, dst string) error {
return err
}
-// FindApplicationPackage finds the path to an application package from the zip file or directory zipOrDir.
+// FindApplicationPackage finds the path to an application package from the zip file or directory zipOrDir. If
+// requirePackaging is true, the application package is required to be packaged with mvn package.
func FindApplicationPackage(zipOrDir string, requirePackaging bool) (ApplicationPackage, error) {
if isZip(zipOrDir) {
return ApplicationPackage{Path: zipOrDir}, nil
@@ -210,7 +211,7 @@ func FindApplicationPackage(zipOrDir string, requirePackaging bool) (Application
return ApplicationPackage{Path: zip}, nil
}
if requirePackaging {
- return ApplicationPackage{}, errors.New("pom.xml exists but no target/application.zip. Run mvn package first")
+ return ApplicationPackage{}, errors.New("found pom.xml, but target/application.zip does not exist: run 'mvn package' first")
}
}
if path := filepath.Join(zipOrDir, "src", "main", "application"); util.PathExists(path) {