Skip to content

Commit

Permalink
Add support for passing in custom environment variables (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
mglazer authored and uschi2000 committed Oct 25, 2016
1 parent 7ac02b3 commit fd9ec26
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 14 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -61,5 +68,13 @@ and `<custom.xyz>` 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).
18 changes: 10 additions & 8 deletions launchlib/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
69 changes: 66 additions & 3 deletions launchlib/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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)
}

}
68 changes: 65 additions & 3 deletions launchlib/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -80,16 +92,66 @@ 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)
}
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)
}
104 changes: 104 additions & 0 deletions launchlib/launcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down

0 comments on commit fd9ec26

Please sign in to comment.