From fd9ec268b240a0968a2d632c8252a620a788f9ed Mon Sep 17 00:00:00 2001 From: Mike Glazer Date: Mon, 24 Oct 2016 17:12:42 -0700 Subject: [PATCH] Add support for passing in custom environment variables (#9) --- README.md | 15 ++++++ launchlib/config.go | 18 ++++--- launchlib/config_test.go | 69 ++++++++++++++++++++++-- launchlib/launcher.go | 68 ++++++++++++++++++++++-- launchlib/launcher_test.go | 104 +++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e777bc12..2e74f695 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ javaHome: javaHome # The classpath entries; the final classpath is the ':'-concatenated list in the given order classpath: - ./foo.jar +# Environment Variables to be set in the environment +env: + CUSTOM_VAR: CUSTOM_VALUE # JVM options to be passed to the java command jvmOpts: - '-Xmx1g' @@ -34,6 +37,10 @@ args: # CustomLauncherConfig configType: java configVersion: 1 +# Environment variables to be set in the runtime environment +env: + CUSTOM_VAR: CUSTOM_VALUE + CUSTOM_PATH: '{{CWD}}/some/path' # JVM options to be passed to the java command jvmOpts: - '-Xmx2g' @@ -61,5 +68,13 @@ and `` refer to the options from the two configuration files, respec Note that the custom `jvmOpts` appear after the static `jvmOpts` and thus typically take precendence; the exact behaviour may depend on the Java distribution. +`env` block, both in static and custom configuration, supports restricted set of automatic expansions for values +assigned to environment variables. Variables are expanded if they are surrounded with `{{` and `}}` as shown above +for `CUSTOM_PATH`. The following fixed expansions are supported: + +* `{{CWD}}`: The current working directory of the user which executed this process + +Expansions are only performed on the values. No expansions are performed on the keys. + # License This repository is made available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/launchlib/config.go b/launchlib/config.go index 87c18c1a..25cc1aaf 100644 --- a/launchlib/config.go +++ b/launchlib/config.go @@ -22,20 +22,22 @@ import ( ) type StaticLauncherConfig struct { - ConfigType string `yaml:"configType"` - ConfigVersion int `yaml:"configVersion"` - ServiceName string `yaml:"serviceName"` - MainClass string `yaml:"mainClass"` - JavaHome string `yaml:"javaHome"` + ConfigType string `yaml:"configType"` + ConfigVersion int `yaml:"configVersion"` + ServiceName string `yaml:"serviceName"` + MainClass string `yaml:"mainClass"` + JavaHome string `yaml:"javaHome"` + Env map[string]string `yaml:"env"` Classpath []string JvmOpts []string `yaml:"jvmOpts"` Args []string } type CustomLauncherConfig struct { - ConfigType string `yaml:"configType"` - ConfigVersion int `yaml:"configVersion"` - JvmOpts []string `yaml:"jvmOpts"` + ConfigType string `yaml:"configType"` + ConfigVersion int `yaml:"configVersion"` + JvmOpts []string `yaml:"jvmOpts"` + Env map[string]string `yaml:"env"` } func ParseStaticConfig(yamlString []byte) StaticLauncherConfig { diff --git a/launchlib/config_test.go b/launchlib/config_test.go index 39b954e0..dec947b2 100644 --- a/launchlib/config_test.go +++ b/launchlib/config_test.go @@ -26,6 +26,9 @@ configType: java configVersion: 1 mainClass: mainClass javaHome: javaHome +env: + SOME_ENV_VAR: /etc/profile + OTHER_ENV_VAR: /etc/redhat-release classpath: - classpath1 - classpath2 @@ -41,9 +44,13 @@ args: ConfigVersion: 1, MainClass: "mainClass", JavaHome: "javaHome", - Classpath: []string{"classpath1", "classpath2"}, - JvmOpts: []string{"jvmOpt1", "jvmOpt2"}, - Args: []string{"arg1", "arg2"}} + Env: map[string]string{ + "SOME_ENV_VAR": "/etc/profile", + "OTHER_ENV_VAR": "/etc/redhat-release", + }, + Classpath: []string{"classpath1", "classpath2"}, + JvmOpts: []string{"jvmOpt1", "jvmOpt2"}, + Args: []string{"arg1", "arg2"}} config := ParseStaticConfig(data) if !reflect.DeepEqual(config, expectedConfig) { @@ -55,6 +62,32 @@ func TestParseCustomConfig(t *testing.T) { var data = []byte(` configType: java configVersion: 1 +env: + SOME_ENV_VAR: /etc/profile + OTHER_ENV_VAR: /etc/redhat-release +jvmOpts: + - jvmOpt1 + - jvmOpt2 +`) + expectedConfig := CustomLauncherConfig{ + ConfigType: "java", + ConfigVersion: 1, + Env: map[string]string{ + "SOME_ENV_VAR": "/etc/profile", + "OTHER_ENV_VAR": "/etc/redhat-release", + }, + JvmOpts: []string{"jvmOpt1", "jvmOpt2"}} + + config := ParseCustomConfig(data) + if !reflect.DeepEqual(config, expectedConfig) { + t.Errorf("Expected config %v, found %v", expectedConfig, config) + } +} + +func TestParseCustomConfigWithoutEnv(t *testing.T) { + var data = []byte(` +configType: java +configVersion: 1 jvmOpts: - jvmOpt1 - jvmOpt2 @@ -68,4 +101,34 @@ jvmOpts: if !reflect.DeepEqual(config, expectedConfig) { t.Errorf("Expected config %v, found %v", expectedConfig, config) } + + if config.Env != nil { + t.Errorf("Expected environment to be nil, but was %v", config.Env) + } +} + +func TestParseCustomConfigWithEnvPlaceholder(t *testing.T) { + var data = []byte(` +configType: java +configVersion: 1 +env: + SOME_ENV_VAR: '{{CWD}}/etc/profile' +jvmOpts: + - jvmOpt1 + - jvmOpt2 +`) + + expectedConfig := CustomLauncherConfig{ + ConfigType: "java", + ConfigVersion: 1, + Env: map[string]string{ + "SOME_ENV_VAR": "{{CWD}}/etc/profile", + }, + JvmOpts: []string{"jvmOpt1", "jvmOpt2"}} + + config := ParseCustomConfig(data) + if !reflect.DeepEqual(config, expectedConfig) { + t.Errorf("Expected config %v, found %v", expectedConfig, config) + } + } diff --git a/launchlib/launcher.go b/launchlib/launcher.go index 5068e9c9..1dd8d398 100644 --- a/launchlib/launcher.go +++ b/launchlib/launcher.go @@ -23,6 +23,18 @@ import ( "syscall" ) +const ( + TemplateDelimsOpen = "{{" + TemplateDelimsClose = "}}" +) + +type processExecutor interface { + Exec(executable string, args []string, env []string) error +} + +type syscallProcessExecutor struct { +} + // Returns explicitJavaHome if it is not the empty string, or the value of the JAVA_HOME environment variable otherwise. // Panics if neither of them is set. func getJavaHome(explicitJavaHome string) string { @@ -80,12 +92,18 @@ func Launch(staticConfig *StaticLauncherConfig, customConfig *CustomLauncherConf args = append(args, staticConfig.Args...) fmt.Printf("Argument list to Java binary: %v\n\n", args) - execWithChecks(javaCommand, args) + env := replaceEnvironmentVariables(merge(staticConfig.Env, customConfig.Env)) + + execWithChecks(javaCommand, args, env, &syscallProcessExecutor{}) } -func execWithChecks(javaExecutable string, args []string) { +func execWithChecks(javaExecutable string, args []string, customEnv map[string]string, p processExecutor) { env := os.Environ() - execErr := syscall.Exec(javaExecutable, args, env) + for key, value := range customEnv { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + + execErr := p.Exec(javaExecutable, args, env) if execErr != nil { if os.IsNotExist(execErr) { fmt.Println("Java Executable not found at:", javaExecutable) @@ -93,3 +111,47 @@ func execWithChecks(javaExecutable string, args []string) { panic(execErr) } } + +func (s *syscallProcessExecutor) Exec(executable string, args []string, env []string) error { + return syscall.Exec(executable, args, env) +} + +// Performs replacement of all replaceable values in env, returning a new +// map, with the same keys as env, but possibly changed values +func replaceEnvironmentVariables(env map[string]string) map[string]string { + replacer := createReplacer() + + returnMap := make(map[string]string) + for key, value := range env { + returnMap[key] = replacer.Replace(value) + } + + return returnMap +} + +// copy all the keys and values from overrideMap into origMap. If a key already +// exists in origMap, it's value is overridden +func merge(origMap map[string]string, overrideMap map[string]string) map[string]string { + if overrideMap == nil { + return origMap + } + + returnMap := make(map[string]string) + for key, value := range origMap { + returnMap[key] = value + } + for key, value := range overrideMap { + returnMap[key] = value + } + return returnMap +} + +func createReplacer() *strings.Replacer { + return strings.NewReplacer( + delim("CWD"), getWorkingDir(), + ) +} + +func delim(str string) string { + return fmt.Sprintf("%s%s%s", TemplateDelimsOpen, str, TemplateDelimsClose) +} diff --git a/launchlib/launcher_test.go b/launchlib/launcher_test.go index 7e184a2b..144b64d7 100644 --- a/launchlib/launcher_test.go +++ b/launchlib/launcher_test.go @@ -16,10 +16,19 @@ package launchlib import ( + "fmt" "os" + "reflect" + "sort" "testing" ) +type mockProcessExecutor struct { + command string + args []string + env []string +} + func TestGetJavaHome(t *testing.T) { originalJavaHome := os.Getenv("JAVA_HOME") setEnvOrFail("JAVA_HOME", "foo") @@ -36,6 +45,101 @@ func TestGetJavaHome(t *testing.T) { setEnvOrFail("JAVA_HOME", originalJavaHome) } +func TestSetCustomEnvironment(t *testing.T) { + originalEnv := make(map[string]string) + customEnv := map[string]string{ + "SOME_PATH": "{{CWD}}/full/path", + "SOME_VAR": "CUSTOM_VAR", + } + + env := replaceEnvironmentVariables(merge(originalEnv, customEnv)) + + cwd := getWorkingDir() + + if val, ok := env["SOME_PATH"]; ok { + expected := fmt.Sprintf("%s/full/path", cwd) + if val != expected { + t.Errorf("For SOME_PATH, expected %s, but got %s", expected, val) + } + } else { + t.Errorf("Expected SOME_PATH to exist in map but it didn't") + } + + if val, ok := env["SOME_VAR"]; ok { + if val != "CUSTOM_VAR" { + t.Errorf("For SOME_VAR, expected %s, but got %s", "CUSTOM_VAR", val) + } + } else { + t.Errorf("Expected CUSTOM_VAR to exist in map, but it didn't") + } + + m := mockProcessExecutor{} + args := []string{"arg1", "arg2"} + execWithChecks("my-command", args, env, &m) + + if m.command != "my-command" { + t.Errorf("Expected command to be run was %s, but instead was %s", "my-command", m.command) + } + + if !reflect.DeepEqual(m.args, args) { + t.Errorf("Expected incoming args to be %v, but were %v", args, m.args) + } + + startingEnv := os.Environ() + expectedEnv := append(startingEnv, []string{ + fmt.Sprintf("SOME_PATH=%s/full/path", cwd), + "SOME_VAR=CUSTOM_VAR", + }...) + + sort.Strings(m.env) + sort.Strings(expectedEnv) + if !reflect.DeepEqual(m.env, expectedEnv) { + t.Errorf("Expected custom environment to be %v, but instead was %v", expectedEnv, m.env) + } +} + +func TestUnknownVariablesAreNotExpanded(t *testing.T) { + originalEnv := make(map[string]string) + customEnv := map[string]string{ + "SOME_VAR": "{{FOO}}", + } + + env := replaceEnvironmentVariables(merge(originalEnv, customEnv)) + + if val, ok := env["SOME_VAR"]; ok { + if val != "{{FOO}}" { + t.Errorf("For SOME_VAR, expected %s, but got %s", "{{FOO}}", val) + } + } else { + t.Errorf("Expected SOME_VAR to exist in map, but it didn't") + } +} + +func TestKeysAreNotExpanded(t *testing.T) { + originalEnv := make(map[string]string) + customEnv := map[string]string{ + "{{CWD}}": "Value", + } + + env := replaceEnvironmentVariables(merge(originalEnv, customEnv)) + + if val, ok := env["{{CWD}}"]; ok { + if val != "Value" { + t.Errorf("For %%CWD%%, expected %s, but got %s", "Value", val) + } + } else { + t.Errorf("Expected %%CWD%% to exist in map and not be expanded, but it didn't") + } +} + +func (m *mockProcessExecutor) Exec(command string, args []string, env []string) error { + m.command = command + m.args = args + m.env = env + + return nil +} + func setEnvOrFail(key string, value string) { err := os.Setenv(key, value) if err != nil {