refactor: rewrite proxy plugin in golang

This commit is contained in:
Jose Diaz-Gonzalez
2020-02-22 06:35:30 -05:00
parent 26e8524ae5
commit 6fd4b5adb1
54 changed files with 1899 additions and 514 deletions

View File

@@ -15,6 +15,19 @@ The following commands were previously deprecated and were removed in this relea
- `trace`: Use `trace:on` or `trace:off` instead.
- `ps`: Use `enter` with a container ID instead to run `ps auxww` within a container.
The refactor of the proxy plugin removes the following functions from being sourced in plugins:
- `proxy#get_app_proxy_port_map()`
- `proxy#list_app_proxy_ports()`
- `proxy#filter_app_proxy_ports()`
- `proxy#add_proxy_ports()`
- `proxy#remove_proxy_ports()`
- `proxy#set_proxy_ports()`
The informal policy is to allow removal of functions within a `functions` file if they are not used in OSS plugins. Should you be affected by any such removals, please file an issue to expose the functionality via a plugin trigger.
## Deprecations
- `nginx:show-conf` has been deprecated in favor of `nginx:show-config`.
- `proxy#is_app_proxy_enabled()` is deprecated in favor of `plugn trigger proxy-is-enabled`
- `proxy#get_app_proxy_type()` is deprecated in favor of `plugn trigger proxy-type`

View File

