Merge pull request #2751 from alexquick/feature-config-golang

Move config plugin to golang
This commit is contained in:
Jose Diaz-Gonzalez
2017-10-28 20:40:06 -04:00
committed by GitHub
60 changed files with 2770 additions and 392 deletions

View File

@@ -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:
#

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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
}

View File

@@ -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
View File

@@ -0,0 +1,2 @@
/commands
/subcommands/*

25
plugins/config/Makefile Normal file
View 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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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

View 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")
}

View 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
}

View File

@@ -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
View 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: []

View 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

View 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))
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -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 "$@"

View File

@@ -0,0 +1 @@
.DS_Store

View 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
View 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.

View File

@@ -0,0 +1,163 @@
# GoDotEnv [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4?svg=true)](https://ci.appveyor.com/project/joho/godotenv) [![Go Report Card](https://goreportcard.com/badge/github.com/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: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](https://travis-ci.org/joho/godotenv) Windows: [![Build status](https://ci.appveyor.com/api/projects/status/9v40vnfvvgde64u4)](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.

View 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()
}

View 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)
}
}

View File

@@ -0,0 +1,2 @@
export OPTION_A='postgres://localhost:5432/database?sslmode=disable'

View File

@@ -0,0 +1,2 @@
export OPTION_A=2
export OPTION_B='\n'

View File

@@ -0,0 +1,2 @@
INVALID LINE
foo=bar

View File

@@ -0,0 +1,7 @@
OPTION_A=1
OPTION_B=2
OPTION_C= 3
OPTION_D =4
OPTION_E = 5
OPTION_F =
OPTION_G=

View 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"

View 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
}

View 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)
}
}
}

View File

@@ -0,0 +1,3 @@
language: go
go:
- tip

View 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.

View File

@@ -0,0 +1,71 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](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.

View 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)
}

View 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)
}
})
}
}

View File

@@ -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
View 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 "$@"

View File

@@ -12,4 +12,5 @@ test_url () {
base_url="$1"
test_url / "config-test"
test_url /hello "Hello world"
test_url /hello "Hello
'world'"

View File

@@ -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"

View File

@@ -24,7 +24,7 @@
"description": "The number of processes to run.",
"generator": "echo 5"
}
},
},
"image": "gliderlabs/herokuish",
"addons": [
"dokku-postgres",

View File

@@ -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' "

View File

@@ -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'))
})

View 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"

View File

@@ -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' "

View File

@@ -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)

View 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"

View 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
}