Skip to content

Commit

Permalink
Support set/unset environment variables through tanzu config file (vm…
Browse files Browse the repository at this point in the history
…ware-tanzu#1086)

- The change allows users to configure env variables as part of tanzu
  config file
- Env configuration can be set using `tanzu config set env.global.<variable>
  <value>` or `tanzu config set env.<plugin>.<variable>` command
- User can unset the already configured settings with new `tanzu config
  unset env.<plugin>.<variable>` command
- In the case of conflicts, meaning user has set environment variable
  outside tanzu config file with `export FOO=bar` and as part of tanzu
  config file, higer precedence is given to user configured environment
  variable to allow user to override config for one off command runs
  • Loading branch information
anujc25 authored and yharish991 committed Nov 9, 2021
1 parent ac4b754 commit 9d8de33
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 16 deletions.
10 changes: 10 additions & 0 deletions apis/config/v1alpha1/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ func (c *ClientConfig) IsConfigFeatureActivated(featurePath string) (bool, error
return booleanValue, nil
}

// GetEnvConfigurations returns a map of environment variables to values
// it returns nil if configuration is not yet defined
func (c *ClientConfig) GetEnvConfigurations(plugin string) EnvMap {
if c.ClientOptions == nil || c.ClientOptions.Env == nil ||
c.ClientOptions.Env[plugin] == nil {
return nil
}
return c.ClientOptions.Env[plugin]
}

// SplitFeaturePath splits a features path into the pluginName and the featureName
// For example "features.management-cluster.dual-stack" returns "management-cluster", "dual-stack"
// An error results from a malformed path, including any path that does not start with "features."
Expand Down
4 changes: 4 additions & 0 deletions apis/config/v1alpha1/clientconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,15 @@ type ClientOptions struct {
// CLI options specific to the CLI.
CLI *CLIOptions `json:"cli,omitempty" yaml:"cli"`
Features map[string]FeatureMap `json:"features,omitempty" yaml:"features"`
Env map[string]EnvMap `json:"env,omitempty" yaml:"env"`
}

// FeatureMap is simply a hash table, but needs an explicit type to be an object in another hash map (cf ClientOptions.Features)
type FeatureMap map[string]string

// EnvMap is simply a hash table, but needs an explicit type to be an object in another hash map (cf ClientOptions.Env)
type EnvMap map[string]string

// CLIOptions are options for the CLI.
type CLIOptions struct {
// Repositories are the plugin repositories.
Expand Down
111 changes: 101 additions & 10 deletions pkg/v1/cli/command/core/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ import (
"github.com/vmware-tanzu/tanzu-framework/pkg/v1/config"
)

// ConfigLiterals used with set/unset commands
const (
ConfigLiteralFeatures = "features"
ConfigLiteralEnv = "env"
)

func init() {
configCmd.SetUsageFunc(cli.SubCmdUsageFunc)
configCmd.AddCommand(
getConfigCmd,
initConfigCmd,
setConfigCmd,
unsetConfigCmd,
serversCmd,
)
serversCmd.AddCommand(listServersCmd)
Expand Down Expand Up @@ -67,7 +74,7 @@ var getConfigCmd = &cobra.Command{

var setConfigCmd = &cobra.Command{
Use: "set <path> <value>",
Short: "Set config values at the given path. path values: [unstable-versions, features.global.<feature>, features.<plugin>.<feature>]",
Short: "Set config values at the given path. path values: [unstable-versions, features.global.<feature>, features.<plugin>.<feature>, env.global.<variable>, env.<plugin>.<variable>]",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return errors.Errorf("both path and value are required")
Expand All @@ -80,7 +87,7 @@ var setConfigCmd = &cobra.Command{
return err
}

err = setFeature(cfg, args[0], args[1])
err = setConfiguration(cfg, args[0], args[1])
if err != nil {
return err
}
Expand All @@ -89,8 +96,8 @@ var setConfigCmd = &cobra.Command{
},
}

// setFeature sets the key-value pair for the given path
func setFeature(cfg *configv1alpha1.ClientConfig, pathParam, value string) error {
// setConfiguration sets the key-value pair for the given path
func setConfiguration(cfg *configv1alpha1.ClientConfig, pathParam, value string) error {
// special cases:
// backward compatibility
if pathParam == "unstable-versions" {
Expand All @@ -100,17 +107,26 @@ func setFeature(cfg *configv1alpha1.ClientConfig, pathParam, value string) error
// parse the param
paramArray := strings.Split(pathParam, ".")
if len(paramArray) != 3 {
return errors.New("unable to parse config path parameter into three parts [" + pathParam + "] (was expecting features.<plugin>.<feature>)")
return errors.New("unable to parse config path parameter into three parts [" + pathParam + "] (was expecting 'features.<plugin>.<feature>' or 'env.<plugin>.<env_variable>')")
}

featuresLiteral := paramArray[0]
configLiteral := paramArray[0]
plugin := paramArray[1]
key := paramArray[2]

if featuresLiteral != "features" {
return errors.New("unsupported config path parameter [" + featuresLiteral + "] (was expecting 'features.<plugin>.<feature>')")
switch configLiteral {
case ConfigLiteralFeatures:
setFeatures(cfg, plugin, key, value)
case ConfigLiteralEnv:
setEnvs(cfg, plugin, key, value)
default:
return errors.New("unsupported config path parameter [" + configLiteral + "] (was expecting 'features.<plugin>.<feature>' or 'env.<plugin>.<env_variable>')")
}

return nil
}

func setFeatures(cfg *configv1alpha1.ClientConfig, plugin, featureName, value string) {
if cfg.ClientOptions == nil {
cfg.ClientOptions = &configv1alpha1.ClientOptions{}
}
Expand All @@ -120,9 +136,20 @@ func setFeature(cfg *configv1alpha1.ClientConfig, pathParam, value string) error
if cfg.ClientOptions.Features[plugin] == nil {
cfg.ClientOptions.Features[plugin] = configv1alpha1.FeatureMap{}
}
cfg.ClientOptions.Features[plugin][key] = value
cfg.ClientOptions.Features[plugin][featureName] = value
}

return nil
func setEnvs(cfg *configv1alpha1.ClientConfig, plugin, envVariable, value string) {
if cfg.ClientOptions == nil {
cfg.ClientOptions = &configv1alpha1.ClientOptions{}
}
if cfg.ClientOptions.Env == nil {
cfg.ClientOptions.Env = make(map[string]configv1alpha1.EnvMap)
}
if cfg.ClientOptions.Env[plugin] == nil {
cfg.ClientOptions.Env[plugin] = configv1alpha1.EnvMap{}
}
cfg.ClientOptions.Env[plugin][envVariable] = value
}

func setUnstableVersions(cfg *configv1alpha1.ClientConfig, value string) error {
Expand Down Expand Up @@ -256,3 +283,67 @@ var deleteServersCmd = &cobra.Command{
return nil
},
}

var unsetConfigCmd = &cobra.Command{
Use: "unset <path>",
Short: "Unset config values at the given path. path values: [features.global.<feature>, features.<plugin>.<feature>, env.global.<variable>, env.<plugin>.<variable>]",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.Errorf("path is required")
}
if len(args) > 1 {
return errors.Errorf("only path is allowed")
}
cfg, err := config.GetClientConfig()
if err != nil {
return err
}

err = unsetConfiguration(cfg, args[0])
if err != nil {
return err
}

return config.StoreClientConfig(cfg)
},
}

// unsetConfiguration unsets the key-value pair for the given path and removes it
func unsetConfiguration(cfg *configv1alpha1.ClientConfig, pathParam string) error {
// parse the param
paramArray := strings.Split(pathParam, ".")
if len(paramArray) != 3 {
return errors.New("unable to parse config path parameter into three parts [" + pathParam + "] (was expecting 'features.<plugin>.<feature>' or 'env.<plugin>.<env_variable>')")
}

configLiteral := paramArray[0]
plugin := paramArray[1]
key := paramArray[2]

switch configLiteral {
case ConfigLiteralFeatures:
unsetFeatures(cfg, plugin, key)
case ConfigLiteralEnv:
unsetEnvs(cfg, plugin, key)
default:
return errors.New("unsupported config path parameter [" + configLiteral + "] (was expecting 'features.<plugin>.<feature>' or 'env.<plugin>.<env_variable>')")
}

return nil
}

func unsetFeatures(cfg *configv1alpha1.ClientConfig, plugin, featureName string) {
if cfg.ClientOptions == nil || cfg.ClientOptions.Features == nil ||
cfg.ClientOptions.Features[plugin] == nil {
return
}
delete(cfg.ClientOptions.Features[plugin], featureName)
}

func unsetEnvs(cfg *configv1alpha1.ClientConfig, plugin, envVariable string) {
if cfg.ClientOptions == nil || cfg.ClientOptions.Env == nil ||
cfg.ClientOptions.Env[plugin] == nil {
return
}
delete(cfg.ClientOptions.Env[plugin], envVariable)
}
53 changes: 47 additions & 6 deletions pkg/v1/cli/command/core/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import (
"strings"
"testing"

"github.com/stretchr/testify/assert"

configv1alpha1 "github.com/vmware-tanzu/tanzu-framework/apis/config/v1alpha1"
)

// Test_config_MalformedPathArg validates functionality when an invalid argument is provided.
func Test_config_MalformedPathArg(t *testing.T) {
err := setFeature(nil, "invalid-arg", "")
err := setConfiguration(nil, "invalid-arg", "")
if err == nil {
t.Error("Malformed path argument should have resulted in an error")
}
Expand All @@ -24,7 +26,7 @@ func Test_config_MalformedPathArg(t *testing.T) {

// Test_config_InvalidPathArg validates functionality when an invalid argument is provided.
func Test_config_InvalidPathArg(t *testing.T) {
err := setFeature(nil, "shouldbefeatures.plugin.feature", "")
err := setConfiguration(nil, "shouldbefeatures.plugin.feature", "")
if err == nil {
t.Error("Invalid path argument should have resulted in an error")
}
Expand All @@ -37,7 +39,7 @@ func Test_config_InvalidPathArg(t *testing.T) {
// Test_config_UnstableVersions validates functionality when path argument unstable-versions is provided.
func Test_config_UnstableVersions(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
err := setFeature(cfg, "unstable-versions", "experimental")
err := setConfiguration(cfg, "unstable-versions", "experimental")
if err != nil {
t.Errorf("Unexpected error returned for unstable-versions path argument: %s", err.Error())
}
Expand All @@ -50,7 +52,7 @@ func Test_config_UnstableVersions(t *testing.T) {
// Test_config_InvalidUnstableVersions validates functionality when invalid unstable-versions is provided.
func Test_config_InvalidUnstableVersions(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
err := setFeature(cfg, "unstable-versions", "invalid-unstable-versions-value")
err := setConfiguration(cfg, "unstable-versions", "invalid-unstable-versions-value")
if err == nil {
t.Error("Invalid unstable-versions should have resulted in error")
}
Expand All @@ -64,7 +66,7 @@ func Test_config_InvalidUnstableVersions(t *testing.T) {
func Test_config_GlobalFeature(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
value := "bar"
err := setFeature(cfg, "features.global.foo", value)
err := setConfiguration(cfg, "features.global.foo", value)
if err != nil {
t.Errorf("Unexpected error returned for global features path argument: %s", err.Error())
}
Expand All @@ -78,7 +80,7 @@ func Test_config_GlobalFeature(t *testing.T) {
func Test_config_Feature(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
value := "barr"
err := setFeature(cfg, "features.any-plugin.foo", value)
err := setConfiguration(cfg, "features.any-plugin.foo", value)
if err != nil {
t.Errorf("Unexpected error returned for any-plugin features path argument: %s", err.Error())
}
Expand All @@ -87,3 +89,42 @@ func Test_config_Feature(t *testing.T) {
t.Error("cfg.ClientOptions.Features[\"any-plugin\"][\"foo\"] was not assigned the value \"" + value + "\"")
}
}

// Test_config_GlobalEnv validates functionality when env feature path argument is provided.
func Test_config_GlobalEnv(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
value := "baar"
err := setConfiguration(cfg, "env.global.foo", value)
if err != nil {
t.Errorf("Unexpected error returned for global env path argument: %s", err.Error())
}

if cfg.ClientOptions.Env["global"]["foo"] != value {
t.Error("cfg.ClientOptions.Env[\"global\"][\"foo\"] was not assigned the value \"" + value + "\"")
}
}

// Test_config_Env validates functionality when normal env path argument is provided.
func Test_config_Env(t *testing.T) {
cfg := &configv1alpha1.ClientConfig{}
value := "baarr"
err := setConfiguration(cfg, "env.any-plugin.foo", value)
if err != nil {
t.Errorf("Unexpected error returned for any-plugin env path argument: %s", err.Error())
}

if cfg.ClientOptions.Env["any-plugin"]["foo"] != value {
t.Error("cfg.ClientOptions.Features[\"any-plugin\"][\"foo\"] was not assigned the value \"" + value + "\"")
}
}

// Test_config_Env validates functionality when normal env path argument is provided.
func Test_config_IncorrectConfigLiteral(t *testing.T) {
assert := assert.New(t)

cfg := &configv1alpha1.ClientConfig{}
value := "b"
err := setConfiguration(cfg, "fake.any-plugin.foo", value)
assert.NotNil(err)
assert.Contains(err.Error(), "unsupported config path parameter [fake] (was expecting 'features.<plugin>.<feature>' or 'env.<plugin>.<env_variable>')")
}
5 changes: 5 additions & 0 deletions pkg/v1/cli/command/core/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ func NewRootCmd() (*cobra.Command, error) {
}
}

// configure defined global environment variables
// under tanzu config file
// global environment variables can be defined as `env.global.FOO`
config.ConfigureEnvVariables("global")

for _, plugin := range plugins {
RootCmd.AddCommand(cli.GetCmd(plugin))
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/v1/cli/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"path/filepath"

"github.com/aunum/log"

"github.com/vmware-tanzu/tanzu-framework/pkg/v1/config"
)

// Runner is a plugin runner.
Expand All @@ -38,6 +40,9 @@ func NewRunner(name, pluginAbsPath string, args []string, options ...Option) *Ru

// Run runs a plugin.
func (r *Runner) Run(ctx context.Context) error {
// configures plugin specific environment variables
// defined under tanzu config file
config.ConfigureEnvVariables(r.name)
return r.runStdOutput(ctx, r.pluginPath())
}

Expand Down
28 changes: 28 additions & 0 deletions pkg/v1/config/clientconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,3 +607,31 @@ func GetDiscoverySources(serverName string) []configv1alpha1.PluginDiscovery {
}
return discoverySources
}

// GetEnvConfigurations returns a map of configured environment variables
// to values as part of tanzu configuration file
// it returns nil if configuration is not yet defined
func GetEnvConfigurations(plugin string) configv1alpha1.EnvMap {
cfg, err := GetClientConfig()
if err != nil {
return nil
}
return cfg.GetEnvConfigurations(plugin)
}

// ConfigureEnvVariables reads and configures provided environment variables
// as part of tanzu configuration file based on the provided plugin name
// plugin can be a name of the plugin or 'global' if it generic variable
func ConfigureEnvVariables(plugin string) {
envMap := GetEnvConfigurations(plugin)
if envMap == nil {
return
}
for variable, value := range envMap {
// If environment variable is not already set
// set the environment variable
if os.Getenv(variable) == "" {
os.Setenv(variable, value)
}
}
}

0 comments on commit 9d8de33

Please sign in to comment.