@@ -1295,6 +1295,36 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `proxy-is-enabled`
- Description: Checks if there is a proxy enabled for the app
- Invoked by:
- Arguments: `$APP`
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `proxy-type`
- Description: Returns the proxy type for an app
- Invoked by:
- Arguments: `$APP`
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
# TODO
```
### `receive-app`
- Description: Allows you to customize what occurs when an app is received. Normally just triggers an app build.

View File

@@ -0,0 +1 @@
hook

View File

@@ -0,0 +1 @@
hook

View File

@@ -895,7 +895,7 @@ internal_get_app_urls() {
local SCHEME="https"
fi
if [[ "$(is_app_proxy_enabled "$APP")" == "false" ]]; then
if [[ "$(plugn trigger proxy-is-enabled "$APP")" == "false" ]]; then
if [[ -n "$RAW_TCP_PORTS" ]]; then
local APP_CONTAINER_PORTS="$(get_container_ports "$APP")"
local app_port

View File

@@ -12,7 +12,7 @@ trigger-nginx-vhosts-core-post-deploy() {
declare APP="$1"
local HAS_NETWORK_CONFIG
if [[ "$(get_app_proxy_type "$1")" != "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then
return
fi

View File

@@ -295,7 +295,7 @@ nginx_build_config() {
local IS_APP_VHOST_ENABLED=$(is_app_vhost_enabled "$APP")
if [[ "$(is_app_proxy_enabled "$APP")" == "true" ]]; then
if [[ "$(plugn trigger proxy-is-enabled "$APP")" == "true" ]]; then
if [[ -z "$DOKKU_APP_LISTEN_PORT" ]] && [[ -z "$DOKKU_APP_LISTEN_IP" ]]; then
DOKKU_APP_LISTENERS="$(plugn trigger network-get-listeners "$APP" | xargs)"
elif [[ -n "$DOKKU_APP_LISTEN_PORT" ]] && [[ -n "$DOKKU_APP_LISTEN_IP" ]]; then

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
trigger-nginx-vhosts-post-certs-update() {
declare desc="unset port config vars after SSL cert is added"
declare trigger="post-certs-update"
declare APP="$1"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
local DOKKU_PROXY_PORT=$(config_get "$APP" DOKKU_PROXY_PORT)
local DOKKU_PROXY_SSL_PORT=$(config_get "$APP" DOKKU_PROXY_SSL_PORT)
local DOKKU_PROXY_PORT_MAP=$(config_get "$APP" DOKKU_PROXY_PORT_MAP)
if [[ "$DOKKU_PROXY_PORT" == "80" ]]; then
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_PORT
fi
if [[ "$DOKKU_PROXY_SSL_PORT" == "443" ]]; then
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_SSL_PORT
fi
if [[ "$DOKKU_PROXY_PORT_MAP" == *http:80:* ]]; then
# shellcheck disable=SC2046
remove_proxy_ports "$APP" $(filter_app_proxy_ports "$APP" "https" "443")
# shellcheck disable=SC2046
for port_map in $(filter_app_proxy_ports "$APP" "http" "80"); do
add_proxy_ports "$APP" "https:443:$(cut -d ':' -f3 <<<"$port_map")"
done
fi
fi
}
trigger-nginx-vhosts-post-certs-update "$@"

View File

@@ -12,7 +12,7 @@ trigger-nginx-vhosts-post-domains-update() {
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
nginx_build_config "$APP"
fi
}

View File

@@ -10,7 +10,7 @@ trigger-nginx-vhosts-post-proxy-ports-update() {
declare trigger="post-proxy-ports-update"
declare APP="$1" ACTION="$2"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
nginx_build_config "$APP"
fi
}

View File

@@ -10,7 +10,7 @@ trigger-nginx-vhosts-pre-disable-vhost() {
declare trigger="pre-disable-vhost"
declare APP="$1"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_PORT DOKKU_PROXY_SSL_PORT DOKKU_PROXY_PORT_MAP
fi
}

View File

@@ -10,7 +10,7 @@ trigger-nginx-vhosts-pre-enable-vhost() {
declare trigger="pre-enable-vhost"
declare APP="$1"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_PORT DOKKU_PROXY_SSL_PORT DOKKU_PROXY_PORT_MAP
fi
}

View File

@@ -11,7 +11,7 @@ trigger-nginx-vhosts-proxy-build-config() {
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
plugn trigger network-build-config "$APP"
nginx_build_config "$APP"
fi

View File

@@ -11,7 +11,7 @@ trigger-nginx-vhosts-proxy-clear-config() {
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
plugn trigger network-clear-config "$APP"
nginx_clear_config "$APP"
fi

View File

@@ -12,7 +12,7 @@ trigger-nginx-vhosts-proxy-disable() {
declare APP="$1"
verify_app_name "$APP"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
disable_app_vhost "$APP" --no-restart
ps_restart "$APP"
fi

View File

@@ -12,7 +12,7 @@ trigger-nginx-vhosts-proxy-enable() {
declare APP="$1"
verify_app_name "$APP"
if [[ "$(get_app_proxy_type "$APP")" == "nginx" ]]; then
if [[ "$(plugn trigger proxy-type "$APP")" == "nginx" ]]; then
enable_app_vhost "$APP" --no-restart
ps_restart "$APP"
fi

View File

@@ -12,7 +12,7 @@ cmd-nginx-build-config() {
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
local PROXY_APP_TYPE
PROXY_APP_TYPE="$(get_app_proxy_type "$APP")"
PROXY_APP_TYPE="$(plugn trigger proxy-type "$APP")"
if [[ "$PROXY_APP_TYPE" == "nginx" ]]; then
plugn trigger proxy-build-config "$APP"

8
plugins/proxy/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/commands
/subcommands/*
/triggers/*
/triggers
/proxy-*
/install
/post-*
/report

6
plugins/proxy/Makefile Normal file
View File

@@ -0,0 +1,6 @@
SUBCOMMANDS = subcommands/disable subcommands/enable subcommands/ports subcommands/ports-add subcommands/ports-clear subcommands/ports-remove subcommands/ports-set subcommands/report subcommands/set
TRIGGERS = triggers/proxy-is-enabled triggers/proxy-type triggers/post-certs-remove triggers/post-certs-update triggers/report
BUILD = commands subcommands triggers
PLUGIN_NAME = proxy
include ../../common.mk

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
[[ " help proxy:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
source "$PLUGIN_AVAILABLE_PATH/proxy/help-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
case "$1" in
help | proxy:help)
cmd-proxy-help "$@"
;;
*)
exit "$DOKKU_NOT_IMPLEMENTED_EXIT"
;;
esac

View File

@@ -6,124 +6,18 @@ source "$PLUGIN_AVAILABLE_PATH/config/functions"
is_app_proxy_enabled() {
declare desc="return true if proxy is enabled; otherwise return false"
local APP="$1"
verify_app_name "$APP"
local APP_PROXY_ENABLED=true
declare APP="$1"
declare deprecated=true
dokku_log_warn "Deprecated: plugn#proxy-is-enabled"
local DOKKU_DISABLE_PROXY=$(config_get "$APP" DOKKU_DISABLE_PROXY)
if [[ -n "$DOKKU_DISABLE_PROXY" ]]; then
local APP_PROXY_ENABLED=false
fi
echo $APP_PROXY_ENABLED
plugn trigger proxy-is-enabled "$APP"
}
get_app_proxy_type() {
declare desc="return app proxy type"
local APP="$1"
verify_app_name "$APP"
local DOKKU_APP_PROXY_TYPE="$(config_get "$APP" DOKKU_APP_PROXY_TYPE || true)"
local APP_PROXY_TYPE="${DOKKU_APP_PROXY_TYPE:-nginx}"
declare APP="$1"
declare deprecated=true
dokku_log_warn "Deprecated: plugn#proxy-type"
echo "$APP_PROXY_TYPE"
}
get_app_proxy_port_map() {
declare desc="return a list of configured port mappings"
local APP="$1"
local PROXY_PORT_MAP="$(config_get "$APP" DOKKU_PROXY_PORT_MAP || true)"
echo "$PROXY_PORT_MAP"
}
list_app_proxy_ports() {
declare desc="list proxy port mappings for an app"
local APP="$1"
verify_app_name "$APP"
local PROXY_PORT_MAP="$(get_app_proxy_port_map "$APP")"
if [[ -n "$PROXY_PORT_MAP" ]]; then
dokku_log_info1_quiet "Port mappings for $APP"
dokku_col_log_info1_quiet "scheme" "host port" "container port"
local port_map
for port_map in $PROXY_PORT_MAP; do
dokku_col_log_msg "$(cut -d ':' -f1 <<<"$port_map")" "$(cut -d ':' -f2 <<<"$port_map")" "$(cut -d ':' -f3 <<<"$port_map")"
done
else
dokku_log_fail "No port mappings configured for app ($APP)"
fi
}
filter_app_proxy_ports() {
declare desc="list proxy port mappings for an app that start with a particular scheme and a host port"
local APP="$1"
verify_app_name "$APP"
local SCHEME="$2" HOST_PORT="$3"
local PROXY_PORT_MAP="$(get_app_proxy_port_map "$APP")"
if [[ -n "$PROXY_PORT_MAP" ]]; then
local port_map
for port_map in $PROXY_PORT_MAP; do
local scheme="$(cut -d ':' -f1 <<<"$port_map")"
local host_port="$(cut -d ':' -f2 <<<"$port_map")"
if [[ "$scheme" == "$SCHEME" ]] && [[ "$host_port" == "$HOST_PORT" ]]; then
echo "$port_map"
fi
done
fi
}
add_proxy_ports() {
declare desc="add proxy port mappings for an app"
local APP="$1"
local PROXY_PORT_MAP="$(get_app_proxy_port_map "$APP")"
shift 1
local INPUT_PORTS="$*"
if [[ -n "$INPUT_PORTS" ]]; then
PROXY_PORT_MAP="$(merge_dedupe_list "$PROXY_PORT_MAP $INPUT_PORTS" " ")"
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_PROXY_PORT_MAP="$PROXY_PORT_MAP"
else
dokku proxy:help
dokku_log_fail "No port mapping specified. Exiting..."
fi
}
remove_proxy_ports() {
declare desc="remove specific proxy port mappings for an app"
local APP="$1"
local PROXY_PORT_MAP="$(get_app_proxy_port_map "$APP")"
local RE_PORT='^[0-9]+$'
shift 1
local INPUT_PORTS="$*"
local port
PROXY_PORT_MAP=" $PROXY_PORT_MAP "
for port in $INPUT_PORTS; do
if [[ "$port" =~ $RE_PORT ]]; then
PROXY_PORT_MAP="$(sed -r "s/[[:alnum:]]+:$port:[[:alnum:]]+//g" <<<"$PROXY_PORT_MAP")"
else
PROXY_PORT_MAP="$(sed -r "s/ $port / /g" <<<"$PROXY_PORT_MAP")"
fi
done
PROXY_PORT_MAP="$(echo "$PROXY_PORT_MAP" | xargs)"
PROXY_PORT_MAP="$(merge_dedupe_list "$PROXY_PORT_MAP" " ")"
if [[ -n "$PROXY_PORT_MAP" ]]; then
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_PROXY_PORT_MAP="$PROXY_PORT_MAP"
else
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_PORT_MAP
fi
}
set_proxy_ports() {
declare desc="SET proxy port mappings for an app"
local APP="$1"
shift 1
local INPUT_PORTS="$*"
if [[ -n "$INPUT_PORTS" ]]; then
PROXY_PORT_MAP="$(merge_dedupe_list "$INPUT_PORTS" " ")"
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_PROXY_PORT_MAP="$PROXY_PORT_MAP"
else
dokku proxy:help
dokku_log_fail "No port mapping specified. Exiting..."
fi
plugn trigger proxy-type "$APP"
}

3
plugins/proxy/glide.yaml Normal file
View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/ryanuber/columnize

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-proxy-help() {
declare desc="help command"
declare CMD="$1"
local plugin_name="proxy"
local plugin_description="Manage the proxy integration for an app"
if [[ "$CMD" == "$plugin_name:help" ]]; then
echo -e "Usage: dokku $plugin_name[:COMMAND]"
echo ''
echo "$plugin_description"
echo ''
echo 'Additional commands:'
fn-help-content | sort | column -c2 -t -s,
elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then
fn-help-content
else
cat <<help_desc
$plugin_name, $plugin_description
help_desc
fi
}
fn-help-content() {
declare desc="return help content"
cat <<help_content
proxy:disable <app>, Disable proxy for app
proxy:enable <app>, Enable proxy for app
proxy:ports <app>, List proxy port mappings for app
proxy:ports-add <app> [<scheme>:<host-port>:<container-port>...], Add proxy port mappings to an app
proxy:ports-clear <app>, Clear all proxy port mappings for an app
proxy:ports-remove <app> [<host-port>|<scheme>:<host-port>:<container-port>...], Remove specific proxy port mappings from an app
proxy:ports-set <app> [<scheme>:<host-port>:<container-port>...], Set proxy port mappings for an app
proxy:report [<app>] [<flag>], Displays a proxy report for one or more apps
proxy:set <app> <proxy-type>, Set proxy type for app
help_content
}

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-proxy-report() {
declare desc="displays a proxy report for one or more apps"
declare cmd="proxy:report"
[[ "$1" == "$cmd" ]] && shift 1
local INSTALLED_APPS=$(dokku_apps)
local APP="$1" INFO_FLAG="$2"
if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then
INFO_FLAG="$APP"
APP=""
fi
if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then
INFO_FLAG="true"
fi
if [[ -z "$APP" ]]; then
for app in $INSTALLED_APPS; do
cmd-proxy-report-single "$app" "$INFO_FLAG" | tee || true
done
else
cmd-proxy-report-single "$APP" "$INFO_FLAG"
fi
}
cmd-proxy-report-single() {
declare APP="$1" INFO_FLAG="$2"
if [[ "$INFO_FLAG" == "true" ]]; then
INFO_FLAG=""
fi
verify_app_name "$APP"
local flag_map=(
"--proxy-enabled: $(fn-proxy-enabled "$APP")"
"--proxy-type: $(get_app_proxy_type "$APP")"
"--proxy-port-map: $(get_app_proxy_port_map "$APP")"
)
if [[ -z "$INFO_FLAG" ]]; then
dokku_log_info2_quiet "$APP proxy information"
for flag in "${flag_map[@]}"; do
key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')"
dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")"
done
else
local match=false
local value_exists=false
for flag in "${flag_map[@]}"; do
valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)"
if [[ "$flag" == "${INFO_FLAG}:"* ]]; then
value=${flag#*: }
size="${#value}"
if [[ "$size" -ne 0 ]]; then
echo "$value" && match=true && value_exists=true
else
match=true
fi
fi
done
[[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}"
[[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed"
fi
}
fn-proxy-enabled() {
declare APP="$1"
local PROXY_ENABLED=false
if [[ "$(is_app_proxy_enabled "$APP")" == "true" ]]; then
PROXY_ENABLED=true
fi
echo "$PROXY_ENABLED"
}

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
trigger-proxy-post-certs-remove() {
declare desc="unset port config vars after SSL cert is removed"
declare trigger="post-certs-remove"
declare APP="$1"
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_SSL_PORT
# shellcheck disable=SC2046
remove_proxy_ports "$APP" $(filter_app_proxy_ports "$APP" "https" "443")
}
trigger-proxy-post-certs-remove "$@"

View File

@@ -1,10 +1,27 @@
package proxy
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
columnize "github.com/ryanuber/columnize"
)
type PortMap struct {
ContainerPort int
HostPort int
Scheme string
}
func (p PortMap) String() string {
return fmt.Sprintf("%s:%d:%d", p.Scheme, p.HostPort, p.ContainerPort)
}
// IsAppProxyEnabled returns true if proxy is enabled; otherwise return false
func IsAppProxyEnabled(appName string) bool {
err := common.VerifyAppName(appName)
@@ -19,3 +36,176 @@ func IsAppProxyEnabled(appName string) bool {
}
return proxyEnabled
}
// ReportSingleApp is an internal function that displays the app report for one or more apps
func ReportSingleApp(appName string, infoFlag string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
proxyEnabled := "false"
if IsAppProxyEnabled(appName) {
proxyEnabled = "true"
}
var proxyPortMap []string
for _, portMap := range getProxyPortMap(appName) {
proxyPortMap = append(proxyPortMap, portMap.String())
}
infoFlags := map[string]string{
"--proxy-enabled": proxyEnabled,
"--proxy-type": getAppProxyType(appName),
"--proxy-port-map": strings.Join(proxyPortMap, " "),
}
trimPrefix := false
uppercaseFirstCharacter := true
return common.ReportSingleApp("proxy", appName, infoFlag, infoFlags, trimPrefix, uppercaseFirstCharacter)
}
func addProxyPorts(appName string, proxyPortMap []PortMap) error {
allPortMaps := getProxyPortMap(appName)
allPortMaps = append(allPortMaps, proxyPortMap...)
return setProxyPorts(appName, allPortMaps)
}
func filterAppProxyPorts(appName string, scheme string, hostPort int) []PortMap {
var filteredProxyMaps []PortMap
proxyPortMap := getProxyPortMap(appName)
for _, portMap := range proxyPortMap {
if portMap.Scheme == scheme && portMap.HostPort == hostPort {
filteredProxyMaps = append(filteredProxyMaps, portMap)
}
}
return filteredProxyMaps
}
func getAppName(args []string) (appName string, err error) {
if len(args) >= 1 {
appName = args[0]
} else {
err = errors.New("Please specify an app to run the command on")
}
return
}
func getAppProxyType(appName string) string {
return config.GetWithDefault(appName, "DOKKU_APP_PROXY_TYPE", "nginx")
}
func getProxyPortMap(appName string) []PortMap {
value := config.GetWithDefault(appName, "DOKKU_PROXY_PORT_MAP", "")
return parseProxyPortMapString(value)
}
func listAppProxyPorts(appName string) error {
proxyPortMap := getProxyPortMap(appName)
if len(proxyPortMap) == 0 {
return errors.New("No port mappings configured for app")
}
var lines []string
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
lines = append(lines, "-----> scheme:host port:container port")
}
for _, portMap := range proxyPortMap {
lines = append(lines, portMap.String())
}
common.LogInfo1Quiet(fmt.Sprintf("Port mappings for %s", appName))
config := columnize.DefaultConfig()
config.Delim = ":"
config.Prefix = " "
config.Empty = ""
fmt.Println(columnize.Format(lines, config))
return nil
}
func setProxyPorts(appName string, proxyPortMap []PortMap) error {
var value []string
for _, portMap := range uniqueProxyPortMap(proxyPortMap) {
value = append(value, portMap.String())
}
entries := map[string]string{
"DOKKU_PROXY_PORT_MAP": strings.Join(value, " "),
}
return config.SetMany(appName, entries, false)
}
func removeProxyPorts(appName string, proxyPortMap []PortMap) error {
var toRemove map[string]bool
for _, portMap := range proxyPortMap {
toRemove[portMap.String()] = true
}
var toSet []PortMap
existingPortMaps := getProxyPortMap(appName)
for _, portMap := range existingPortMaps {
if toRemove[portMap.String()] {
continue
}
toSet = append(toSet, portMap)
}
if len(toSet) == 0 {
keys := []string{"DOKKU_PROXY_PORT_MAP"}
return config.UnsetMany(appName, keys, false)
}
return setProxyPorts(appName, toSet)
}
func parseProxyPortMapString(stringPortMap string) []PortMap {
var proxyPortMap []PortMap
for _, v := range strings.Split(strings.TrimSpace(stringPortMap), "") {
parts := strings.SplitN(v, ":", 3)
if len(parts) != 3 {
common.LogWarn(fmt.Sprintf("Invalid port map %s", v))
continue
}
hostPort, err := strconv.Atoi(parts[1])
if err != nil {
common.LogWarn(fmt.Sprintf("Invalid port map %s", v))
}
containerPort, err := strconv.Atoi(parts[2])
if err != nil {
common.LogWarn(fmt.Sprintf("Invalid port map %s", v))
}
proxyPortMap = append(proxyPortMap, PortMap{
ContainerPort: containerPort,
HostPort: hostPort,
Scheme: parts[0],
})
}
return uniqueProxyPortMap(proxyPortMap)
}
func uniqueProxyPortMap(proxyPortMap []PortMap) []PortMap {
var unique []PortMap
var existingPortMaps map[string]bool
for _, portMap := range proxyPortMap {
if existingPortMaps[portMap.String()] {
continue
}
existingPortMaps[portMap.String()] = true
unique = append(unique, portMap)
}
return unique
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_AVAILABLE_PATH/proxy/internal-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-proxy-report-single "$@"

View File

@@ -0,0 +1,70 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
columnize "github.com/ryanuber/columnize"
)
const (
helpHeader = `Usage: dokku proxy[:COMMAND]
Manage the proxy integration for an app
Additional commands:`
helpContent = `
proxy:disable <app>, Disable proxy for app
proxy:enable <app>, Enable proxy for app
proxy:ports <app>, List proxy port mappings for app
proxy:ports-add <app> [<scheme>:<host-port>:<container-port>...], Add proxy port mappings to an app
proxy:ports-clear <app>, Clear all proxy port mappings for an app
proxy:ports-remove <app> [<host-port>|<scheme>:<host-port>:<container-port>...], Remove specific proxy port mappings from an app
proxy:ports-set <app> [<scheme>:<host-port>:<container-port>...], Set proxy port mappings for an app
proxy:report [<app>] [<flag>], Displays a proxy report for one or more apps
proxy:set <app> <proxy-type>, Set proxy type for app
`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "proxy", "proxy:help":
usage()
case "help":
command := common.NewShellCmd(fmt.Sprintf("ps -o command= %d", os.Getppid()))
command.ShowOutput = false
output, err := command.Output()
if err == nil && strings.Contains(string(output), "--all") {
fmt.Println(helpContent)
} else {
fmt.Print("\n proxy, Manage the proxy integration for an app\n")
}
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 = " "
config.Empty = ""
content := strings.Split(helpContent, "\n")[1:]
fmt.Println(helpHeader)
fmt.Println(columnize.Format(content, config))
}

6
plugins/proxy/src/glide.lock generated Normal file
View File

@@ -0,0 +1,6 @@
hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149
updated: 2017-01-03T17:16:50.97156327-08:00
imports:
- name: github.com/ryanuber/columnize
version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360
testImports: []

View File

@@ -0,0 +1,3 @@
package: .
import:
- package: github.com/ryanuber/columnize

View File

@@ -0,0 +1,63 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/proxy"
)
// main entrypoint to all subcommands
func main() {
parts := strings.Split(os.Args[0], "/")
subcommand := parts[len(parts)-1]
var err error
switch subcommand {
case "disable":
args := flag.NewFlagSet("proxy:disable", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandDisable(args.Args())
case "enable":
args := flag.NewFlagSet("proxy:enable", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandEnable(args.Args())
case "ports":
args := flag.NewFlagSet("proxy:ports", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandPorts(args.Args())
case "ports-add":
args := flag.NewFlagSet("proxy:ports-add", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandPortsAdd(args.Args())
case "ports-clear":
args := flag.NewFlagSet("proxy:ports-clear", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandPortsClear(args.Args())
case "ports-remove":
args := flag.NewFlagSet("proxy:ports-remove", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandPortsRemove(args.Args())
case "ports-set":
args := flag.NewFlagSet("proxy:ports-set", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandPortsSet(args.Args())
case "report":
args := flag.NewFlagSet("proxy:report", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandReport(args.Args())
case "set":
args := flag.NewFlagSet("proxy:set", flag.ExitOnError)
args.Parse(os.Args[2:])
err = proxy.CommandSet(args.Args())
default:
common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand))
}
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,43 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/proxy"
)
// main entrypoint to all triggers
func main() {
parts := strings.Split(os.Args[0], "/")
trigger := parts[len(parts)-1]
flag.Parse()
var err error
switch trigger {
case "proxy-is-enabled":
appName := flag.Arg(0)
proxy.TriggerProxyIsEnabled(appName)
case "proxy-type":
appName := flag.Arg(0)
proxy.TriggerProxyType(appName)
case "post-certs-remove":
appName := flag.Arg(0)
proxy.TriggerPostCertsRemove(appName)
case "post-certs-update":
appName := flag.Arg(0)
proxy.TriggerPostCertsUpdate(appName)
case "report":
appName := flag.Arg(0)
err = proxy.ReportSingleApp(appName, "")
default:
common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger))
}
if err != nil {
common.LogFail(err.Error())
}
}

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,69 @@
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 = ""
```
* `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
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,178 @@
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
}
// DefaultConfig returns a *Config with default values.
func DefaultConfig() *Config {
return &Config{
Delim: "|",
Glue: " ",
Prefix: "",
Empty: "",
}
}
// 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 {
var result Config = *a
// Return quickly if either side was nil
if a == nil || b == nil {
return &result
}
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
}
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{} {
seperated := strings.Split(line, config.Delim)
elements := make([]interface{}, len(seperated))
for i, field := range seperated {
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,306 @@
package columnize
import (
"fmt"
"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 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) {
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
conf3 := &Config{Delim: "c", Prefix: "c"}
m := MergeConfig(conf1, conf2)
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, conf3)
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, nil)
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, &Config{})
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
}

View File

@@ -0,0 +1,201 @@
package proxy
import (
"errors"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
)
// CommandDisable disables the proxy for app via command line
func CommandDisable(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if !IsAppProxyEnabled(appName) {
common.LogInfo1("Proxy is already disable for app")
return nil
}
common.LogInfo1("Disabling proxy for app")
restart := true
if len(args) >= 2 && args[1] == "--no-restart" {
restart = false
}
entries := map[string]string{
"DOKKU_DISABLE_PROXY": "1",
}
if err := config.SetMany(appName, entries, restart); err != nil {
return err
}
return common.PlugnTrigger("proxy-disable", []string{appName}...)
}
// CommandEnable enables the proxy for app via command line
func CommandEnable(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if IsAppProxyEnabled(appName) {
common.LogInfo1("Proxy is already enabled for app")
return nil
}
common.LogInfo1("Enabling proxy for app")
restart := true
if len(args) >= 2 && args[1] == "--no-restart" {
restart = false
}
keys := []string{"DOKKU_DISABLE_PROXY"}
if err := config.UnsetMany(appName, keys, restart); err != nil {
return err
}
return common.PlugnTrigger("proxy-enable", []string{appName}...)
}
// CommandPorts is a cmd wrapper to list proxy port mappings for an app
func CommandPorts(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
return listAppProxyPorts(appName)
}
// CommandPortsAdd adds proxy port mappings to an app
func CommandPortsAdd(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if len(args) == 1 {
return errors.New("No port mapping specified")
}
stringPortMap := strings.Join(args[1:], " ")
proxyPortMap := parseProxyPortMapString(stringPortMap)
if err := addProxyPorts(appName, proxyPortMap); err != nil {
return err
}
return common.PlugnTrigger("post-proxy-ports-update", []string{appName, "add"}...)
}
// CommandPortsClear clears all proxy port mappings for an app
func CommandPortsClear(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
keys := []string{"DOKKU_PROXY_PORT_MAP"}
err = config.UnsetMany(appName, keys, false)
if err != nil {
return err
}
return common.PlugnTrigger("post-proxy-ports-update", []string{appName, "clear"}...)
}
// CommandPortsRemove removes specific proxy port mappings from an app
func CommandPortsRemove(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if len(args) == 1 {
return errors.New("No port mapping specified")
}
stringPortMap := strings.Join(args[1:], " ")
proxyPortMap := parseProxyPortMapString(stringPortMap)
if err := removeProxyPorts(appName, proxyPortMap); err != nil {
return err
}
return common.PlugnTrigger("post-proxy-ports-update", []string{appName, "remove"}...)
}
// CommandPortsSet sets proxy port mappings for an app
func CommandPortsSet(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if len(args) == 1 {
return errors.New("No port mapping specified")
}
stringPortMap := strings.Join(args[1:], " ")
proxyPortMap := parseProxyPortMapString(stringPortMap)
if err := setProxyPorts(appName, proxyPortMap); err != nil {
return err
}
return common.PlugnTrigger("post-proxy-ports-update", []string{appName, "set"}...)
}
// CommandReport displays a proxy report for one or more apps
func CommandReport(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
infoFlag := ""
if len(args) > 1 {
infoFlag = args[1]
}
if strings.HasPrefix(appName, "--") {
infoFlag = appName
appName = ""
}
if len(appName) == 0 {
apps, err := common.DokkuApps()
if err != nil {
return err
}
for _, appName := range apps {
if err := ReportSingleApp(appName, infoFlag); err != nil {
return err
}
}
return nil
}
return ReportSingleApp(appName, infoFlag)
}
// CommandSet sets a proxy for an app
func CommandSet(args []string) error {
appName, err := getAppName(args)
if err != nil {
return err
}
if len(appName) < 2 {
return errors.New("Please specify a proxy type!")
}
proxyType := args[1]
entries := map[string]string{
"DOKKU_APP_PROXY_TYPE": proxyType,
}
return config.SetMany(appName, entries, false)
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/proxy/help-functions"
cmd-proxy-help "proxy:help"

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
cmd-proxy-disable() {
declare desc="disable proxy for app via command line"
declare cmd="proxy:disable"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
verify_app_name "$APP"
if [[ "$(is_app_proxy_enabled "$APP")" == "true" ]]; then
dokku_log_info1 "Disabling proxy for app ($APP)"
[[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2
# shellcheck disable=SC2086
DOKKU_QUIET_OUTPUT=1 config_set $CONFIG_SET_ARGS $APP DOKKU_DISABLE_PROXY=1
plugn trigger proxy-disable "$APP"
else
dokku_log_info1 "proxy is already disable for app ($APP)"
fi
}
cmd-proxy-disable "$2" --no-restart

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
cmd-proxy-enable() {
declare desc="enable proxy for app via command line"
declare cmd="proxy:enable"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
verify_app_name "$APP"
if [[ "$(is_app_proxy_enabled "$APP")" == "false" ]]; then
dokku_log_info1 "Enabling proxy for app ($APP)"
[[ "$2" == "--no-restart" ]] && local CONFIG_SET_ARGS=$2
# shellcheck disable=SC2086
DOKKU_QUIET_OUTPUT=1 config_unset $CONFIG_SET_ARGS $APP DOKKU_DISABLE_PROXY
plugn trigger proxy-enable "$APP"
else
dokku_log_info1 "proxy is already enabled for app ($APP)"
fi
}
cmd-proxy-enable "$2" --no-restart

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
cmd-proxy-ports() {
declare desc="cmd wrapper to list proxy port mappings for an app"
declare cmd="proxy:ports"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
list_app_proxy_ports "$APP"
}
cmd-proxy-ports "$@"

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
cmd-proxy-ports-add() {
declare desc="add proxy port mappings to an app"
declare cmd="proxy:ports-add"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
shift 1
add_proxy_ports "$APP" "$@"
plugn trigger post-proxy-ports-update "$APP" "add"
}
cmd-proxy-ports-add "$@"

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
cmd-proxy-ports-clear() {
declare desc="clear all proxy port mappings for an app"
declare cmd="proxy:ports-clear"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
DOKKU_QUIET_OUTPUT=1 config_unset --no-restart "$APP" DOKKU_PROXY_PORT_MAP
plugn trigger post-proxy-ports-update "$APP" "clear"
}
cmd-proxy-ports-clear "$@"

View File

@@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
cmd-proxy-ports-remove() {
declare desc="remove specific proxy port mappings from an app"
declare cmd="proxy:ports-remove"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
shift 1
local INPUT_PORTS="$*"
if [[ -z "$INPUT_PORTS" ]]; then
dokku_log_warn "No port mapping specified. Please supply a valid configured host port"
list_app_proxy_ports "$APP"
dokku_log_fail "exiting..."
fi
remove_proxy_ports "$APP" "$@"
plugn trigger post-proxy-ports-update "$APP" "remove"
}
cmd-proxy-ports-remove "$@"

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/proxy/functions"
cmd-proxy-ports-set() {
declare desc="set proxy port mappings for an app"
declare cmd="proxy:ports-set"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
shift 1
set_proxy_ports "$APP" "$@"
plugn trigger post-proxy-ports-update "$APP" "set"
}
cmd-proxy-ports-set "$@"

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
source "$PLUGIN_AVAILABLE_PATH/proxy/internal-functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-proxy-report "$@"

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
cmd-proxy-set() {
declare desc="enable proxy for app via command line"
declare cmd="proxy:set"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" PROXY_TYPE="$2"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
verify_app_name "$APP"
if [[ -n "$PROXY_TYPE" ]]; then
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_PROXY_TYPE="$PROXY_TYPE"
else
dokku_log_fail "Please specify a proxy type!"
fi
}
cmd-proxy-set "$@"

89
plugins/proxy/triggers.go Normal file
View File

@@ -0,0 +1,89 @@
package proxy
import (
"fmt"
"github.com/dokku/dokku/plugins/config"
)
func TriggerProxyIsEnabled(appName string) error {
if IsAppProxyEnabled(appName) {
fmt.Println("true")
} else {
fmt.Println("false")
}
return nil
}
func TriggerProxyType(appName string) error {
proxyType := getAppProxyType(appName)
fmt.Println(proxyType)
return nil
}
func TriggerPostCertsRemove(appName string) error {
keys := []string{"DOKKU_PROXY_SSL_PORT"}
if err := config.UnsetMany(appName, keys, false); err != nil {
return err
}
return removeProxyPorts(appName, filterAppProxyPorts(appName, "https", 443))
}
// TriggerPostCertsUpdate unsets port config vars after SSL cert is added
func TriggerPostCertsUpdate(appName string) error {
port := config.GetWithDefault(appName, "DOKKU_PROXY_PORT", "")
sslPort := config.GetWithDefault(appName, "DOKKU_PROXY_SSL_PORT", "")
proxyPortMap := getProxyPortMap(appName)
toUnset := []string{}
if port == "80" {
toUnset = append(toUnset, "DOKKU_PROXY_PORT")
}
if sslPort == "443" {
toUnset = append(toUnset, "DOKKU_PROXY_SSL_PORT")
}
if len(toUnset) > 0 {
if err := config.UnsetMany(appName, toUnset, false); err != nil {
return err
}
}
var http80Ports []PortMap
for _, portMap := range proxyPortMap {
if portMap.Scheme == "http" && portMap.HostPort == 80 {
http80Ports = append(http80Ports, portMap)
}
}
if len(http80Ports) > 0 {
var https443Ports []PortMap
for _, portMap := range proxyPortMap {
if portMap.Scheme == "https" && portMap.HostPort == 443 {
https443Ports = append(https443Ports, portMap)
}
}
if err := removeProxyPorts(appName, https443Ports); err != nil {
return err
}
var toAdd []PortMap
for _, portMap := range http80Ports {
toAdd = append(toAdd, PortMap{
Scheme: "https",
HostPort: 443,
ContainerPort: portMap.ContainerPort,
})
}
if err := addProxyPorts(appName, toAdd); err != nil {
return err
}
}
return nil
}

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,69 @@
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 = ""
```
* `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
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,178 @@
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
}
// DefaultConfig returns a *Config with default values.
func DefaultConfig() *Config {
return &Config{
Delim: "|",
Glue: " ",
Prefix: "",
Empty: "",
}
}
// 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 {
var result Config = *a
// Return quickly if either side was nil
if a == nil || b == nil {
return &result
}
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
}
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{} {
seperated := strings.Split(line, config.Delim)
elements := make([]interface{}, len(seperated))
for i, field := range seperated {
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,306 @@
package columnize
import (
"fmt"
"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 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) {
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
conf3 := &Config{Delim: "c", Prefix: "c"}
m := MergeConfig(conf1, conf2)
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, conf3)
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, nil)
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, &Config{})
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
}