Merge pull request #2751 from alexquick/feature-config-golang
Move config plugin to golang
This commit is contained in:
@@ -5,10 +5,11 @@ Typically an application will require some configuration to run properly. Dokku
|
||||
The `config` plugin provides the following commands to manage your variables:
|
||||
|
||||
```
|
||||
config (<app>|--global) Display all global or app-specific config vars
|
||||
config:get (<app>|--global) KEY Display a global or app-specific config value
|
||||
config:set (<app>|--global) KEY1=VALUE1 [KEY2=VALUE2 ...] Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...] Unset one or more config vars
|
||||
config (<app>|--global) Display all global or app-specific config vars
|
||||
config:get (<app>|--global) KEY Display a global or app-specific config value
|
||||
config:set (<app>|--global) KEY1=VALUE1 [KEY2=VALUE2 ...] Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...] Unset one or more config vars
|
||||
config:export [--format=FORMAT] [--merged] (<app>|--global) Export all global or app-specific configuration
|
||||
```
|
||||
|
||||
The variables are available both at run time and during the application build/compilation step for buildpack-based deploys. For security reasons - and as per [docker recommendations](https://github.com/docker/docker/issues/13490) - Dockerfile-based deploys have variables available *only* during runtime, as noted in [this issue](https://github.com/dokku/dokku/issues/1860).
|
||||
@@ -23,10 +24,15 @@ You can set multiple environment variables at once:
|
||||
dokku config:set node-js-app ENV=prod COMPILE_ASSETS=1
|
||||
```
|
||||
|
||||
When setting variables with whitespaces, you need to escape them:
|
||||
> Note: Whitespace and special characters get tricky. If you are using dokku locally you don't need to do any special escaping. If you are using dokku over ssh you will need to backslash-escape spaces:
|
||||
```shell
|
||||
dokku config:set node-js-app KEY="VAL\ WITH\ SPACES"
|
||||
```
|
||||
|
||||
Dokku can also read base64 encoded values. That's the easiest way to set a value with newlines or spaces. To set a value with newlines you need to base64 encode it first and pass the `--encoded` flag:
|
||||
|
||||
```shell
|
||||
dokku config:set node-js-app KEY=\"VAL\ WITH\ SPACES\"
|
||||
dokku config:set --encoded node-js-app KEY="$(base64 ~/.ssh/id_rsa)"
|
||||
```
|
||||
|
||||
When setting or unsetting environment variables, you may wish to avoid an application restart. This is useful when developing plugins or when setting multiple environment variables in a scripted manner. To do so, use the `--no-restart` flag:
|
||||
@@ -35,23 +41,24 @@ When setting or unsetting environment variables, you may wish to avoid an applic
|
||||
dokku config:set --no-restart node-js-app ENV=prod
|
||||
```
|
||||
|
||||
If you wish to have the variables output in an `eval`-compatible form, you can use the `--export` flag:
|
||||
If you wish to have the variables output in an `eval`-compatible form, you can use the `config:export` command
|
||||
|
||||
```shell
|
||||
dokku config node-js-app --export
|
||||
dokku config:export node-js-app
|
||||
# outputs variables in the form:
|
||||
#
|
||||
# export ENV='prod'
|
||||
# export COMPILE_ASSETS='1'
|
||||
|
||||
# source in all the node-js-app app environment variables
|
||||
eval $(dokku config node-js-app --export)
|
||||
eval $(dokku config:export node-js-app)
|
||||
```
|
||||
|
||||
You can also output the variables in a single-line for usage in command-line utilities with the `--shell` flag:
|
||||
You can control the format of the exported variables with the `--format` flag.
|
||||
`--format=shell` will output the variables in a single-line for usage in command-line utilities:
|
||||
|
||||
```shell
|
||||
dokku config node-js-app --shell
|
||||
dokku config:export --format shell node-js-app
|
||||
|
||||
# outputs variables in the form:
|
||||
#
|
||||
|
||||
@@ -23,8 +23,10 @@ get_phase_script() {
|
||||
|
||||
execute_script() {
|
||||
declare desc="executes appropriate phase script key from app.json"
|
||||
local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
|
||||
local PHASE_SCRIPT_KEY="$3"
|
||||
local APP="$1"; local IMAGE_TAG="$2"; local PHASE_SCRIPT_KEY="$3"
|
||||
local IMAGE id
|
||||
|
||||
IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
|
||||
local SCRIPT_CMD=$(get_phase_script "$IMAGE" "$PHASE_SCRIPT_KEY" 2>/dev/null)
|
||||
if [[ -n "$SCRIPT_CMD" ]];then
|
||||
dokku_log_info1 "Running '$SCRIPT_CMD' in app container"
|
||||
@@ -55,8 +57,11 @@ execute_script() {
|
||||
# strip --restart args from DOCKER_ARGS
|
||||
local DOCKER_ARGS=$(sed -e "s/--restart=[[:graph:]]\+[[:blank:]]\?//g" <<< "$DOCKER_ARGS")
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
local id=$(docker run "$DOKKU_GLOBAL_RUN_ARGS" -e DOKKU_TRACE="$DOKKU_TRACE" --label=dokku_phase_script="${PHASE_SCRIPT_KEY}" -d -v "$CACHE_DIR:/cache" $DOCKER_ARGS "$IMAGE" /bin/bash -c "$COMMAND")
|
||||
# eval args as array to respect escapes
|
||||
declare -a ARG_ARRAY
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
|
||||
id=$(docker run "$DOKKU_GLOBAL_RUN_ARGS" -e DOKKU_TRACE="$DOKKU_TRACE" --label=dokku_phase_script="${PHASE_SCRIPT_KEY}" -d -v "$CACHE_DIR:/cache" "${ARG_ARRAY[@]}" "$IMAGE" /bin/bash -c "$COMMAND")
|
||||
if test "$(docker wait "$id")" -eq 0; then
|
||||
dokku_container_log_verbose_quiet "$id"
|
||||
if [[ "$PHASE_SCRIPT_KEY" != "postdeploy" ]];then
|
||||
|
||||
@@ -17,6 +17,7 @@ app_user_pre_deploy_trigger() {
|
||||
local desc="app user pre-deploy trigger"
|
||||
local APP="$1"; local IMAGE_TAG="$2"; local IMAGE; local DOCKER_ARGS
|
||||
local DOKKU_APP_TYPE DOKKU_APP_USER APP_PATHS CONTAINER_PATHS
|
||||
declare -a ARG_ARRAY
|
||||
|
||||
IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG")
|
||||
|
||||
@@ -27,14 +28,15 @@ app_user_pre_deploy_trigger() {
|
||||
|
||||
if [[ -n "$APP_PATHS" ]]; then
|
||||
CONTAINER_PATHS=$(echo "$APP_PATHS" | awk -F ':' '{ print $2 }' | xargs)
|
||||
DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG" | xargs)
|
||||
DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG")
|
||||
# strip --restart args from DOCKER_ARGS
|
||||
DOCKER_ARGS=$(sed -e "s/--restart=[[:graph:]]\+[[:blank:]]\?//g" <<< "$DOCKER_ARGS")
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
fi
|
||||
|
||||
if [[ "$DOKKU_APP_TYPE" == "herokuish" ]] && [[ -n "$CONTAINER_PATHS" ]]; then
|
||||
# shellcheck disable=SC2086
|
||||
docker run $DOKKU_GLOBAL_RUN_ARGS $DOCKER_ARGS $IMAGE /bin/bash -c "find $CONTAINER_PATHS -not -user $DOKKU_APP_USER -print0 | xargs -0 -r chown -R $DOKKU_APP_USER" || true
|
||||
docker run $DOKKU_GLOBAL_RUN_ARGS "${ARG_ARRAY[@]}" $IMAGE /bin/bash -c "find $CONTAINER_PATHS -not -user $DOKKU_APP_USER -print0 | xargs -0 -r chown -R $DOKKU_APP_USER" || true
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ dokku_run_cmd() {
|
||||
local DOCKER_ARGS=$(: | plugn trigger docker-args-run "$APP" "$IMAGE_TAG")
|
||||
[[ "$DOKKU_TRACE" ]] && local DOCKER_ARGS+=" -e TRACE=true "
|
||||
|
||||
declare -a ARG_ARRAY
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
|
||||
local DOKKU_RUN_OPTS=" -e DYNO=run.$RANDOM"
|
||||
if [[ "$DOKKU_RM_CONTAINER" ]]; then
|
||||
DOKKU_RUN_OPTS+=" --rm"
|
||||
@@ -44,7 +47,7 @@ dokku_run_cmd() {
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker run $DOKKU_GLOBAL_RUN_ARGS $DOKKU_RUN_OPTS $DOCKER_ARGS $IMAGE $EXEC_CMD "$@"
|
||||
docker run $DOKKU_GLOBAL_RUN_ARGS $DOKKU_RUN_OPTS "${ARG_ARRAY[@]}" $IMAGE $EXEC_CMD "$@"
|
||||
}
|
||||
|
||||
dokku_run_cmd "$@"
|
||||
|
||||
@@ -6,36 +6,26 @@ source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
build_env_pre_build_buildpack() {
|
||||
declare desc="build-env pre-build-buildpack plugin trigger"
|
||||
local trigger="build_env_pre_build_buildpack"
|
||||
local APP="$1"; local IMAGE=$(get_app_image_name "$APP"); local BUILD_ENV=""
|
||||
local APP="$1"
|
||||
local IMAGE id
|
||||
|
||||
verify_app_name "$APP"
|
||||
IMAGE=$(get_app_image_name "$APP");
|
||||
|
||||
[[ -z $(config_get --global CURL_CONNECT_TIMEOUT) ]] && config_set --global CURL_CONNECT_TIMEOUT=90
|
||||
[[ -z $(config_get --global CURL_TIMEOUT) ]] && config_set --global CURL_TIMEOUT=60
|
||||
|
||||
if [[ -n $(config_export global) ]]; then
|
||||
local BUILD_ENV+=$'\n'
|
||||
local BUILD_ENV+=$(config_export global)
|
||||
local BUILD_ENV+=$'\n'
|
||||
fi
|
||||
if [[ -n $(config_export app "$APP") ]]; then
|
||||
local BUILD_ENV+=$'\n'
|
||||
local BUILD_ENV+=$(config_export app "$APP")
|
||||
local BUILD_ENV+=$'\n'
|
||||
fi
|
||||
dokku_log_info1 "Adding BUILD_ENV to build environment..."
|
||||
# create build env files for use in buildpacks like this:
|
||||
# https://github.com/niteoweb/heroku-buildpack-buildout/blob/5879fa3418f7d8e079f1aa5816ba1adde73f4948/bin/compile#L34
|
||||
id=$(config_bundle --merged "$APP" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /tmp/env; cat | tar -x -C /tmp/env")
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
|
||||
if [[ ! -z "$BUILD_ENV" ]]; then
|
||||
dokku_log_info1 "Adding BUILD_ENV to build environment..."
|
||||
# create build env files for use in buildpacks like this:
|
||||
# https://github.com/niteoweb/heroku-buildpack-buildout/blob/5879fa3418f7d8e079f1aa5816ba1adde73f4948/bin/compile#L34
|
||||
local id=$(echo $BUILD_ENV | sed -e 's@export @@g' -e 's@\\n@ @g' | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /tmp/env; for ENV_VAR in $(cat); do echo \$ENV_VAR | cut -d'=' -f2- > /tmp/env/\$(echo \$ENV_VAR | cut -d'=' -f1); done " )
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
|
||||
# create build env for 'old style' buildpacks and dokku plugins
|
||||
local id=$(echo -e "$BUILD_ENV" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "cat >> /app/.env")
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
fi
|
||||
# create build env for 'old style' buildpacks and dokku plugins
|
||||
id=$(config_export app "$APP" --format envfile --merged | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "cat >> /app/.env")
|
||||
test "$(docker wait "$id")" -eq 0
|
||||
docker commit "$id" "$IMAGE" > /dev/null
|
||||
}
|
||||
|
||||
build_env_pre_build_buildpack "$@"
|
||||
|
||||
@@ -343,3 +343,15 @@ func VerifyImage(image string) bool {
|
||||
imageCmd.ShowOutput = false
|
||||
return imageCmd.Execute()
|
||||
}
|
||||
|
||||
//PlugnTrigger fire the given plugn trigger with the given args
|
||||
func PlugnTrigger(triggerName string, args ...string) (string, error) {
|
||||
shellArgs := make([]interface{}, len(args)+2)
|
||||
shellArgs[0] = "trigger"
|
||||
shellArgs[1] = triggerName
|
||||
for i, arg := range args {
|
||||
shellArgs[i+2] = arg
|
||||
}
|
||||
res, err := sh.Command("plugn", shellArgs...).Output()
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
@@ -459,8 +459,10 @@ dokku_build() {
|
||||
|
||||
local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE")
|
||||
[[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true "
|
||||
declare -a ARG_ARRAY
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
# shellcheck disable=SC2086
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -v $CACHE_DIR:/cache -e CACHE_PATH=/cache $DOCKER_ARGS $IMAGE /build)
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -v $CACHE_DIR:/cache -e CACHE_PATH=/cache "${ARG_ARRAY[@]}" $IMAGE /build)
|
||||
docker attach "$cid"
|
||||
test "$(docker wait "$cid")" -eq 0
|
||||
docker commit "$cid" "$IMAGE" > /dev/null
|
||||
@@ -484,9 +486,11 @@ dokku_build() {
|
||||
local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE")
|
||||
# strip --volume and -v args from DOCKER_ARGS
|
||||
local DOCKER_ARGS=$(sed -e "s/--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<< "$DOCKER_ARGS")
|
||||
declare -a ARG_ARRAY
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker build $DOCKER_ARGS $DOKKU_DOCKER_BUILD_OPTS -t $IMAGE .
|
||||
docker build "${ARG_ARRAY[@]}" $DOKKU_DOCKER_BUILD_OPTS -t $IMAGE .
|
||||
|
||||
plugn trigger post-build-dockerfile "$APP"
|
||||
;;
|
||||
@@ -585,6 +589,9 @@ dokku_deploy_cmd() {
|
||||
DOCKER_ARGS+=" -e DYNO=$PROC_TYPE.$CONTAINER_INDEX "
|
||||
[[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true "
|
||||
|
||||
declare -a ARG_ARRAY
|
||||
eval "ARG_ARRAY=($DOCKER_ARGS)"
|
||||
|
||||
[[ "$DOKKU_HEROKUISH" == "true" ]] && local START_CMD="/start $PROC_TYPE"
|
||||
|
||||
if [[ "$DOKKU_HEROKUISH" == "false" ]]; then
|
||||
@@ -606,14 +613,14 @@ dokku_deploy_cmd() {
|
||||
|
||||
if [[ "$DOKKU_NETWORK_BIND_ALL" == "false" ]]; then
|
||||
# shellcheck disable=SC2086
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -e PORT=$DOKKU_PORT $DOCKER_ARGS $IMAGE $START_CMD)
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -e PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOKKU_DOCKER_PORT_ARGS -e PORT=$DOKKU_PORT $DOCKER_ARGS $IMAGE $START_CMD)
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOKKU_DOCKER_PORT_ARGS -e PORT=$DOKKU_PORT "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
|
||||
fi
|
||||
else
|
||||
# shellcheck disable=SC2086
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOCKER_ARGS $IMAGE $START_CMD)
|
||||
cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d "${ARG_ARRAY[@]}" $IMAGE $START_CMD)
|
||||
fi
|
||||
|
||||
ipaddr=$(plugn trigger network-get-ipaddr "$APP" "$PROC_TYPE" "$cid")
|
||||
|
||||
2
plugins/config/.gitignore
vendored
Normal file
2
plugins/config/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/commands
|
||||
/subcommands/*
|
||||
25
plugins/config/Makefile
Normal file
25
plugins/config/Makefile
Normal file
@@ -0,0 +1,25 @@
|
||||
include ../../common.mk
|
||||
|
||||
GO_ARGS ?= -a
|
||||
|
||||
SUBCOMMANDS = subcommands/export subcommands/get subcommands/set subcommands/unset subcommands/keys subcommands/bundle
|
||||
|
||||
build-in-docker: clean
|
||||
docker run --rm \
|
||||
-v $$PWD/../..:$(GO_REPO_ROOT) \
|
||||
-w $(GO_REPO_ROOT)/plugins/config \
|
||||
$(BUILD_IMAGE) \
|
||||
bash -c "GO_ARGS='$(GO_ARGS)' make build" || exit $$?
|
||||
|
||||
build: commands subcommands
|
||||
|
||||
commands: **/**/commands.go
|
||||
go build $(GO_ARGS) -o commands src/commands/commands.go
|
||||
|
||||
subcommands: $(SUBCOMMANDS)
|
||||
|
||||
subcommands/%: src/subcommands/*/%.go
|
||||
go build $(GO_ARGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -rf commands subcommands
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
[[ " help config:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
case "$1" in
|
||||
help | config:help)
|
||||
help_content_func () {
|
||||
declare desc="return config plugin help content"
|
||||
cat<<help_content
|
||||
config (<app>|--global), Display all global or app-specific config vars
|
||||
config:get (<app>|--global) KEY, Display a global or app-specific config value
|
||||
config:set (<app>|--global) KEY1=VALUE1 [KEY2=VALUE2 ...], Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...], Unset one or more config vars
|
||||
help_content
|
||||
}
|
||||
|
||||
if [[ $1 = "config:help" ]] ; then
|
||||
echo -e 'Usage: dokku config[:COMMAND] (<app>|--global)'
|
||||
echo ''
|
||||
echo 'Manage global or app-specific config vars.'
|
||||
echo ''
|
||||
echo 'Additional commands:'
|
||||
help_content_func | sort | column -c2 -t -s,
|
||||
else
|
||||
help_content_func
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
|
||||
;;
|
||||
|
||||
esac
|
||||
@@ -2,29 +2,85 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
// GetWithDefault returns the value set for a given key, returning defaultValue if none found
|
||||
func GetWithDefault(appName string, key string, defaultValue string) (value string) {
|
||||
value = defaultValue
|
||||
//Get retreives a value from a config. If appName is empty the global config is used.
|
||||
func Get(appName string, key string) (value string, ok bool) {
|
||||
env, err := loadAppOrGlobalEnv(appName)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return env.Get(key)
|
||||
}
|
||||
|
||||
envFile := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"}, "/")
|
||||
lines, err := common.FileToSlice(envFile)
|
||||
//GetWithDefault gets a value from a config. If appName is empty the global config is used. If the appName or key do not exist defaultValue is returned.
|
||||
func GetWithDefault(appName string, key string, defaultValue string) (value string) {
|
||||
value, ok := Get(appName, key)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
//SetMany variables in the environment. If appName is empty the global config is used. If restart is true the app is restarted.
|
||||
func SetMany(appName string, entries map[string]string, restart bool) (err error) {
|
||||
global := appName == ""
|
||||
env, err := loadAppOrGlobalEnv(appName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
prefix := fmt.Sprintf("export %v=", key)
|
||||
for _, line := range lines {
|
||||
if !strings.HasPrefix(line, prefix) {
|
||||
continue
|
||||
}
|
||||
value = strings.TrimPrefix(line, prefix)
|
||||
if strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'") {
|
||||
value = strings.TrimPrefix(strings.TrimSuffix(value, "'"), "'")
|
||||
}
|
||||
keys := make([]string, 0, len(entries))
|
||||
for k, v := range entries {
|
||||
env.Set(k, v)
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
common.LogInfo1("Setting config vars")
|
||||
fmt.Println(prettyPrintEnvEntries(" ", entries))
|
||||
env.Write()
|
||||
args := append([]string{appName, "set"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
triggerRestart(appName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//UnsetMany a value in a config. If appName is empty the global config is used. If restart is true the app is restarted.
|
||||
func UnsetMany(appName string, keys []string, restart bool) (err error) {
|
||||
global := appName == ""
|
||||
env, err := loadAppOrGlobalEnv(appName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var changed = false
|
||||
for _, k := range keys {
|
||||
common.LogInfo1(fmt.Sprintf("Unsetting %s", k))
|
||||
env.Unset(k)
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
env.Write()
|
||||
args := append([]string{appName, "unset"}, keys...)
|
||||
common.PlugnTrigger("post-config-update", args...)
|
||||
}
|
||||
if !global && restart && env.GetBoolDefault("DOKKU_APP_RESTORE", true) {
|
||||
triggerRestart(appName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func triggerRestart(appName string) {
|
||||
common.LogInfo1(fmt.Sprintf("Restarting app %s", appName))
|
||||
common.PlugnTrigger("app-restart", appName)
|
||||
}
|
||||
|
||||
func loadAppOrGlobalEnv(appName string) (env *Env, err error) {
|
||||
if appName == "" || appName == "--global" {
|
||||
return LoadGlobalEnv()
|
||||
}
|
||||
return LoadAppEnv(appName)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,29 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var (
|
||||
testAppName = "test-app-1"
|
||||
testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "")
|
||||
testAppName = "test-app-1"
|
||||
dokkuRoot = common.MustGetEnv("DOKKU_ROOT")
|
||||
testAppDir = strings.Join([]string{dokkuRoot, testAppName}, "/")
|
||||
globalConfigFile = strings.Join([]string{dokkuRoot, "ENV"}, "/")
|
||||
)
|
||||
|
||||
func setupTestApp() (err error) {
|
||||
Expect(os.MkdirAll(testAppDir, 0644)).To(Succeed())
|
||||
Expect(os.MkdirAll(testAppDir, 0766)).To(Succeed())
|
||||
b := []byte("export testKey=TESTING\n")
|
||||
if err = ioutil.WriteFile(strings.Join([]string{testAppDir, "/ENV"}, ""), b, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b = []byte("export testKey=GLOBAL_TESTING\nexport globalKey=GLOBAL_VALUE")
|
||||
if err = ioutil.WriteFile(globalConfigFile, b, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,5 +41,96 @@ func TestConfigGetWithDefault(t *testing.T) {
|
||||
Expect(setupTestApp()).To(Succeed())
|
||||
Expect(GetWithDefault(testAppName, "unknownKey", "UNKNOWN")).To(Equal("UNKNOWN"))
|
||||
Expect(GetWithDefault(testAppName, "testKey", "testKey")).To(Equal("TESTING"))
|
||||
Expect(GetWithDefault(testAppName+"-nonexistent", "testKey", "default")).To(Equal("default"))
|
||||
teardownTestApp()
|
||||
}
|
||||
|
||||
func TestConfigGet(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
Expect(setupTestApp()).To(Succeed())
|
||||
defer teardownTestApp()
|
||||
|
||||
expectValue(testAppName, "testKey", "TESTING")
|
||||
expectValue("", "testKey", "GLOBAL_TESTING")
|
||||
|
||||
expectNoValue(testAppName, "testKey2")
|
||||
expectNoValue("", "testKey2")
|
||||
}
|
||||
|
||||
func TestConfigSetMany(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
Expect(setupTestApp()).To(Succeed())
|
||||
defer teardownTestApp()
|
||||
|
||||
expectValue(testAppName, "testKey", "TESTING")
|
||||
|
||||
vals := map[string]string{"testKey": "updated", "testKey2": "new"}
|
||||
Expect(SetMany(testAppName, vals, false)).To(Succeed())
|
||||
expectValue(testAppName, "testKey", "updated")
|
||||
expectValue(testAppName, "testKey2", "new")
|
||||
|
||||
vals = map[string]string{"testKey": "updated_global", "testKey2": "new_global"}
|
||||
Expect(SetMany("", vals, false)).To(Succeed())
|
||||
expectValue("", "testKey", "updated_global")
|
||||
expectValue("", "testKey2", "new_global")
|
||||
expectValue("", "globalKey", "GLOBAL_VALUE")
|
||||
expectValue(testAppName, "testKey", "updated")
|
||||
expectValue(testAppName, "testKey2", "new")
|
||||
|
||||
Expect(SetMany(testAppName+"does_not_exist", vals, false)).ToNot(Succeed())
|
||||
}
|
||||
|
||||
func TestConfigUnsetMany(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
Expect(setupTestApp()).To(Succeed())
|
||||
defer teardownTestApp()
|
||||
|
||||
expectValue(testAppName, "testKey", "TESTING")
|
||||
expectValue("", "testKey", "GLOBAL_TESTING")
|
||||
|
||||
keys := []string{"testKey", "noKey"}
|
||||
Expect(UnsetMany(testAppName, keys, false)).To(Succeed())
|
||||
expectNoValue(testAppName, "testKey")
|
||||
expectValue("", "testKey", "GLOBAL_TESTING")
|
||||
|
||||
Expect(UnsetMany(testAppName, keys, false)).To(Succeed())
|
||||
expectNoValue(testAppName, "testKey")
|
||||
expectNoValue(testAppName, "globalKey")
|
||||
|
||||
Expect(UnsetMany(testAppName+"does-not-exist", keys, false)).ToNot(Succeed())
|
||||
}
|
||||
|
||||
func TestEnvironmentLoading(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
Expect(setupTestApp()).To(Succeed())
|
||||
defer teardownTestApp()
|
||||
|
||||
env, err := LoadMergedAppEnv(testAppName)
|
||||
Expect(err).To(Succeed())
|
||||
v, _ := env.Get("testKey")
|
||||
Expect(v).To(Equal("TESTING"))
|
||||
v, _ = env.Get("globalKey")
|
||||
Expect(v).To(Equal("GLOBAL_VALUE"))
|
||||
Expect(env.Write()).ToNot(Succeed())
|
||||
|
||||
env, err = LoadAppEnv(testAppName)
|
||||
env.Set("testKey", "TESTING-updated")
|
||||
env.Set("testKey2", "TESTING-'\n'-updated")
|
||||
env.Write()
|
||||
|
||||
expectValue(testAppName, "testKey", "TESTING-updated")
|
||||
expectValue(testAppName, "testKey2", "TESTING-'\n'-updated")
|
||||
expectValue("", "testKey", "GLOBAL_TESTING")
|
||||
Expect(err).To(Succeed())
|
||||
}
|
||||
|
||||
func expectValue(appName string, key string, expected string) {
|
||||
v, ok := Get(appName, key)
|
||||
Expect(ok).To(Equal(true))
|
||||
Expect(v).To(Equal(expected))
|
||||
}
|
||||
|
||||
func expectNoValue(appName string, key string) {
|
||||
_, ok := Get(appName, key)
|
||||
Expect(ok).To(Equal(false))
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_docker_args() {
|
||||
declare desc="config docker-args plugin trigger"
|
||||
local trigger="$0 config_docker_args"
|
||||
local STDIN=$(cat); local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
|
||||
local DOCKERFILE_ENV_FILE="$DOKKU_ROOT/$APP/DOCKERFILE_ENV_FILE"
|
||||
declare APP="$1" IMAGE_TAG="$2"
|
||||
local ENV_ARGS IMAGE STDIN trigger
|
||||
|
||||
IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
|
||||
STDIN=$(cat);
|
||||
trigger="$0 config_docker_args"
|
||||
verify_app_name "$APP"
|
||||
|
||||
if ! is_image_herokuish_based "$IMAGE"; then
|
||||
> "$DOCKERFILE_ENV_FILE"
|
||||
config_export global | sed -e "s:^export ::g" -e "s:=':=:g" -e "s:'$::g" > "$DOCKERFILE_ENV_FILE"
|
||||
config_export app "$APP" | sed -e "s:^export ::g" -e "s:=':=:g" -e "s:'$::g" >> "$DOCKERFILE_ENV_FILE"
|
||||
|
||||
echo -n "$STDIN --env-file=$DOCKERFILE_ENV_FILE"
|
||||
ENV_ARGS="$(config_export app "$APP" --format docker-args --merged)"
|
||||
echo -n "$STDIN $ENV_ARGS"
|
||||
else
|
||||
echo -n "$STDIN"
|
||||
fi
|
||||
|
||||
274
plugins/config/environment.go
Normal file
274
plugins/config/environment.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"archive/tar"
|
||||
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
//ExportFormat types of possible exports
|
||||
type ExportFormat int
|
||||
|
||||
const (
|
||||
//ExportFormatExports format: Sourceable exports
|
||||
ExportFormatExports ExportFormat = iota
|
||||
//ExportFormatEnvfile format: dotenv file
|
||||
ExportFormatEnvfile
|
||||
//ExportFormatDockerArgs format: --env args for docker
|
||||
ExportFormatDockerArgs
|
||||
//ExportFormatShell format: env arguments for shell
|
||||
ExportFormatShell
|
||||
//ExportFormatPretty format: pretty-printed in columns
|
||||
ExportFormatPretty
|
||||
)
|
||||
|
||||
//Env is a representation for global or app environment
|
||||
type Env struct {
|
||||
name string
|
||||
filename string
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
//newEnvFromString creates an env from the given ENVFILE contents representation
|
||||
func newEnvFromString(rep string) (env *Env, err error) {
|
||||
envMap, err := godotenv.Unmarshal(rep)
|
||||
env = &Env{
|
||||
name: "<unknown>",
|
||||
filename: "",
|
||||
env: envMap,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//LoadAppEnv loads an environment for the given app
|
||||
func LoadAppEnv(appName string) (env *Env, err error) {
|
||||
appfile, err := getAppFile(appName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return loadFromFile(appName, appfile)
|
||||
}
|
||||
|
||||
//LoadMergedAppEnv loads an app environment merged with the global environment
|
||||
func LoadMergedAppEnv(appName string) (env *Env, err error) {
|
||||
env, err = LoadAppEnv(appName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
global, err := LoadGlobalEnv()
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
global.Merge(env)
|
||||
global.filename = ""
|
||||
global.name = env.name
|
||||
return global, err
|
||||
}
|
||||
|
||||
//LoadGlobalEnv loads the global environment
|
||||
func LoadGlobalEnv() (*Env, error) {
|
||||
return loadFromFile("global", getGlobalFile())
|
||||
}
|
||||
|
||||
//Get an environment variable
|
||||
func (e *Env) Get(key string) (value string, ok bool) {
|
||||
value, ok = e.env[key]
|
||||
return
|
||||
}
|
||||
|
||||
//GetDefault an environment variable or a default if it doesnt exist
|
||||
func (e *Env) GetDefault(key string, defaultValue string) string {
|
||||
v, ok := e.env[key]
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
//GetBoolDefault gets the bool value of the given key with the given default
|
||||
//right now that is evaluated as `value != "0"`
|
||||
func (e *Env) GetBoolDefault(key string, defaultValue bool) bool {
|
||||
v, ok := e.Get(key)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
return v != "0"
|
||||
}
|
||||
|
||||
//Set an environment variable
|
||||
func (e *Env) Set(key string, value string) {
|
||||
e.env[key] = value
|
||||
}
|
||||
|
||||
//Unset an environment variable
|
||||
func (e *Env) Unset(key string) {
|
||||
delete(e.env, key)
|
||||
}
|
||||
|
||||
//Keys gets the keys in this environment
|
||||
func (e *Env) Keys() (keys []string) {
|
||||
keys = make([]string, 0, len(e.env))
|
||||
for k := range e.env {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return
|
||||
}
|
||||
|
||||
//Len return the number of items in this environment
|
||||
func (e *Env) Len() int {
|
||||
return len(e.env)
|
||||
}
|
||||
|
||||
//Map return the Env as a map
|
||||
func (e *Env) Map() map[string]string {
|
||||
return e.env
|
||||
}
|
||||
|
||||
func (e *Env) String() string {
|
||||
return e.EnvfileString()
|
||||
}
|
||||
|
||||
//Merge merges the given environment on top of the reciever
|
||||
func (e *Env) Merge(other *Env) {
|
||||
for _, k := range other.Keys() {
|
||||
e.Set(k, other.GetDefault(k, ""))
|
||||
}
|
||||
}
|
||||
|
||||
//Write an Env back to the file it was read from as an exportfile
|
||||
func (e *Env) Write() error {
|
||||
if e.filename == "" {
|
||||
return errors.New("this Env was created unbound to a file")
|
||||
}
|
||||
return godotenv.Write(e.Map(), e.filename)
|
||||
}
|
||||
|
||||
//Export the Env in the given format
|
||||
func (e *Env) Export(format ExportFormat) string {
|
||||
switch format {
|
||||
case ExportFormatExports:
|
||||
return e.ExportfileString()
|
||||
case ExportFormatEnvfile:
|
||||
return e.EnvfileString()
|
||||
case ExportFormatDockerArgs:
|
||||
return e.DockerArgsString()
|
||||
case ExportFormatShell:
|
||||
return e.ShellString()
|
||||
case ExportFormatPretty:
|
||||
return prettyPrintEnvEntries("", e.Map())
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Unknown export format: %v", format))
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//EnvfileString returns the contents of this Env in dotenv format
|
||||
func (e *Env) EnvfileString() string {
|
||||
rep, _ := godotenv.Marshal(e.Map())
|
||||
return rep
|
||||
}
|
||||
|
||||
//ExportfileString returns the contents of this Env as bash exports
|
||||
func (e *Env) ExportfileString() string {
|
||||
return e.stringWithPrefixAndSeparator("export ", "\n")
|
||||
}
|
||||
|
||||
//DockerArgsString gets the contents of this Env in the form -env=KEY=VALUE --env...
|
||||
func (e *Env) DockerArgsString() string {
|
||||
return e.stringWithPrefixAndSeparator("--env=", " ")
|
||||
}
|
||||
|
||||
//ShellString gets the contents of this Env in the form "KEY='value' KEY2='value'"
|
||||
// for passing the environment in the shell
|
||||
func (e *Env) ShellString() string {
|
||||
return e.stringWithPrefixAndSeparator("", " ")
|
||||
}
|
||||
|
||||
//ExportBundle writes a tarfile of the environmnet to the given io.Writer.
|
||||
// for every environment variable there is a file with the variable's key
|
||||
// with its content set to the variable's value
|
||||
func (e *Env) ExportBundle(dest io.Writer) error {
|
||||
tarfile := tar.NewWriter(dest)
|
||||
defer tarfile.Close()
|
||||
|
||||
for _, k := range e.Keys() {
|
||||
val, _ := e.Get(k)
|
||||
valbin := []byte(val)
|
||||
|
||||
header := &tar.Header{
|
||||
Name: k,
|
||||
Mode: 0600,
|
||||
Size: int64(len(valbin)),
|
||||
}
|
||||
tarfile.WriteHeader(header)
|
||||
tarfile.Write(valbin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//stringWithPrefixAndSeparator makes a string of the environment
|
||||
// with the given prefix and separator for each entry
|
||||
func (e *Env) stringWithPrefixAndSeparator(prefix string, separator string) string {
|
||||
keys := e.Keys()
|
||||
entries := make([]string, len(keys))
|
||||
for i, k := range keys {
|
||||
v := singleQuoteEscape(e.env[k])
|
||||
entries[i] = fmt.Sprintf("%s%s='%s'", prefix, k, v)
|
||||
}
|
||||
return strings.Join(entries, separator)
|
||||
}
|
||||
|
||||
//singleQuoteEscape escapes the value as if it were shell-quoted in single quotes
|
||||
func singleQuoteEscape(value string) string { // so that 'esc'apped' -> 'esc'\''aped'
|
||||
return strings.Replace(value, "'", "'\\''", -1)
|
||||
}
|
||||
|
||||
//prettyPrintEnvEntries in columns
|
||||
func prettyPrintEnvEntries(prefix string, entries map[string]string) (representation string) {
|
||||
colConfig := columnize.DefaultConfig()
|
||||
colConfig.Prefix = prefix
|
||||
colConfig.Delim = "\x00"
|
||||
lines := make([]string, 0, len(entries))
|
||||
for k, v := range entries {
|
||||
lines = append(lines, fmt.Sprintf("%s:\x00%s", k, v))
|
||||
}
|
||||
return columnize.Format(lines, colConfig)
|
||||
}
|
||||
|
||||
func loadFromFile(name string, filename string) (env *Env, err error) {
|
||||
envMap := make(map[string]string)
|
||||
if _, err := os.Stat(filename); err == nil {
|
||||
envMap, err = godotenv.Read(filename)
|
||||
}
|
||||
|
||||
env = &Env{
|
||||
name: name,
|
||||
filename: filename,
|
||||
env: envMap,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getAppFile(appName string) (string, error) {
|
||||
err := common.VerifyAppName(appName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"), nil
|
||||
}
|
||||
|
||||
func getGlobalFile() string {
|
||||
return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), "ENV")
|
||||
}
|
||||
102
plugins/config/environment_test.go
Normal file
102
plugins/config/environment_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestExportfileRoundtrip(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
env, err := newEnvFromString("HI='ho'\nFoo='Bar'\n\nBaz='BOFF'")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("Baz", "BOFF", "Foo", "Bar", "HI", "ho")))
|
||||
Expect(env.String()).To(Equal("Baz=\"BOFF\"\nFoo=\"Bar\"\nHI=\"ho\""))
|
||||
|
||||
env, err = newEnvFromString(`export HI="h\no"`)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "h\no")))
|
||||
Expect(env.String()).To(Equal(`HI="h\no"`))
|
||||
|
||||
env, err = newEnvFromString("HI='ho'\nFOO=\"'\\nBAR=''\"")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("HI", "ho", "FOO", "'\nBAR=''")))
|
||||
Expect(env.String()).To(Equal("FOO=\"'\\nBAR=''\"\nHI=\"ho\""))
|
||||
|
||||
env, err = newEnvFromString("FOO='bar' ")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(env.Map()).To(Equal(pairs("FOO", "bar")))
|
||||
Expect(env.String()).To(Equal(`FOO="bar"`))
|
||||
|
||||
}
|
||||
|
||||
func TestExportfileErrors(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
|
||||
_, err := newEnvFromString("F\nOO='bar'") //keys cannot have embedded newlines
|
||||
Expect(err).To(HaveOccurred())
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
e, _ := newEnvFromString("FOO='bar'")
|
||||
e2, _ := newEnvFromString("BAR='baz'")
|
||||
e.Merge(e2)
|
||||
Expect(e.Map()).To(Equal(pairs("BAR", "baz", "FOO", "bar")))
|
||||
|
||||
e3, _ := newEnvFromString("FOO='ba \\nz'")
|
||||
e.Merge(e3)
|
||||
Expect(e.Map()).To(Equal(pairs("BAR", "baz", "FOO", "ba \nz")))
|
||||
}
|
||||
|
||||
func TestExport(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
e, _ := newEnvFromString("BAR='BAZ'\nFOO='b'ar '\nBAZ='a\\nb'")
|
||||
Expect(e.Export(ExportFormatEnvfile)).To(Equal("BAR=\"BAZ\"\nBAZ=\"a\\nb\"\nFOO=\"b'ar \""))
|
||||
Expect(e.Export(ExportFormatDockerArgs)).To(Equal("--env=BAR='BAZ' --env=BAZ='a\nb' --env=FOO='b'\\''ar '"))
|
||||
Expect(e.Export(ExportFormatShell)).To(Equal("BAR='BAZ' BAZ='a\nb' FOO='b'\\''ar '"))
|
||||
Expect(e.Export(ExportFormatExports)).To(Equal("export BAR='BAZ'\nexport BAZ='a\nb'\nexport FOO='b'\\''ar '"))
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
RegisterTestingT(t)
|
||||
e, err := newEnvFromString("BAR='BAZ'\nFOO='ba\\nr '\nGO='1'\nNOGO='0'")
|
||||
Expect(err).To(Succeed())
|
||||
|
||||
v, ok := e.Get("GO")
|
||||
Expect(ok).To(Equal(true))
|
||||
Expect(v).To(Equal("1"))
|
||||
|
||||
v = e.GetDefault("GO", "default")
|
||||
Expect(v).To(Equal("1"))
|
||||
|
||||
v = e.GetDefault("dne", "default")
|
||||
Expect(v).To(Equal("default"))
|
||||
|
||||
b := e.GetBoolDefault("dne", true)
|
||||
Expect(b).To(Equal(true))
|
||||
|
||||
b = e.GetBoolDefault("dne", false)
|
||||
Expect(b).To(Equal(false))
|
||||
|
||||
b = e.GetBoolDefault("GO", false)
|
||||
Expect(b).To(Equal(true))
|
||||
|
||||
b = e.GetBoolDefault("NOGO", true)
|
||||
Expect(b).To(Equal(false))
|
||||
|
||||
b = e.GetBoolDefault("BAR", false)
|
||||
Expect(b).To(Equal(true)) //anything but "0" is true
|
||||
}
|
||||
|
||||
func pairs(vars ...string) map[string]string {
|
||||
res := map[string]string{}
|
||||
var i = 0
|
||||
for i < len(vars)-1 {
|
||||
key := vars[i]
|
||||
value := vars[i+1]
|
||||
res[key] = value
|
||||
i += 2
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -2,255 +2,49 @@
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
|
||||
|
||||
config_sub(){
|
||||
declare desc="executes a config subcommand"
|
||||
local name="$1"
|
||||
shift
|
||||
"$PLUGIN_AVAILABLE_PATH/config/subcommands/$name" "config:$name" "$@"
|
||||
}
|
||||
|
||||
config_export() {
|
||||
declare desc="returns export command for config variable of specified type (app/global)"
|
||||
local CONFIG_TYPE="$1"
|
||||
local APP="$2"
|
||||
local ENV_FILE="$DOKKU_ROOT/$APP/ENV"
|
||||
|
||||
[[ $CONFIG_TYPE == "global" ]] && local ENV_FILE="$DOKKU_ROOT/ENV"
|
||||
[[ ! -f $ENV_FILE ]] && return 0
|
||||
[[ ! -s $ENV_FILE ]] && return 0
|
||||
|
||||
local VARS=$(grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | cut -d" " -f2-)
|
||||
echo "$VARS" | awk '{print "export " $0}'
|
||||
shift
|
||||
local APP="$1"
|
||||
if [[ $CONFIG_TYPE == "global" ]]; then
|
||||
APP="--global"
|
||||
else
|
||||
shift
|
||||
fi
|
||||
config_sub export "$@" "$APP"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_sanitized_config_args() {
|
||||
local desc="return sanitized arguments for config plugin"
|
||||
local SANITIZED_ARGS=("$@")
|
||||
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--global/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--no-restart/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--export/}")
|
||||
SANITIZED_ARGS=("${SANITIZED_ARGS[@]//--shell/}")
|
||||
|
||||
echo "${SANITIZED_ARGS[@]}"
|
||||
}
|
||||
|
||||
config_parse_args() {
|
||||
declare desc="parse config plugin args"
|
||||
unset APP ENV_FILE DOKKU_CONFIG_TYPE DOKKU_CONFIG_RESTART DOKKU_CONFIG_EXPORT
|
||||
|
||||
for var; do
|
||||
if [[ "$var" == "--global" ]]; then
|
||||
ENV_FILE="$DOKKU_ROOT/ENV"
|
||||
DOKKU_CONFIG_TYPE="global"
|
||||
DOKKU_CONFIG_RESTART=false
|
||||
fi
|
||||
if [[ "$var" == "--no-restart" ]]; then
|
||||
DOKKU_CONFIG_RESTART=false
|
||||
fi
|
||||
if [[ "$var" == "--export" ]]; then
|
||||
DOKKU_CONFIG_EXPORT=true
|
||||
fi
|
||||
if [[ "$var" == "--shell" ]]; then
|
||||
DOKKU_CONFIG_SHELL=true
|
||||
fi
|
||||
done
|
||||
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
DOKKU_CONFIG_TYPE=${DOKKU_CONFIG_TYPE:="app"}
|
||||
DOKKU_CONFIG_RESTART=${DOKKU_CONFIG_RESTART:=true}
|
||||
|
||||
if [[ "$DOKKU_CONFIG_TYPE" == "app" ]]; then
|
||||
if [[ -z $2 ]]; then
|
||||
dokku_log_fail "Please specify an app to run the command on"
|
||||
else
|
||||
verify_app_name "$2"
|
||||
APP=${APP:="$2"}
|
||||
ENV_FILE=${ENV_FILE:="$DOKKU_ROOT/$APP/ENV"}
|
||||
fi
|
||||
fi
|
||||
export APP ENV_FILE DOKKU_CONFIG_TYPE DOKKU_CONFIG_RESTART DOKKU_CONFIG_EXPORT
|
||||
}
|
||||
|
||||
config_create () {
|
||||
declare desc="create config env file"
|
||||
local ENV_FILE=$1
|
||||
[[ -f $ENV_FILE ]] || touch "$ENV_FILE"
|
||||
}
|
||||
|
||||
config_styled_hash () {
|
||||
declare desc="internal config hash"
|
||||
local vars="$1"
|
||||
local prefix="$2"
|
||||
local longest=""
|
||||
|
||||
while read -r word; do
|
||||
local KEY=$(echo "$word" | cut -d"=" -f1)
|
||||
if [[ ${#KEY} -gt ${#longest} ]]; then
|
||||
local longest=$KEY
|
||||
fi
|
||||
done <<< "$vars"
|
||||
|
||||
while read -r word; do
|
||||
local KEY=$(echo "$word" | cut -d"=" -f1)
|
||||
local VALUE=$(echo "$word" | cut -d"=" -f2- | sed -e "s/^'//" -e "s/'$//" -e "s/\$$//g")
|
||||
|
||||
local num_zeros=$((${#longest} - ${#KEY}))
|
||||
local zeros=" "
|
||||
while [[ $num_zeros -gt 0 ]]; do
|
||||
local zeros="$zeros "
|
||||
local num_zeros=$((num_zeros - 1))
|
||||
done
|
||||
echo "$prefix$KEY:$zeros$VALUE"
|
||||
done <<< "$vars"
|
||||
}
|
||||
|
||||
config_write() {
|
||||
declare desc="writes config vars out to appropriate file path"
|
||||
local ENV_TEMP="$1"
|
||||
local ENV_FILE_TEMP="${ENV_FILE}.tmp"
|
||||
|
||||
echo "$ENV_TEMP" | sed '/^$/d' | sort > "$ENV_FILE_TEMP"
|
||||
if ! cmp -s "$ENV_FILE" "$ENV_FILE_TEMP"; then
|
||||
cp -f "$ENV_FILE_TEMP" "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
fi
|
||||
rm -f "$ENV_FILE_TEMP"
|
||||
}
|
||||
|
||||
config_all() {
|
||||
declare desc="print or export config vars"
|
||||
[[ "$1" == "config" ]] || set -- "config" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
[[ $DOKKU_CONFIG_EXPORT ]] && config_export "$DOKKU_CONFIG_TYPE" "$APP" && return 0
|
||||
|
||||
[[ $APP ]] && local DOKKU_CONFIG_TYPE=$APP
|
||||
[[ ! -s $ENV_FILE ]] && dokku_log_fail "no config vars for $DOKKU_CONFIG_TYPE"
|
||||
|
||||
local VARS=$(grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | cut -d" " -f2-)
|
||||
|
||||
[[ $DOKKU_CONFIG_SHELL ]] && echo "$VARS" && return 0
|
||||
|
||||
dokku_log_info2_quiet "$DOKKU_CONFIG_TYPE config vars"
|
||||
config_styled_hash "$VARS"
|
||||
config_keys() {
|
||||
declare desc="returns keys for app or global"
|
||||
config_keys "$@"
|
||||
return 0
|
||||
}
|
||||
|
||||
config_get() {
|
||||
declare desc="get value of given config var"
|
||||
[[ "$1" == "config:get" ]] || set -- "config:get" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
if [[ -z $2 ]]; then
|
||||
echo "Usage: dokku config:get APP KEY"
|
||||
echo "Must specify KEY."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
if [[ ! -s $ENV_FILE ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local KEY="${*: -1}"
|
||||
|
||||
grep -Eo "export ([a-zA-Z_][a-zA-Z0-9_]*=.*)" "$ENV_FILE" | grep "^export $KEY=" | cut -d"=" -f2- | sed -e "s/^'//" -e "s/'$//"
|
||||
config_sub get "$@"
|
||||
}
|
||||
|
||||
config_set() {
|
||||
declare desc="set value of given config var"
|
||||
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
|
||||
|
||||
[[ "$1" == "config:set" ]] || set -- "config:set" "$@"
|
||||
config_parse_args "$@"
|
||||
|
||||
if [[ -z "${*:3}" ]]; then
|
||||
echo "Usage: dokku config:set APP KEY1=VALUE1 [KEY2=VALUE2 ...]"
|
||||
echo "Whitespaces need to be escaped, i.e. KEY=\"VAL\ WITH\ SPACES\""
|
||||
echo "Must specify KEY and VALUE to set."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
local ENV_ADD=""
|
||||
local ENV_TEMP=$(cat "${ENV_FILE}")
|
||||
local RESTART_APP=false
|
||||
shift 2
|
||||
|
||||
for var; do
|
||||
if [[ $var == "--global" ]] || [[ $var == "--no-restart" ]] || ([[ $APP ]] && [[ $var == "$APP" ]]); then
|
||||
shift 1
|
||||
elif [[ $var != *"="* ]]; then
|
||||
echo "Usage: dokku config:set APP KEY1=VALUE1 [KEY2=VALUE2 ...]"
|
||||
echo "Whitespaces need to be escaped, i.e. KEY=\"VAL\ WITH\ SPACES\""
|
||||
echo "Must specify KEY and VALUE to set."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for var; do
|
||||
local KEY=$(echo ${var} | cut -d"=" -f1)
|
||||
local VALUE=$(echo ${var} | cut -d"=" -f2-)
|
||||
|
||||
if [[ $KEY =~ [a-zA-Z_][a-zA-Z0-9_]* ]]; then
|
||||
local RESTART_APP=true
|
||||
local ENV_TEMP=$(echo "${ENV_TEMP}" | sed "/^export $KEY=/ d")
|
||||
local ENV_TEMP="${ENV_TEMP}
|
||||
export $KEY='$VALUE'"
|
||||
local ENV_ADD=$(echo -e "${ENV_ADD}" | sed "/^$KEY=/ d")
|
||||
local ENV_ADD="${ENV_ADD}$
|
||||
${var}"
|
||||
fi
|
||||
done
|
||||
local ENV_ADD=$(echo "$ENV_ADD" | tail -n +2) #remove first empty line
|
||||
|
||||
if [[ $RESTART_APP ]]; then
|
||||
dokku_log_info1 "Setting config vars"
|
||||
config_styled_hash "$ENV_ADD" " "
|
||||
|
||||
config_write "$ENV_TEMP"
|
||||
plugn trigger post-config-update "$APP" "set" "$@"
|
||||
fi
|
||||
|
||||
[[ $APP ]] && local DOKKU_APP_RESTORE=$(config_get "$APP" DOKKU_APP_RESTORE || true)
|
||||
if [[ "$DOKKU_CONFIG_RESTART" == "true" ]] && [[ "$DOKKU_APP_RESTORE" != 0 ]]; then
|
||||
dokku_log_info1 "Restarting app $APP"
|
||||
ps_restart "$APP"
|
||||
fi
|
||||
config_sub set "$@"
|
||||
}
|
||||
|
||||
config_unset() {
|
||||
declare desc="unset given config var"
|
||||
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
|
||||
|
||||
[[ "$1" == "config:unset" ]] || set -- "config:unset" "$@"
|
||||
config_parse_args "$@"
|
||||
local SANITIZED_ARGS=($(get_sanitized_config_args "$@"))
|
||||
set -- "${SANITIZED_ARGS[@]}"
|
||||
|
||||
if [[ -z $2 ]]; then
|
||||
echo "Usage: dokku config:unset APP KEY1 [KEY2 ...]"
|
||||
echo "Must specify KEY to unset."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
config_create "$ENV_FILE"
|
||||
local ENV_TEMP=$(cat "${ENV_FILE}")
|
||||
local VARS="${*:2}"
|
||||
|
||||
for var in $VARS; do
|
||||
dokku_log_info1 "Unsetting $var"
|
||||
local ENV_TEMP=$(echo "${ENV_TEMP}" | sed "/^export $var=/ d")
|
||||
|
||||
config_write "$ENV_TEMP"
|
||||
done
|
||||
|
||||
plugn trigger post-config-update "$APP" "unset" "$@"
|
||||
|
||||
[[ $APP ]] && local DOKKU_APP_RESTORE=$(config_get "$APP" DOKKU_APP_RESTORE || true)
|
||||
if [[ "$DOKKU_CONFIG_RESTART" == "true" ]] && [[ "$DOKKU_APP_RESTORE" != 0 ]]; then
|
||||
dokku_log_info1 "Restarting app $APP"
|
||||
ps_restart "$APP"
|
||||
fi
|
||||
declare desc="set value of given config var"
|
||||
config_sub unset "$@"
|
||||
}
|
||||
|
||||
config_bundle() {
|
||||
declare desc="export tar bundle of config"
|
||||
config_sub bundle "$@"
|
||||
}
|
||||
|
||||
8
plugins/config/glide.lock
generated
Normal file
8
plugins/config/glide.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
hash: dd48defef474bdf5c6e47281a256ca941d71fe1e637d11443fb49acf3e4b5326
|
||||
updated: 2017-09-23T16:25:15.559883959-04:00
|
||||
imports:
|
||||
- name: github.com/joho/godotenv
|
||||
version: a79fa1e548e2c689c241d10173efd51e5d689d5b
|
||||
- name: github.com/ryanuber/columnize
|
||||
version: abc90934186a77966e2beeac62ed966aac0561d5
|
||||
testImports: []
|
||||
8
plugins/config/glide.yaml
Normal file
8
plugins/config/glide.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
package: github.com/dokku/dokku/plugins/config
|
||||
ignore:
|
||||
- github.com/dokku/dokku/plugins/common
|
||||
- github.com/onsi/gomega
|
||||
import:
|
||||
- package: github.com/ryanuber/columnize
|
||||
- package: github.com/joho/godotenv
|
||||
version: ^1.2.0
|
||||
67
plugins/config/src/commands/commands.go
Normal file
67
plugins/config/src/commands/commands.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
columnize "github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
const (
|
||||
helpHeader = `Usage: dokku config [<app>|--global]
|
||||
|
||||
Display all global or app-specific config vars
|
||||
|
||||
Additional commands:`
|
||||
|
||||
helpContent = `
|
||||
config (<app>|--global), Pretty-print an app or global environment
|
||||
config:get (<app>|--global) KEY, Display a global or app-specific config value
|
||||
config:set (<app>|--global) [--encoded] [--no-restart] KEY1=VALUE1 [KEY2=VALUE2 ...], Set one or more config vars
|
||||
config:unset (<app>|--global) KEY1 [KEY2 ...], Unset one or more config vars
|
||||
config:export (<app>|--global) [--envfile], Export a global or app environment
|
||||
config:keys (<app> | --global) [--merged], Show keys set in environment
|
||||
config:bundle (<app> | --global) [--merged], Bundle environment into tarfile
|
||||
`
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
switch cmd {
|
||||
case "config", "config:show":
|
||||
args := flag.NewFlagSet("config:show", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
shell := args.Bool("shell", false, "--shell: in a single-line for usage in command-line utilities [deprecated]")
|
||||
export := args.Bool("export", false, "--export: print the env as eval-compatible exports [deprecated]")
|
||||
merged := args.Bool("merged", false, "--merged: display the app's envionment merged with the global environment")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandShow(args.Args(), *global, *shell, *export, *merged)
|
||||
case "config:help":
|
||||
case "help":
|
||||
usage()
|
||||
default:
|
||||
dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT"))
|
||||
if err != nil {
|
||||
fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable")
|
||||
dokkuNotImplementExitCode = 10
|
||||
}
|
||||
os.Exit(dokkuNotImplementExitCode)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
config := columnize.DefaultConfig()
|
||||
config.Delim = ","
|
||||
config.Prefix = "\t"
|
||||
config.Empty = ""
|
||||
content := strings.Split(helpContent, "\n")[1:]
|
||||
fmt.Println(helpHeader)
|
||||
fmt.Println(columnize.Format(content, config))
|
||||
}
|
||||
16
plugins/config/src/subcommands/bundle/bundle.go
Normal file
16
plugins/config/src/subcommands/bundle/bundle.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandBundle(args.Args(), *global, *merged)
|
||||
}
|
||||
20
plugins/config/src/subcommands/export/export.go
Normal file
20
plugins/config/src/subcommands/export/export.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
// print the environment to stdout
|
||||
func main() {
|
||||
const defaultPrefix = "export "
|
||||
const defaultSeparator = "\n"
|
||||
args := flag.NewFlagSet("config:export", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
format := args.String("format", "exports", "--format: [ exports | envfile | docker-args | shell ] which format to export as)")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandExport(args.Args(), *global, *merged, *format)
|
||||
}
|
||||
17
plugins/config/src/subcommands/get/get.go
Normal file
17
plugins/config/src/subcommands/get/get.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
// get the given entries from the specified environment
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
quoted := args.Bool("quoted", false, "--quoted: get the value quoted")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandGet(args.Args(), *global, *quoted)
|
||||
}
|
||||
16
plugins/config/src/subcommands/keys/keys.go
Normal file
16
plugins/config/src/subcommands/keys/keys.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:get", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
merged := args.Bool("merged", false, "--merged: merge app environment and global environment")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandKeys(args.Args(), *global, *merged)
|
||||
}
|
||||
18
plugins/config/src/subcommands/set/set.go
Normal file
18
plugins/config/src/subcommands/set/set.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
// set the given entries from the specified environment
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:set", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
encoded := args.Bool("encoded", false, "--encoded: interpret VALUEs as base64")
|
||||
noRestart := args.Bool("no-restart", false, "--no-restart: no restart")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandSet(args.Args(), *global, *noRestart, *encoded)
|
||||
}
|
||||
17
plugins/config/src/subcommands/unset/unset.go
Normal file
17
plugins/config/src/subcommands/unset/unset.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"github.com/dokku/dokku/plugins/config"
|
||||
)
|
||||
|
||||
//unset the given entries from the given environment
|
||||
func main() {
|
||||
args := flag.NewFlagSet("config:unset", flag.ExitOnError)
|
||||
global := args.Bool("global", false, "--global: use the global environment")
|
||||
noRestart := args.Bool("no-restart", false, "--no-restart: no restart")
|
||||
args.Parse(os.Args[2:])
|
||||
config.CommandUnset(args.Args(), *global, *noRestart)
|
||||
}
|
||||
167
plugins/config/subcommands.go
Normal file
167
plugins/config/subcommands.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/dokku/dokku/plugins/common"
|
||||
)
|
||||
|
||||
//CommandShow implementes config:show
|
||||
func CommandShow(args []string, global bool, shell bool, export bool, merged bool) {
|
||||
appName, _ := getCommonArgs(global, args)
|
||||
env := getEnvironment(appName, merged)
|
||||
if shell && export {
|
||||
common.LogFail("Only one of --shell and --export can be given")
|
||||
}
|
||||
if shell {
|
||||
fmt.Print(env.Export(ExportFormatShell))
|
||||
} else if export {
|
||||
fmt.Println(env.Export(ExportFormatExports))
|
||||
} else {
|
||||
contextName := "global"
|
||||
if appName != "" {
|
||||
contextName = appName
|
||||
}
|
||||
common.LogInfo2(contextName + " env vars")
|
||||
fmt.Println(env.Export(ExportFormatPretty))
|
||||
}
|
||||
}
|
||||
|
||||
//CommandGet implements config:get
|
||||
func CommandGet(args []string, global bool, quoted bool) {
|
||||
appName, keys := getCommonArgs(global, args)
|
||||
if len(keys) > 1 {
|
||||
common.LogFail(fmt.Sprintf("Unexpected argument(s): %v", keys[1:]))
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
common.LogFail("Expected: key")
|
||||
}
|
||||
if value, ok := Get(appName, keys[0]); !ok {
|
||||
os.Exit(1)
|
||||
} else {
|
||||
if quoted {
|
||||
fmt.Printf("'%s'", singleQuoteEscape(value))
|
||||
} else {
|
||||
fmt.Printf("%s", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//CommandUnset implements config:unset
|
||||
func CommandUnset(args []string, global bool, noRestart bool) {
|
||||
appName, keys := getCommonArgs(global, args)
|
||||
err := UnsetMany(appName, keys, !noRestart)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
//CommandSet implements config:set
|
||||
func CommandSet(args []string, global bool, noRestart bool, encoded bool) {
|
||||
appName, pairs := getCommonArgs(global, args)
|
||||
updated := make(map[string]string)
|
||||
for _, e := range pairs {
|
||||
parts := strings.SplitN(e, "=", 2)
|
||||
if len(parts) == 1 {
|
||||
common.LogFail("Invalid env pair: " + e)
|
||||
}
|
||||
key, value := parts[0], parts[1]
|
||||
if encoded {
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
common.LogFail(fmt.Sprintf("%s for key '%s'", err.Error(), key))
|
||||
}
|
||||
value = string(decoded)
|
||||
}
|
||||
updated[key] = value
|
||||
}
|
||||
err := SetMany(appName, updated, !noRestart)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
//CommandKeys implements config:keys
|
||||
func CommandKeys(args []string, global bool, merged bool) {
|
||||
appName, trailingArgs := getCommonArgs(global, args)
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
env := getEnvironment(appName, merged)
|
||||
for _, k := range env.Keys() {
|
||||
fmt.Println(k)
|
||||
}
|
||||
}
|
||||
|
||||
//CommandExport implements config:export
|
||||
func CommandExport(args []string, global bool, merged bool, format string) {
|
||||
appName, trailingArgs := getCommonArgs(global, args)
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
env := getEnvironment(appName, merged)
|
||||
exportType := ExportFormatExports
|
||||
suffix := "\n"
|
||||
switch format {
|
||||
case "exports":
|
||||
exportType = ExportFormatExports
|
||||
case "envfile":
|
||||
exportType = ExportFormatEnvfile
|
||||
case "docker-args":
|
||||
exportType = ExportFormatDockerArgs
|
||||
case "shell":
|
||||
exportType = ExportFormatShell
|
||||
suffix = " "
|
||||
case "pretty":
|
||||
exportType = ExportFormatPretty
|
||||
default:
|
||||
common.LogFail(fmt.Sprintf("Unknown export format: %v", format))
|
||||
}
|
||||
exported := env.Export(exportType)
|
||||
fmt.Print(exported + suffix)
|
||||
}
|
||||
|
||||
//CommandBundle implements config:bundle
|
||||
func CommandBundle(args []string, global bool, merged bool) {
|
||||
appName, trailingArgs := getCommonArgs(global, args)
|
||||
if len(trailingArgs) > 0 {
|
||||
common.LogFail(fmt.Sprintf("Trailing argument(s): %v", trailingArgs))
|
||||
}
|
||||
env := getEnvironment(appName, merged)
|
||||
env.ExportBundle(os.Stdout)
|
||||
}
|
||||
|
||||
//getEnvironment for the given app (global config if appName is empty). Merge with global environment if merged is true.
|
||||
func getEnvironment(appName string, merged bool) (env *Env) {
|
||||
env, err := loadAppOrGlobalEnv(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
if appName != "" && merged {
|
||||
env, err = LoadMergedAppEnv(appName)
|
||||
if err != nil {
|
||||
common.LogFail(err.Error())
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
//getCommonArgs extracts common positional args (appName and keys)
|
||||
func getCommonArgs(global bool, args []string) (appName string, keys []string) {
|
||||
nextArg := 0
|
||||
if !global {
|
||||
if len(args) > 0 {
|
||||
appName = args[0]
|
||||
}
|
||||
if appName == "" {
|
||||
common.LogFail("Please specify an app or --global")
|
||||
} else {
|
||||
nextArg++
|
||||
}
|
||||
}
|
||||
keys = args[nextArg:]
|
||||
return appName, keys
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_main_cmd() {
|
||||
declare desc="print config vars for app via command line"
|
||||
local cmd="config"
|
||||
config_all "$@"
|
||||
}
|
||||
|
||||
config_main_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_get_cmd() {
|
||||
declare desc="get specified config vars from app via command line"
|
||||
local cmd="config:get"
|
||||
config_get "$@"
|
||||
}
|
||||
|
||||
config_get_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_set_cmd() {
|
||||
declare desc="set specified config vars for app via command line"
|
||||
local cmd="config:set"
|
||||
config_set "$@"
|
||||
}
|
||||
|
||||
config_set_cmd "$@"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_AVAILABLE_PATH/config/functions"
|
||||
|
||||
config_unset_cmd() {
|
||||
declare desc="unset specified config vars for app via command line"
|
||||
local cmd="config:unset"
|
||||
config_unset "$@"
|
||||
}
|
||||
|
||||
config_unset_cmd "$@"
|
||||
1
plugins/config/vendor/github.com/joho/godotenv/.gitignore
generated
vendored
Normal file
1
plugins/config/vendor/github.com/joho/godotenv/.gitignore
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
8
plugins/config/vendor/github.com/joho/godotenv/.travis.yml
generated
vendored
Normal file
8
plugins/config/vendor/github.com/joho/godotenv/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.8
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
23
plugins/config/vendor/github.com/joho/godotenv/LICENCE
generated
vendored
Normal file
23
plugins/config/vendor/github.com/joho/godotenv/LICENCE
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
Copyright (c) 2013 John Barton
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
163
plugins/config/vendor/github.com/joho/godotenv/README.md
generated
vendored
Normal file
163
plugins/config/vendor/github.com/joho/godotenv/README.md
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
# GoDotEnv [](https://travis-ci.org/joho/godotenv) [](https://ci.appveyor.com/project/joho/godotenv) [](https://goreportcard.com/report/github.com/joho/godotenv)
|
||||
|
||||
A Go (golang) port of the Ruby dotenv project (which loads env vars from a .env file)
|
||||
|
||||
From the original Library:
|
||||
|
||||
> Storing configuration in the environment is one of the tenets of a twelve-factor app. Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
|
||||
>
|
||||
> But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a .env file into ENV when the environment is bootstrapped.
|
||||
|
||||
It can be used as a library (for loading in env for your own daemons etc) or as a bin command.
|
||||
|
||||
There is test coverage and CI for both linuxish and windows environments, but I make no guarantees about the bin version working on windows.
|
||||
|
||||
## Installation
|
||||
|
||||
As a library
|
||||
|
||||
```shell
|
||||
go get github.com/joho/godotenv
|
||||
```
|
||||
|
||||
or if you want to use it as a bin command
|
||||
```shell
|
||||
go get github.com/joho/godotenv/cmd/godotenv
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Add your application configuration to your `.env` file in the root of your project:
|
||||
|
||||
```shell
|
||||
S3_BUCKET=YOURS3BUCKET
|
||||
SECRET_KEY=YOURSECRETKEYGOESHERE
|
||||
```
|
||||
|
||||
Then in your Go app you can do something like
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/joho/godotenv"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
s3Bucket := os.Getenv("S3_BUCKET")
|
||||
secretKey := os.Getenv("SECRET_KEY")
|
||||
|
||||
// now do something with s3 or whatever
|
||||
}
|
||||
```
|
||||
|
||||
If you're even lazier than that, you can just take advantage of the autoload package which will read in `.env` on import
|
||||
|
||||
```go
|
||||
import _ "github.com/joho/godotenv/autoload"
|
||||
```
|
||||
|
||||
While `.env` in the project root is the default, you don't have to be constrained, both examples below are 100% legit
|
||||
|
||||
```go
|
||||
_ = godotenv.Load("somerandomfile")
|
||||
_ = godotenv.Load("filenumberone.env", "filenumbertwo.env")
|
||||
```
|
||||
|
||||
If you want to be really fancy with your env file you can do comments and exports (below is a valid env file)
|
||||
|
||||
```shell
|
||||
# I am a comment and that is OK
|
||||
SOME_VAR=someval
|
||||
FOO=BAR # comments at line end are OK too
|
||||
export BAR=BAZ
|
||||
```
|
||||
|
||||
Or finally you can do YAML(ish) style
|
||||
|
||||
```yaml
|
||||
FOO: bar
|
||||
BAR: baz
|
||||
```
|
||||
|
||||
as a final aside, if you don't want godotenv munging your env you can just get a map back instead
|
||||
|
||||
```go
|
||||
var myEnv map[string]string
|
||||
myEnv, err := godotenv.Read()
|
||||
|
||||
s3Bucket := myEnv["S3_BUCKET"]
|
||||
```
|
||||
|
||||
... or from an `io.Reader` instead of a local file
|
||||
|
||||
```go
|
||||
reader := getRemoteFile()
|
||||
myEnv, err := godotenv.Parse(reader)
|
||||
```
|
||||
|
||||
... or from a `string` if you so desire
|
||||
|
||||
```go
|
||||
content := getRemoteFileContent()
|
||||
myEnv, err := godotenv.Unmarshal(content)
|
||||
```
|
||||
|
||||
### Command Mode
|
||||
|
||||
Assuming you've installed the command as above and you've got `$GOPATH/bin` in your `$PATH`
|
||||
|
||||
```
|
||||
godotenv -f /some/path/to/.env some_command with some args
|
||||
```
|
||||
|
||||
If you don't specify `-f` it will fall back on the default of loading `.env` in `PWD`
|
||||
|
||||
### Writing Env Files
|
||||
|
||||
Godotenv can also write a map representing the environment to a correctly-formatted and escaped file
|
||||
|
||||
```go
|
||||
env, err := godotenv.Unmarshal("KEY=value")
|
||||
err := godotenv.Write(env, "./.env")
|
||||
```
|
||||
|
||||
... or to a string
|
||||
|
||||
```go
|
||||
env, err := godotenv.Unmarshal("KEY=value")
|
||||
content, err := godotenv.Marshal(env)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are most welcome! The parser itself is pretty stupidly naive and I wouldn't be surprised if it breaks with edge cases.
|
||||
|
||||
*code changes without tests will not be accepted*
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Added some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
5. Create new Pull Request
|
||||
|
||||
## Releases
|
||||
|
||||
Releases should follow [Semver](http://semver.org/) though the first couple of releases are `v1` and `v1.1`.
|
||||
|
||||
Use [annotated tags for all releases](https://github.com/joho/godotenv/issues/30). Example `git tag -a v1.2.1`
|
||||
|
||||
## CI
|
||||
|
||||
Linux: [](https://travis-ci.org/joho/godotenv) Windows: [](https://ci.appveyor.com/project/joho/godotenv)
|
||||
|
||||
## Who?
|
||||
|
||||
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library.
|
||||
15
plugins/config/vendor/github.com/joho/godotenv/autoload/autoload.go
generated
vendored
Normal file
15
plugins/config/vendor/github.com/joho/godotenv/autoload/autoload.go
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
package autoload
|
||||
|
||||
/*
|
||||
You can just read the .env file on import just by doing
|
||||
|
||||
import _ "github.com/joho/godotenv/autoload"
|
||||
|
||||
And bob's your mother's brother
|
||||
*/
|
||||
|
||||
import "github.com/joho/godotenv"
|
||||
|
||||
func init() {
|
||||
godotenv.Load()
|
||||
}
|
||||
54
plugins/config/vendor/github.com/joho/godotenv/cmd/godotenv/cmd.go
generated
vendored
Normal file
54
plugins/config/vendor/github.com/joho/godotenv/cmd/godotenv/cmd.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var showHelp bool
|
||||
flag.BoolVar(&showHelp, "h", false, "show help")
|
||||
var rawEnvFilenames string
|
||||
flag.StringVar(&rawEnvFilenames, "f", "", "comma separated paths to .env files")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
usage := `
|
||||
Run a process with a env setup from a .env file
|
||||
|
||||
godotenv [-f ENV_FILE_PATHS] COMMAND_ARGS
|
||||
|
||||
ENV_FILE_PATHS: comma separated paths to .env files
|
||||
COMMAND_ARGS: command and args you want to run
|
||||
|
||||
example
|
||||
godotenv -f /path/to/something/.env,/another/path/.env fortune
|
||||
`
|
||||
// if no args or -h flag
|
||||
// print usage and return
|
||||
args := flag.Args()
|
||||
if showHelp || len(args) == 0 {
|
||||
fmt.Println(usage)
|
||||
return
|
||||
}
|
||||
|
||||
// load env
|
||||
var envFilenames []string
|
||||
if rawEnvFilenames != "" {
|
||||
envFilenames = strings.Split(rawEnvFilenames, ",")
|
||||
}
|
||||
|
||||
// take rest of args and "exec" them
|
||||
cmd := args[0]
|
||||
cmdArgs := args[1:]
|
||||
|
||||
err := godotenv.Exec(envFilenames, cmd, cmdArgs)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/equals.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/equals.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export OPTION_A='postgres://localhost:5432/database?sslmode=disable'
|
||||
|
||||
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/exported.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/exported.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export OPTION_A=2
|
||||
export OPTION_B='\n'
|
||||
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/invalid1.env
generated
vendored
Normal file
2
plugins/config/vendor/github.com/joho/godotenv/fixtures/invalid1.env
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
INVALID LINE
|
||||
foo=bar
|
||||
7
plugins/config/vendor/github.com/joho/godotenv/fixtures/plain.env
generated
vendored
Normal file
7
plugins/config/vendor/github.com/joho/godotenv/fixtures/plain.env
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
OPTION_A=1
|
||||
OPTION_B=2
|
||||
OPTION_C= 3
|
||||
OPTION_D =4
|
||||
OPTION_E = 5
|
||||
OPTION_F =
|
||||
OPTION_G=
|
||||
8
plugins/config/vendor/github.com/joho/godotenv/fixtures/quoted.env
generated
vendored
Normal file
8
plugins/config/vendor/github.com/joho/godotenv/fixtures/quoted.env
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
OPTION_A='1'
|
||||
OPTION_B='2'
|
||||
OPTION_C=''
|
||||
OPTION_D='\n'
|
||||
OPTION_E="1"
|
||||
OPTION_F="2"
|
||||
OPTION_G=""
|
||||
OPTION_H="\n"
|
||||
314
plugins/config/vendor/github.com/joho/godotenv/godotenv.go
generated
vendored
Normal file
314
plugins/config/vendor/github.com/joho/godotenv/godotenv.go
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
// Package godotenv is a go port of the ruby dotenv library (https://github.com/bkeepers/dotenv)
|
||||
//
|
||||
// Examples/readme can be found on the github page at https://github.com/joho/godotenv
|
||||
//
|
||||
// The TL;DR is that you make a .env file that looks something like
|
||||
//
|
||||
// SOME_ENV_VAR=somevalue
|
||||
//
|
||||
// and then in your go code you can call
|
||||
//
|
||||
// godotenv.Load()
|
||||
//
|
||||
// and all the env vars declared in .env will be available through os.Getenv("SOME_ENV_VAR")
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
|
||||
|
||||
// Load will read your env file(s) and load them into ENV for this process.
|
||||
//
|
||||
// Call this function as close as possible to the start of your program (ideally in main)
|
||||
//
|
||||
// If you call Load without any args it will default to loading .env in the current path
|
||||
//
|
||||
// You can otherwise tell it which files to load (there can be more than one) like
|
||||
//
|
||||
// godotenv.Load("fileone", "filetwo")
|
||||
//
|
||||
// It's important to note that it WILL NOT OVERRIDE an env variable that already exists - consider the .env file to set dev vars or sensible defaults
|
||||
func Load(filenames ...string) (err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
|
||||
for _, filename := range filenames {
|
||||
err = loadFile(filename, false)
|
||||
if err != nil {
|
||||
return // return early on a spazout
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Overload will read your env file(s) and load them into ENV for this process.
|
||||
//
|
||||
// Call this function as close as possible to the start of your program (ideally in main)
|
||||
//
|
||||
// If you call Overload without any args it will default to loading .env in the current path
|
||||
//
|
||||
// You can otherwise tell it which files to load (there can be more than one) like
|
||||
//
|
||||
// godotenv.Overload("fileone", "filetwo")
|
||||
//
|
||||
// It's important to note this WILL OVERRIDE an env variable that already exists - consider the .env file to forcefilly set all vars.
|
||||
func Overload(filenames ...string) (err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
|
||||
for _, filename := range filenames {
|
||||
err = loadFile(filename, true)
|
||||
if err != nil {
|
||||
return // return early on a spazout
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Read all env (with same file loading semantics as Load) but return values as
|
||||
// a map rather than automatically writing values into env
|
||||
func Read(filenames ...string) (envMap map[string]string, err error) {
|
||||
filenames = filenamesOrDefault(filenames)
|
||||
envMap = make(map[string]string)
|
||||
|
||||
for _, filename := range filenames {
|
||||
individualEnvMap, individualErr := readFile(filename)
|
||||
|
||||
if individualErr != nil {
|
||||
err = individualErr
|
||||
return // return early on a spazout
|
||||
}
|
||||
|
||||
for key, value := range individualEnvMap {
|
||||
envMap[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Parse reads an env file from io.Reader, returning a map of keys and values.
|
||||
func Parse(r io.Reader) (envMap map[string]string, err error) {
|
||||
envMap = make(map[string]string)
|
||||
|
||||
var lines []string
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, fullLine := range lines {
|
||||
if !isIgnoredLine(fullLine) {
|
||||
var key, value string
|
||||
key, value, err = parseLine(fullLine)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
envMap[key] = value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//Unmarshal reads an env file from a string, returning a map of keys and values.
|
||||
func Unmarshal(str string) (envMap map[string]string, err error) {
|
||||
return Parse(strings.NewReader(str))
|
||||
}
|
||||
|
||||
// Exec loads env vars from the specified filenames (empty map falls back to default)
|
||||
// then executes the cmd specified.
|
||||
//
|
||||
// Simply hooks up os.Stdin/err/out to the command and calls Run()
|
||||
//
|
||||
// If you want more fine grained control over your command it's recommended
|
||||
// that you use `Load()` or `Read()` and the `os/exec` package yourself.
|
||||
func Exec(filenames []string, cmd string, cmdArgs []string) error {
|
||||
Load(filenames...)
|
||||
|
||||
command := exec.Command(cmd, cmdArgs...)
|
||||
command.Stdin = os.Stdin
|
||||
command.Stdout = os.Stdout
|
||||
command.Stderr = os.Stderr
|
||||
return command.Run()
|
||||
}
|
||||
|
||||
// Write serializes the given environment and writes it to a file
|
||||
func Write(envMap map[string]string, filename string) error {
|
||||
content, error := Marshal(envMap)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
file, error := os.Create(filename)
|
||||
if error != nil {
|
||||
return error
|
||||
}
|
||||
_, err := file.WriteString(content)
|
||||
return err
|
||||
}
|
||||
|
||||
// Marshal outputs the given environment as a dotenv-formatted environment file.
|
||||
// Each line is in the format: KEY="VALUE" where VALUE is backslash-escaped.
|
||||
func Marshal(envMap map[string]string) (string, error) {
|
||||
lines := make([]string, 0, len(envMap))
|
||||
for k, v := range envMap {
|
||||
lines = append(lines, fmt.Sprintf(`%s="%s"`, k, doubleQuoteEscape(v)))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func filenamesOrDefault(filenames []string) []string {
|
||||
if len(filenames) == 0 {
|
||||
return []string{".env"}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
|
||||
func loadFile(filename string, overload bool) error {
|
||||
envMap, err := readFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentEnv := map[string]bool{}
|
||||
rawEnv := os.Environ()
|
||||
for _, rawEnvLine := range rawEnv {
|
||||
key := strings.Split(rawEnvLine, "=")[0]
|
||||
currentEnv[key] = true
|
||||
}
|
||||
|
||||
for key, value := range envMap {
|
||||
if !currentEnv[key] || overload {
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFile(filename string) (envMap map[string]string, err error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
return Parse(file)
|
||||
}
|
||||
|
||||
func parseLine(line string) (key string, value string, err error) {
|
||||
if len(line) == 0 {
|
||||
err = errors.New("zero length string")
|
||||
return
|
||||
}
|
||||
|
||||
// ditch the comments (but keep quoted hashes)
|
||||
if strings.Contains(line, "#") {
|
||||
segmentsBetweenHashes := strings.Split(line, "#")
|
||||
quotesAreOpen := false
|
||||
var segmentsToKeep []string
|
||||
for _, segment := range segmentsBetweenHashes {
|
||||
if strings.Count(segment, "\"") == 1 || strings.Count(segment, "'") == 1 {
|
||||
if quotesAreOpen {
|
||||
quotesAreOpen = false
|
||||
segmentsToKeep = append(segmentsToKeep, segment)
|
||||
} else {
|
||||
quotesAreOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(segmentsToKeep) == 0 || quotesAreOpen {
|
||||
segmentsToKeep = append(segmentsToKeep, segment)
|
||||
}
|
||||
}
|
||||
|
||||
line = strings.Join(segmentsToKeep, "#")
|
||||
}
|
||||
|
||||
firstEquals := strings.Index(line, "=")
|
||||
firstColon := strings.Index(line, ":")
|
||||
splitString := strings.SplitN(line, "=", 2)
|
||||
if firstColon != -1 && (firstColon < firstEquals || firstEquals == -1) {
|
||||
//this is a yaml-style line
|
||||
splitString = strings.SplitN(line, ":", 2)
|
||||
}
|
||||
|
||||
if len(splitString) != 2 {
|
||||
err = errors.New("Can't separate key from value")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the key
|
||||
key = splitString[0]
|
||||
if strings.HasPrefix(key, "export") {
|
||||
key = strings.TrimPrefix(key, "export")
|
||||
}
|
||||
key = strings.Trim(key, " ")
|
||||
|
||||
// Parse the value
|
||||
value = parseValue(splitString[1])
|
||||
return
|
||||
}
|
||||
|
||||
func parseValue(value string) string {
|
||||
|
||||
// trim
|
||||
value = strings.Trim(value, " ")
|
||||
|
||||
// check if we've got quoted values or possible escapes
|
||||
if len(value) > 1 {
|
||||
first := string(value[0:1])
|
||||
last := string(value[len(value)-1:])
|
||||
if first == last && strings.ContainsAny(first, `"'`) {
|
||||
// pull the quotes off the edges
|
||||
value = value[1 : len(value)-1]
|
||||
// handle escapes
|
||||
escapeRegex := regexp.MustCompile(`\\.`)
|
||||
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
|
||||
c := strings.TrimPrefix(match, `\`)
|
||||
switch c {
|
||||
case "n":
|
||||
return "\n"
|
||||
case "r":
|
||||
return "\r"
|
||||
default:
|
||||
return c
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
func isIgnoredLine(line string) bool {
|
||||
trimmedLine := strings.Trim(line, " \n\t")
|
||||
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")
|
||||
}
|
||||
|
||||
func doubleQuoteEscape(line string) string {
|
||||
for _, c := range doubleQuoteSpecialChars {
|
||||
toReplace := "\\" + string(c)
|
||||
if c == '\n' {
|
||||
toReplace = `\n`
|
||||
}
|
||||
if c == '\r' {
|
||||
toReplace = `\r`
|
||||
}
|
||||
line = strings.Replace(line, string(c), toReplace, -1)
|
||||
}
|
||||
return line
|
||||
}
|
||||
375
plugins/config/vendor/github.com/joho/godotenv/godotenv_test.go
generated
vendored
Normal file
375
plugins/config/vendor/github.com/joho/godotenv/godotenv_test.go
generated
vendored
Normal file
@@ -0,0 +1,375 @@
|
||||
package godotenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var noopPresets = make(map[string]string)
|
||||
|
||||
func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
|
||||
key, value, _ := parseLine(rawEnvLine)
|
||||
if key != expectedKey || value != expectedValue {
|
||||
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func loadEnvAndCompareValues(t *testing.T, loader func(files ...string) error, envFileName string, expectedValues map[string]string, presets map[string]string) {
|
||||
// first up, clear the env
|
||||
os.Clearenv()
|
||||
|
||||
for k, v := range presets {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
err := loader(envFileName)
|
||||
if err != nil {
|
||||
t.Fatalf("Error loading %v", envFileName)
|
||||
}
|
||||
|
||||
for k := range expectedValues {
|
||||
envValue := os.Getenv(k)
|
||||
v := expectedValues[k]
|
||||
if envValue != v {
|
||||
t.Errorf("Mismatch for key '%v': expected '%v' got '%v'", k, v, envValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadWithNoArgsLoadsDotEnv(t *testing.T) {
|
||||
err := Load()
|
||||
pathError := err.(*os.PathError)
|
||||
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
|
||||
t.Errorf("Didn't try and open .env by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverloadWithNoArgsOverloadsDotEnv(t *testing.T) {
|
||||
err := Overload()
|
||||
pathError := err.(*os.PathError)
|
||||
if pathError == nil || pathError.Op != "open" || pathError.Path != ".env" {
|
||||
t.Errorf("Didn't try and open .env by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileNotFound(t *testing.T) {
|
||||
err := Load("somefilethatwillneverexistever.env")
|
||||
if err == nil {
|
||||
t.Error("File wasn't found but Load didn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverloadFileNotFound(t *testing.T) {
|
||||
err := Overload("somefilethatwillneverexistever.env")
|
||||
if err == nil {
|
||||
t.Error("File wasn't found but Overload didn't return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPlainEnv(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "3",
|
||||
"OPTION_D": "4",
|
||||
"OPTION_E": "5",
|
||||
"OPTION_F": "",
|
||||
"OPTION_G": "",
|
||||
}
|
||||
|
||||
envMap, err := Read(envFileName)
|
||||
if err != nil {
|
||||
t.Error("Error reading file")
|
||||
}
|
||||
|
||||
if len(envMap) != len(expectedValues) {
|
||||
t.Error("Didn't get the right size map back")
|
||||
}
|
||||
|
||||
for key, value := range expectedValues {
|
||||
if envMap[key] != value {
|
||||
t.Error("Read got one of the keys wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
envMap, err := Parse(bytes.NewReader([]byte("ONE=1\nTWO='2'\nTHREE = \"3\"")))
|
||||
expectedValues := map[string]string{
|
||||
"ONE": "1",
|
||||
"TWO": "2",
|
||||
"THREE": "3",
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("error parsing env: %v", err)
|
||||
}
|
||||
for key, value := range expectedValues {
|
||||
if envMap[key] != value {
|
||||
t.Errorf("expected %s to be %s, got %s", key, value, envMap[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDoesNotOverride(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
|
||||
// ensure NO overload
|
||||
presets := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
"OPTION_B": "",
|
||||
}
|
||||
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
"OPTION_B": "",
|
||||
}
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, presets)
|
||||
}
|
||||
|
||||
func TestOveroadDoesOverride(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
|
||||
// ensure NO overload
|
||||
presets := map[string]string{
|
||||
"OPTION_A": "do_not_override",
|
||||
}
|
||||
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
}
|
||||
loadEnvAndCompareValues(t, Overload, envFileName, expectedValues, presets)
|
||||
}
|
||||
|
||||
func TestLoadPlainEnv(t *testing.T) {
|
||||
envFileName := "fixtures/plain.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "3",
|
||||
"OPTION_D": "4",
|
||||
"OPTION_E": "5",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadExportedEnv(t *testing.T) {
|
||||
envFileName := "fixtures/exported.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "2",
|
||||
"OPTION_B": "\n",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadEqualsEnv(t *testing.T) {
|
||||
envFileName := "fixtures/equals.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "postgres://localhost:5432/database?sslmode=disable",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestLoadQuotedEnv(t *testing.T) {
|
||||
envFileName := "fixtures/quoted.env"
|
||||
expectedValues := map[string]string{
|
||||
"OPTION_A": "1",
|
||||
"OPTION_B": "2",
|
||||
"OPTION_C": "",
|
||||
"OPTION_D": "\n",
|
||||
"OPTION_E": "1",
|
||||
"OPTION_F": "2",
|
||||
"OPTION_G": "",
|
||||
"OPTION_H": "\n",
|
||||
}
|
||||
|
||||
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
|
||||
}
|
||||
|
||||
func TestActualEnvVarsAreLeftAlone(t *testing.T) {
|
||||
os.Clearenv()
|
||||
os.Setenv("OPTION_A", "actualenv")
|
||||
_ = Load("fixtures/plain.env")
|
||||
|
||||
if os.Getenv("OPTION_A") != "actualenv" {
|
||||
t.Error("An ENV var set earlier was overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsing(t *testing.T) {
|
||||
// unquoted values
|
||||
parseAndCompare(t, "FOO=bar", "FOO", "bar")
|
||||
|
||||
// parses values with spaces around equal sign
|
||||
parseAndCompare(t, "FOO =bar", "FOO", "bar")
|
||||
parseAndCompare(t, "FOO= bar", "FOO", "bar")
|
||||
|
||||
// parses double quoted values
|
||||
parseAndCompare(t, `FOO="bar"`, "FOO", "bar")
|
||||
|
||||
// parses single quoted values
|
||||
parseAndCompare(t, "FOO='bar'", "FOO", "bar")
|
||||
|
||||
// parses escaped double quotes
|
||||
parseAndCompare(t, `FOO="escaped\"bar"`, "FOO", `escaped"bar`)
|
||||
|
||||
// parses single quotes inside double quotes
|
||||
parseAndCompare(t, `FOO="'d'"`, "FOO", `'d'`)
|
||||
|
||||
// parses yaml style options
|
||||
parseAndCompare(t, "OPTION_A: 1", "OPTION_A", "1")
|
||||
|
||||
//parses yaml values with equal signs
|
||||
parseAndCompare(t, "OPTION_A: Foo=bar", "OPTION_A", "Foo=bar")
|
||||
|
||||
// parses non-yaml options with colons
|
||||
parseAndCompare(t, "OPTION_A=1:B", "OPTION_A", "1:B")
|
||||
|
||||
// parses export keyword
|
||||
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
|
||||
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\n")
|
||||
|
||||
// it 'expands newlines in quoted strings' do
|
||||
// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
|
||||
parseAndCompare(t, `FOO="bar\nbaz"`, "FOO", "bar\nbaz")
|
||||
|
||||
// it 'parses varibales with "." in the name' do
|
||||
// expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar')
|
||||
parseAndCompare(t, "FOO.BAR=foobar", "FOO.BAR", "foobar")
|
||||
|
||||
// it 'parses varibales with several "=" in the value' do
|
||||
// expect(env('FOO=foobar=')).to eql('FOO' => 'foobar=')
|
||||
parseAndCompare(t, "FOO=foobar=", "FOO", "foobar=")
|
||||
|
||||
// it 'strips unquoted values' do
|
||||
// expect(env('foo=bar ')).to eql('foo' => 'bar') # not 'bar '
|
||||
parseAndCompare(t, "FOO=bar ", "FOO", "bar")
|
||||
|
||||
// it 'ignores inline comments' do
|
||||
// expect(env("foo=bar # this is foo")).to eql('foo' => 'bar')
|
||||
parseAndCompare(t, "FOO=bar # this is foo", "FOO", "bar")
|
||||
|
||||
// it 'allows # in quoted value' do
|
||||
// expect(env('foo="bar#baz" # comment')).to eql('foo' => 'bar#baz')
|
||||
parseAndCompare(t, `FOO="bar#baz" # comment`, "FOO", "bar#baz")
|
||||
parseAndCompare(t, "FOO='bar#baz' # comment", "FOO", "bar#baz")
|
||||
parseAndCompare(t, `FOO="bar#baz#bang" # comment`, "FOO", "bar#baz#bang")
|
||||
|
||||
// it 'parses # in quoted values' do
|
||||
// expect(env('foo="ba#r"')).to eql('foo' => 'ba#r')
|
||||
// expect(env("foo='ba#r'")).to eql('foo' => 'ba#r')
|
||||
parseAndCompare(t, `FOO="ba#r"`, "FOO", "ba#r")
|
||||
parseAndCompare(t, "FOO='ba#r'", "FOO", "ba#r")
|
||||
|
||||
//newlines and backslashes should be escaped
|
||||
parseAndCompare(t, `FOO="bar\n\ b\az"`, "FOO", "bar\n baz")
|
||||
parseAndCompare(t, `FOO="bar\\\n\ b\az"`, "FOO", "bar\\\n baz")
|
||||
parseAndCompare(t, `FOO="bar\\r\ b\az"`, "FOO", "bar\\r baz")
|
||||
|
||||
parseAndCompare(t, `="value"`, "", "value")
|
||||
parseAndCompare(t, `KEY="`, "KEY", "\"")
|
||||
parseAndCompare(t, `KEY="value`, "KEY", "\"value")
|
||||
|
||||
// it 'throws an error if line format is incorrect' do
|
||||
// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)
|
||||
badlyFormattedLine := "lol$wut"
|
||||
_, _, err := parseLine(badlyFormattedLine)
|
||||
if err == nil {
|
||||
t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinesToIgnore(t *testing.T) {
|
||||
// it 'ignores empty lines' do
|
||||
// expect(env("\n \t \nfoo=bar\n \nfizz=buzz")).to eql('foo' => 'bar', 'fizz' => 'buzz')
|
||||
if !isIgnoredLine("\n") {
|
||||
t.Error("Line with nothing but line break wasn't ignored")
|
||||
}
|
||||
|
||||
if !isIgnoredLine("\t\t ") {
|
||||
t.Error("Line full of whitespace wasn't ignored")
|
||||
}
|
||||
|
||||
// it 'ignores comment lines' do
|
||||
// expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql('foo' => 'bar')
|
||||
if !isIgnoredLine("# comment") {
|
||||
t.Error("Comment wasn't ignored")
|
||||
}
|
||||
|
||||
if !isIgnoredLine("\t#comment") {
|
||||
t.Error("Indented comment wasn't ignored")
|
||||
}
|
||||
|
||||
// make sure we're not getting false positives
|
||||
if isIgnoredLine(`export OPTION_B='\n'`) {
|
||||
t.Error("ignoring a perfectly valid line to parse")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorReadDirectory(t *testing.T) {
|
||||
envFileName := "fixtures/"
|
||||
envMap, err := Read(envFileName)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", envMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorParsing(t *testing.T) {
|
||||
envFileName := "fixtures/invalid1.env"
|
||||
envMap, err := Read(envFileName)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got %v", envMap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
writeAndCompare := func(env string, expected string) {
|
||||
envMap, _ := Unmarshal(env)
|
||||
actual, _ := Marshal(envMap)
|
||||
if expected != actual {
|
||||
t.Errorf("Expected '%v' (%v) to write as '%v', got '%v' instead.", env, envMap, expected, actual)
|
||||
}
|
||||
}
|
||||
//just test some single lines to show the general idea
|
||||
//TestRoundtrip makes most of the good assertions
|
||||
|
||||
//values are always double-quoted
|
||||
writeAndCompare(`key=value`, `key="value"`)
|
||||
//double-quotes are escaped
|
||||
writeAndCompare(`key=va"lu"e`, `key="va\"lu\"e"`)
|
||||
//but single quotes are left alone
|
||||
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
|
||||
// newlines, backslashes, and some other special chars are escaped
|
||||
writeAndCompare(`foo="$ba\n\r\\r!"`, `foo="\$ba\n\r\\r\!"`)
|
||||
// lines should be sorted
|
||||
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")
|
||||
}
|
||||
|
||||
func TestRoundtrip(t *testing.T) {
|
||||
fixtures := []string{"equals.env", "exported.env", "plain.env", "quoted.env"}
|
||||
for _, fixture := range fixtures {
|
||||
fixtureFilename := fmt.Sprintf("fixtures/%s", fixture)
|
||||
env, err := readFile(fixtureFilename)
|
||||
if err != nil {
|
||||
t.Errorf("Expected '%s' to read without error (%v)", fixtureFilename, err)
|
||||
}
|
||||
rep, err := Marshal(env)
|
||||
if err != nil {
|
||||
t.Errorf("Expected '%s' to Marshal (%v)", fixtureFilename, err)
|
||||
}
|
||||
roundtripped, err := Unmarshal(rep)
|
||||
if err != nil {
|
||||
t.Errorf("Expected '%s' to Mashal and Unmarshal (%v)", fixtureFilename, err)
|
||||
}
|
||||
if !reflect.DeepEqual(env, roundtripped) {
|
||||
t.Errorf("Expected '%s' to roundtrip as '%v', got '%v' instead", fixtureFilename, env, roundtripped)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
plugins/config/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
3
plugins/config/vendor/github.com/ryanuber/columnize/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
language: go
|
||||
go:
|
||||
- tip
|
||||
20
plugins/config/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
20
plugins/config/vendor/github.com/ryanuber/columnize/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2016 Ryan Uber
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
71
plugins/config/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
71
plugins/config/vendor/github.com/ryanuber/columnize/README.md
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
Columnize
|
||||
=========
|
||||
|
||||
Easy column-formatted output for golang
|
||||
|
||||
[](https://travis-ci.org/ryanuber/columnize)
|
||||
[](https://godoc.org/github.com/ryanuber/columnize)
|
||||
|
||||
Columnize is a really small Go package that makes building CLI's a little bit
|
||||
easier. In some CLI designs, you want to output a number similar items in a
|
||||
human-readable way with nicely aligned columns. However, figuring out how wide
|
||||
to make each column is a boring problem to solve and eats your valuable time.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
func main() {
|
||||
output := []string{
|
||||
"Name | Gender | Age",
|
||||
"Bob | Male | 38",
|
||||
"Sally | Female | 26",
|
||||
}
|
||||
result := columnize.SimpleFormat(output)
|
||||
fmt.Println(result)
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, you just pass in a list of strings. And the result:
|
||||
|
||||
```
|
||||
Name Gender Age
|
||||
Bob Male 38
|
||||
Sally Female 26
|
||||
```
|
||||
|
||||
Columnize is tolerant of missing or empty fields, or even empty lines, so
|
||||
passing in extra lines for spacing should show up as you would expect.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Columnize is configured using a `Config`, which can be obtained by calling the
|
||||
`DefaultConfig()` method. You can then tweak the settings in the resulting
|
||||
`Config`:
|
||||
|
||||
```
|
||||
config := columnize.DefaultConfig()
|
||||
config.Delim = "|"
|
||||
config.Glue = " "
|
||||
config.Prefix = ""
|
||||
config.Empty = ""
|
||||
config.NoTrim = false
|
||||
```
|
||||
|
||||
* `Delim` is the string by which columns of **input** are delimited
|
||||
* `Glue` is the string by which columns of **output** are delimited
|
||||
* `Prefix` is a string by which each line of **output** is prefixed
|
||||
* `Empty` is a string used to replace blank values found in output
|
||||
* `NoTrim` is a boolean used to disable the automatic trimming of input values
|
||||
|
||||
You can then pass the `Config` in using the `Format` method (signature below) to
|
||||
have text formatted to your liking.
|
||||
|
||||
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.
|
||||
191
plugins/config/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
191
plugins/config/vendor/github.com/ryanuber/columnize/columnize.go
generated
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
package columnize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config can be used to tune certain parameters which affect the way
|
||||
// in which Columnize will format output text.
|
||||
type Config struct {
|
||||
// The string by which the lines of input will be split.
|
||||
Delim string
|
||||
|
||||
// The string by which columns of output will be separated.
|
||||
Glue string
|
||||
|
||||
// The string by which columns of output will be prefixed.
|
||||
Prefix string
|
||||
|
||||
// A replacement string to replace empty fields.
|
||||
Empty string
|
||||
|
||||
// NoTrim disables automatic trimming of inputs.
|
||||
NoTrim bool
|
||||
}
|
||||
|
||||
// DefaultConfig returns a *Config with default values.
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Delim: "|",
|
||||
Glue: " ",
|
||||
Prefix: "",
|
||||
Empty: "",
|
||||
NoTrim: false,
|
||||
}
|
||||
}
|
||||
|
||||
// MergeConfig merges two config objects together and returns the resulting
|
||||
// configuration. Values from the right take precedence over the left side.
|
||||
func MergeConfig(a, b *Config) *Config {
|
||||
// Return quickly if either side was nil
|
||||
if a == nil {
|
||||
return b
|
||||
}
|
||||
if b == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
var result Config = *a
|
||||
|
||||
if b.Delim != "" {
|
||||
result.Delim = b.Delim
|
||||
}
|
||||
if b.Glue != "" {
|
||||
result.Glue = b.Glue
|
||||
}
|
||||
if b.Prefix != "" {
|
||||
result.Prefix = b.Prefix
|
||||
}
|
||||
if b.Empty != "" {
|
||||
result.Empty = b.Empty
|
||||
}
|
||||
if b.NoTrim {
|
||||
result.NoTrim = true
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
// stringFormat, given a set of column widths and the number of columns in
|
||||
// the current line, returns a sprintf-style format string which can be used
|
||||
// to print output aligned properly with other lines using the same widths set.
|
||||
func stringFormat(c *Config, widths []int, columns int) string {
|
||||
// Create the buffer with an estimate of the length
|
||||
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
|
||||
|
||||
// Start with the prefix, if any was given. The buffer will not return an
|
||||
// error so it does not need to be handled
|
||||
buf.WriteString(c.Prefix)
|
||||
|
||||
// Create the format string from the discovered widths
|
||||
for i := 0; i < columns && i < len(widths); i++ {
|
||||
if i == columns-1 {
|
||||
buf.WriteString("%s\n")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// elementsFromLine returns a list of elements, each representing a single
|
||||
// item which will belong to a column of output.
|
||||
func elementsFromLine(config *Config, line string) []interface{} {
|
||||
separated := strings.Split(line, config.Delim)
|
||||
elements := make([]interface{}, len(separated))
|
||||
for i, field := range separated {
|
||||
value := field
|
||||
if !config.NoTrim {
|
||||
value = strings.TrimSpace(field)
|
||||
}
|
||||
|
||||
// Apply the empty value, if configured.
|
||||
if value == "" && config.Empty != "" {
|
||||
value = config.Empty
|
||||
}
|
||||
elements[i] = value
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
// runeLen calculates the number of visible "characters" in a string
|
||||
func runeLen(s string) int {
|
||||
l := 0
|
||||
for _ = range s {
|
||||
l++
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// widthsFromLines examines a list of strings and determines how wide each
|
||||
// column should be considering all of the elements that need to be printed
|
||||
// within it.
|
||||
func widthsFromLines(config *Config, lines []string) []int {
|
||||
widths := make([]int, 0, 8)
|
||||
|
||||
for _, line := range lines {
|
||||
elems := elementsFromLine(config, line)
|
||||
for i := 0; i < len(elems); i++ {
|
||||
l := runeLen(elems[i].(string))
|
||||
if len(widths) <= i {
|
||||
widths = append(widths, l)
|
||||
} else if widths[i] < l {
|
||||
widths[i] = l
|
||||
}
|
||||
}
|
||||
}
|
||||
return widths
|
||||
}
|
||||
|
||||
// Format is the public-facing interface that takes a list of strings and
|
||||
// returns nicely aligned column-formatted text.
|
||||
func Format(lines []string, config *Config) string {
|
||||
conf := MergeConfig(DefaultConfig(), config)
|
||||
widths := widthsFromLines(conf, lines)
|
||||
|
||||
// Estimate the buffer size
|
||||
glueSize := len(conf.Glue)
|
||||
var size int
|
||||
for _, w := range widths {
|
||||
size += w + glueSize
|
||||
}
|
||||
size *= len(lines)
|
||||
|
||||
// Create the buffer
|
||||
buf := bytes.NewBuffer(make([]byte, 0, size))
|
||||
|
||||
// Create a cache for the string formats
|
||||
fmtCache := make(map[int]string, 16)
|
||||
|
||||
// Create the formatted output using the format string
|
||||
for _, line := range lines {
|
||||
elems := elementsFromLine(conf, line)
|
||||
|
||||
// Get the string format using cache
|
||||
numElems := len(elems)
|
||||
stringfmt, ok := fmtCache[numElems]
|
||||
if !ok {
|
||||
stringfmt = stringFormat(conf, widths, numElems)
|
||||
fmtCache[numElems] = stringfmt
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, stringfmt, elems...)
|
||||
}
|
||||
|
||||
// Get the string result
|
||||
result := buf.String()
|
||||
|
||||
// Remove trailing newline without removing leading/trailing space
|
||||
if n := len(result); n > 0 && result[n-1] == '\n' {
|
||||
result = result[:n-1]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SimpleFormat is a convenience function to format text with the defaults.
|
||||
func SimpleFormat(lines []string) string {
|
||||
return Format(lines, nil)
|
||||
}
|
||||
340
plugins/config/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
340
plugins/config/vendor/github.com/ryanuber/columnize/columnize_test.go
generated
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
package columnize
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
crand "crypto/rand"
|
||||
)
|
||||
|
||||
func TestListOfStringsInput(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyLinesOutput(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeadingSpacePreserved(t *testing.T) {
|
||||
input := []string{
|
||||
"| Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := " Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnWidthCalculator(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"Longer than A | Longer than B | Longer than C",
|
||||
"short | short | short",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "Longer than A Longer than B Longer than C\n"
|
||||
expected += "short short short"
|
||||
|
||||
if output != expected {
|
||||
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||
t.Fatalf("\n%s", printableProof)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
|
||||
"short | short | short",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
|
||||
expected += "short short short"
|
||||
|
||||
if output != expected {
|
||||
printableProof := fmt.Sprintf("\nGot: %+q", output)
|
||||
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
|
||||
t.Fatalf("\n%s", printableProof)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkColumnWidthCalculator(b *testing.B) {
|
||||
// Generate the input
|
||||
input := []string{
|
||||
"UUID A | UUID B | UUID C | Column D | Column E",
|
||||
}
|
||||
|
||||
format := "%s|%s|%s|%s"
|
||||
short := "short"
|
||||
|
||||
uuid := func() string {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := crand.Read(buf); err != nil {
|
||||
panic(fmt.Errorf("failed to read random bytes: %v", err))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
|
||||
buf[0:4],
|
||||
buf[4:6],
|
||||
buf[6:8],
|
||||
buf[8:10],
|
||||
buf[10:16])
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
|
||||
input = append(input, l)
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Format(input, config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariedInputSpacing(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A |Column B| Column C",
|
||||
"x|y| z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariedInputSpacing_NoTrim(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A|Column B|Column C",
|
||||
"x|y| z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.NoTrim = true
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmatchedColumnCounts(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"Value A | Value B",
|
||||
"Value A | Value B | Value C | Value D",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "Value A Value B\n"
|
||||
expected += "Value A Value B Value C Value D"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternateDelimiter(t *testing.T) {
|
||||
input := []string{
|
||||
"Column | A % Column | B % Column | C",
|
||||
"Value A % Value B % Value C",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Delim = "%"
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column | A Column | B Column | C\n"
|
||||
expected += "Value A Value B Value C"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternateSpacingString(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Glue = " "
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimpleFormat(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
output := SimpleFormat(input)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlternatePrefixString(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Prefix = " "
|
||||
output := Format(input, config)
|
||||
|
||||
expected := " Column A Column B Column C\n"
|
||||
expected += " x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFieldReplacement(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | | z",
|
||||
}
|
||||
|
||||
config := DefaultConfig()
|
||||
config.Empty = "<none>"
|
||||
output := Format(input, config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x <none> z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyConfigValues(t *testing.T) {
|
||||
input := []string{
|
||||
"Column A | Column B | Column C",
|
||||
"x | y | z",
|
||||
}
|
||||
|
||||
config := Config{}
|
||||
output := Format(input, &config)
|
||||
|
||||
expected := "Column A Column B Column C\n"
|
||||
expected += "x y z"
|
||||
|
||||
if output != expected {
|
||||
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfig(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
configA *Config
|
||||
configB *Config
|
||||
expect *Config
|
||||
}{
|
||||
{
|
||||
"merges b over a",
|
||||
&Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"},
|
||||
&Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"},
|
||||
&Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"},
|
||||
},
|
||||
{
|
||||
"merges only non-empty config values",
|
||||
&Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"},
|
||||
&Config{Delim: "b", Prefix: "b"},
|
||||
&Config{Delim: "b", Glue: "a", Prefix: "b", Empty: "a"},
|
||||
},
|
||||
{
|
||||
"takes b if a is nil",
|
||||
nil,
|
||||
&Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"},
|
||||
&Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"},
|
||||
},
|
||||
{
|
||||
"takes a if b is nil",
|
||||
&Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"},
|
||||
nil,
|
||||
&Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
m := MergeConfig(tc.configA, tc.configB)
|
||||
if !reflect.DeepEqual(m, tc.expect) {
|
||||
t.Fatalf("\nexpect:\n%#v\n\nactual:\n%#v", tc.expect, m)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -294,6 +294,8 @@ nginx_build_config() {
|
||||
SPDY_SUPPORTED="$(is_spdy_enabled "$NGINX_VERSION")"
|
||||
HTTP2_SUPPORTED="$(is_http2_enabled "$NGINX_VERSION")"
|
||||
|
||||
PROXY_PORT_MAP=$(echo "$PROXY_PORT_MAP" | xargs) # trailing spaces mess up default template
|
||||
|
||||
eval "$(config_export app "$APP")"
|
||||
local SIGIL_PARAMS=(-f $NGINX_TEMPLATE APP="$APP" DOKKU_ROOT="$DOKKU_ROOT"
|
||||
NOSSL_SERVER_NAME="$NOSSL_SERVER_NAME"
|
||||
|
||||
14
plugins/ps/app-restart
Executable file
14
plugins/ps/app-restart
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
|
||||
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
|
||||
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
|
||||
|
||||
ps_app_restart() {
|
||||
declare desc="ps app-restart plugin trigger"
|
||||
local trigger="ps_app_restart"
|
||||
local APP="$1";
|
||||
|
||||
ps_restart "$APP"
|
||||
}
|
||||
|
||||
ps_app_restart "$@"
|
||||
@@ -12,4 +12,5 @@ test_url () {
|
||||
|
||||
base_url="$1"
|
||||
test_url / "config-test"
|
||||
test_url /hello "Hello world"
|
||||
test_url /hello "Hello
|
||||
'world'"
|
||||
|
||||
@@ -4,6 +4,7 @@ set -ex;
|
||||
REMOTE=$1
|
||||
REPO=$2
|
||||
|
||||
# setting config variables with spaces is broken at the moment. ref: 820
|
||||
# ssh "$REMOTE" config:set "$REPO" CONFTEST=config-test HELLO=\"Hello world\"
|
||||
dokku config:set "$REPO" CONFTEST=config-test HELLO="Hello world"
|
||||
GREETING=$(printf "Hello
|
||||
'world'" | base64)
|
||||
ssh "$REMOTE" config:set "$REPO" CONFTEST=config-test
|
||||
ssh "$REMOTE" config:set --encoded "$REPO" HELLO="$GREETING"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"description": "The number of processes to run.",
|
||||
"generator": "echo 5"
|
||||
}
|
||||
},
|
||||
},
|
||||
"image": "gliderlabs/herokuish",
|
||||
"addons": [
|
||||
"dokku-postgres",
|
||||
|
||||
@@ -1,2 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e; output="$(curl -s -S "$1")"; echo "$output"; test "$output" == "Hello World!"
|
||||
set -e;
|
||||
|
||||
test_url () {
|
||||
path="$1"
|
||||
expected_output="$2"
|
||||
url="${base_url}:3000${path}"
|
||||
output="$(curl -s -S "$url")"
|
||||
test "$output" == "$expected_output"
|
||||
}
|
||||
|
||||
base_url="$1"
|
||||
test_url / "Hello World!"
|
||||
test_url /env/HELLO "Hello
|
||||
'world' "
|
||||
|
||||
@@ -8,6 +8,10 @@ app.get('/', function(request, response) {
|
||||
response.send('Hello World!')
|
||||
})
|
||||
|
||||
app.get('/env/:key', function(request, response) {
|
||||
response.send(process.env[request.params["key"]])
|
||||
})
|
||||
|
||||
app.listen(app.get('port'), function() {
|
||||
console.log("Node app is running at localhost:" + app.get('port'))
|
||||
})
|
||||
|
||||
9
tests/apps/dockerfile/post-deploy
Executable file
9
tests/apps/dockerfile/post-deploy
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex;
|
||||
|
||||
REMOTE=$1
|
||||
REPO=$2
|
||||
|
||||
GREETING=$(printf "Hello
|
||||
'world' " | base64)
|
||||
ssh "$REMOTE" config:set --encoded "$REPO" HELLO="$GREETING"
|
||||
@@ -1,2 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e; output="$(curl -s -S "$1")"; echo "$output"; test "$output" == "python/flask"
|
||||
set -e;
|
||||
base_url=$1
|
||||
test_url () {
|
||||
path="$1"
|
||||
expected_output="$2"
|
||||
url="${base_url}${path}"
|
||||
output="$(curl -s -S "$url")"
|
||||
echo "$url -> $output"
|
||||
test "$output" == "$expected_output"
|
||||
}
|
||||
|
||||
base_url="$1"
|
||||
test_url / "python/flask"
|
||||
test_url /HELLO "Hello
|
||||
'world' "
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import os
|
||||
from flask import Flask
|
||||
from flask import Flask, abort
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def hello():
|
||||
return 'python/flask'
|
||||
|
||||
|
||||
@app.route('/<env>')
|
||||
def get_enc(env):
|
||||
val = os.environ.get(env, None)
|
||||
if val:
|
||||
return val
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
9
tests/apps/python-flask/post-deploy
Executable file
9
tests/apps/python-flask/post-deploy
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -ex;
|
||||
|
||||
REMOTE=$1
|
||||
REPO=$2
|
||||
|
||||
GREETING=$(printf "Hello
|
||||
'world' " | base64)
|
||||
ssh "$REMOTE" config:set --encoded "$REPO" HELLO="$GREETING"
|
||||
47
tests/unit/30_config-oddities.bats
Normal file
47
tests/unit/30_config-oddities.bats
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
setup() {
|
||||
global_setup
|
||||
[[ -f ${DOKKU_ROOT}/ENV ]] && mv -f ${DOKKU_ROOT}/ENV ${DOKKU_ROOT}/ENV.bak
|
||||
sudo -H -u dokku /bin/bash -c "echo 'export global_test=true' > ${DOKKU_ROOT}/ENV"
|
||||
create_app
|
||||
}
|
||||
|
||||
teardown() {
|
||||
destroy_app
|
||||
ls -la ${DOKKU_ROOT}
|
||||
if [[ -f ${DOKKU_ROOT}/ENV.bak ]];then
|
||||
mv -f ${DOKKU_ROOT}/ENV.bak ${DOKKU_ROOT}/ENV
|
||||
fi
|
||||
global_teardown
|
||||
}
|
||||
|
||||
@test "(config-oddities) set-local/get with multiple spaces and $" {
|
||||
run dokku config:set --global double_quotes="hello world$" single_quotes='hello$ world'
|
||||
echo "output: "$output
|
||||
echo "status: "$status
|
||||
run dokku config:get --global double_quotes
|
||||
echo "output: "$output
|
||||
echo "status: "$status
|
||||
assert_output 'hello world$'
|
||||
run dokku config:get --global single_quotes
|
||||
echo "output: "$output
|
||||
echo "status: "$status
|
||||
assert_output 'hello$ world'
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "(config-oddities) set-local/get with multiple lines" {
|
||||
multiline='line one
|
||||
line two'
|
||||
run dokku config:set --global double_quotes="$multiline"
|
||||
echo "output: "$output
|
||||
echo "status: "$status
|
||||
run dokku config:get --global double_quotes
|
||||
echo "output: "$output
|
||||
echo "status: "$status
|
||||
assert_output "$multiline"
|
||||
assert_success
|
||||
}
|
||||
Reference in New Issue
Block a user