Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

Support set/unset environment variables through tanzu config file #1086

Merged
merged 1 commit into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
}