diff --git a/.editorconfig b/.editorconfig index 0643f4310..2c518168b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,5 +17,5 @@ indent_size = 4 [*.go] insert_final_newline = true -indent_style = space +indent_style = tab indent_size = 4 diff --git a/.stickler.yml b/.stickler.yml index bb59453ca..0304acb21 100644 --- a/.stickler.yml +++ b/.stickler.yml @@ -7,3 +7,4 @@ linters: files: ignore: - './debian/*' + - '*/vendor/*' diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 4222a9fb7..4179f1c85 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -344,6 +344,141 @@ if [[ ! -f "$DOKKU_ROOT/HOSTNAME" ]]; then fi ``` +### `network-build-config` + +- Description: Rebuilds network configuration +- Invoked by: `internally triggered by proxy-build-config within proxy implementations` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-compute-ports` + +- Description: Computes the ports for a given app container +- Invoked by: `internally triggered by proxy-build-config within proxy implementations` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-config-exists` + +- Description: Returns whether the network configuration for a given app exists +- Invoked by: `internally triggered by core-post-deploy within proxy implementations` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-get-ipaddr` + +- Description: Return the ipaddr for a given app container +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP $PROC_TYPE $CONTAINER_ID` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-get-listeners` + +- Description: Return the listeners (host:port combinations) for a given app container +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-get-property` + +- Description: Return the network value for an application's property +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP $KEY` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-get-port` + +- Description: Return the port for a given app container +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP $PROC_TYPE $CONTAINER_ID $IS_HEROKUISH_CONTAINER` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-write-ipaddr` + +- Description: Write the ipaddr for a given app index +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP $PROC_TYPE $CONTAINER_INDEX $IP_ADDRESS` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `network-write-port` + +- Description: Write the port for a given app index +- Invoked by: `internally triggered by a deploy` +- Arguments: `$APP $PROC_TYPE $CONTAINER_INDEX $PORT` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `nginx-hostname` - Description: Allows you to customize the hostname for a given application. @@ -410,6 +545,40 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x # TODO ``` +### `post-certs-remove` + +- Description: Allows you to run commands after a cert is removed +- Invoked by: `dokku certs:remove` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +APP="$1"; verify_app_name "$APP" + +# TODO +``` + +### `post-certs-update` + +- Description: Allows you to run commands after a cert is added/updated +- Invoked by: `dokku certs:add`, `dokku certs:update` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +APP="$1"; verify_app_name "$APP" + +# TODO +``` + ### `post-create` - Description: Can be used to run commands after an application is created. @@ -763,10 +932,10 @@ APP="$1"; verify_app_name "$APP" # TODO ``` -### `post-certs-update` +### `proxy-build-config` -- Description: Allows you to run commands after a cert is added/updated -- Invoked by: `dokku certs:add`, `dokku certs:update` +- Description: Builds the proxy implementation configuration for a given app +- Invoked by: `internally triggered by ps:restore` - Arguments: `$APP` - Example: @@ -774,16 +943,29 @@ APP="$1"; verify_app_name "$APP" #!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -APP="$1"; verify_app_name "$APP" # TODO ``` -### `post-certs-remove` +### `proxy-enable` -- Description: Allows you to run commands after a cert is removed -- Invoked by: `dokku certs:remove` +- Description: Enables the configured proxy implementation for an app +- Invoked by: `internally triggered by ps:restore` +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `proxy-disable` + +- Description: Disables the configured proxy implementation for an app +- Invoked by: `internally triggered by ps:restore` - Arguments: `$APP` - Example: @@ -791,8 +973,6 @@ APP="$1"; verify_app_name "$APP" #!/usr/bin/env bash set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -APP="$1"; verify_app_name "$APP" # TODO ``` diff --git a/docs/configuration/dns.md b/docs/networking/dns.md similarity index 100% rename from docs/configuration/dns.md rename to docs/networking/dns.md diff --git a/docs/networking/network.md b/docs/networking/network.md new file mode 100644 index 000000000..810b39ecc --- /dev/null +++ b/docs/networking/network.md @@ -0,0 +1,124 @@ +# Network Management + +> New as of 0.11.0 + +``` +network:report [] [] # Displays a network report for one or more apps +network:rebuild # Rebuilds network settings for an app +network:rebuildall # Rebuild network settings for all apps +network:set () # Set or clear a network property for an app +``` + +The Network plugin allows developers to abstract the concept of container network management, allowing developers to both change what networks a given container is attached to as well as rebuild the configuration on the fly. + +## Usage + +### Rebuilding network settings + +There are cases where you may need to rebuild the network configuration for an app, such as on app boot or container restart. In these cases, you can use the `network:rebuild` command: + +```shell +dokku network:rebuild node-js-app +``` + +> This command will exit a non-zero number that depends on the number of containers for which configuration could not be built + +### Rebuilding all network settings + +In some cases, a docker upgrade may reset container IPs or Ports. In both cases, you can quickly rewrite those files by using the `network:rebuildall` command: + +```shell +dokku network:rebuildall +``` + +> This command will exit a non-zero number that depends on the number of containers for which configuration could not be built + +### Container network interface binding + +> This functionality does not control the `--network` docker flag. Please use the [docker-options plugin](docs/advanced-usage/docker-options.md) to manage this flag. + +By default, an application will only bind to the internal interface. This behavior can be modified per app by changing the `bind-all-interfaces` network property. + +```shell +# bind to the default docker interface (`docker0`) with a random internal ip +# this is the default behavior +dokku network:set node-js-app bind-all-interfaces false + +# bind to all interfaces (`0.0.0.0`) on a random port for each upstream port +# this will make the app container directly accessible by other hosts on your network +# ports are randomized for every deploy, e.g. `0.0.0.0:32771->5000/tcp`. +dokku network:set node-js-app bind-all-interfaces true +``` + +By way of example, in the default case, each container is bound to the docker interface: + +```shell +docker ps +``` + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +1b88d8aec3d1 dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute node-js-app.web.1 +``` + +As such, the container's IP address will be an internal IP, and thus it is only accessible on the host itself: + +``` +docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' node-js-app.web.1 +``` + +``` +172.17.0.6 +``` + +However, you can disable the internal proxying via the `network:set` command so that it will listen on the host's IP address: + +```shell +dokku network:set node-js-app bind-all-interfaces true + +# container bound to all interfaces +docker ps +``` + +``` +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +d6499edb0edb dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute 0.0.0.0:49153->5000/tcp node-js-app.web.1 +``` + +### Displaying network reports about an app + +You can get a report about the app's network status using the `network:report` command: + +```shell +dokku network:report +``` + +``` +=====> node-js-app network information + Network bind all interfaces: false + Network listeners: 172.17.0.1:5000 +=====> python-sample network information + Network bind all interfaces: false + Network listeners: 172.17.0.2:5000 +=====> ruby-sample network information + Network bind all interfaces: true + Network listeners: +``` + +You can run the command for a specific app also. + +```shell +dokku network:report node-js-app +``` + +``` +=====> node-js-app network information + Network bind all interfaces: false + Network listeners: 172.17.0.1:5000 +``` + +You can pass flags which will output only the value of the specific information you want. For example: + +```shell +dokku network:report node-js-app --network-bind-all-interfaces +``` diff --git a/docs/advanced-usage/proxy-management.md b/docs/networking/proxy-management.md similarity index 75% rename from docs/advanced-usage/proxy-management.md rename to docs/networking/proxy-management.md index 09f88c0c4..f4f02ef0e 100644 --- a/docs/advanced-usage/proxy-management.md +++ b/docs/networking/proxy-management.md @@ -19,46 +19,9 @@ In Dokku 0.5.0, port proxying was decoupled from the `nginx-vhosts` plugin into ### Container network interface binding -> New as of 0.5.0 +> Changed as of 0.11.0 -By default, the deployed docker container running your app's web process will bind to the internal docker network interface (i.e. `docker inspect --format '{{ .NetworkSettings.IPAddress }}' $CONTAINER_ID`). This behavior can be modified per app by disabling the proxy (i.e. `dokku proxy:disable `). In this case, the container will bind to an external interface (i.e. `0.0.0.0`) and your app container will be directly accessible by other hosts on your network. - -> If a proxy is disabled, Dokku will bind your container's port to a random port on the host for every deploy, e.g. `0.0.0.0:32771->5000/tcp`. - -By way of example, in the default case, each container is bound to the docker interface: - -```shell -docker ps -``` - -``` -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -1b88d8aec3d1 dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute node-js-app.web.1 -``` - -As such, the container's IP address will be an internal IP, and thus it is only accessible on the host itself: - -``` -docker inspect --format '{{ .NetworkSettings.IPAddress }}' node-js-app.web.1 -``` - -``` -172.17.0.6 -``` - -However, you can disable the internal proxying via the `proxy:disable` command so that it will listen on the host's IP address: - -```shell -dokku proxy:disable node-js-app - -# container bound to all interfaces -docker ps -``` - -``` -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -d6499edb0edb dokku/node-js-app:latest "/bin/bash -c '/star About a minute ago Up About a minute 0.0.0.0:49153->5000/tcp node-js-app.web.1 -``` +From Dokku versions `0.5.0` until `0.11.0`, enabling or disabling an application's proxy would **also** control whether or not the application was bound to all interfaces - e.g. `0.0.0.0`. As of `0.10.0`, this is now controlled by the network plugin. Please see the [network documentation](/docs/networking/network.md#container-network-interface-binding) for more information. ### Displaying proxy reports about an app diff --git a/docs/template.html b/docs/template.html index 46dd4be7e..81ced6701 100644 --- a/docs/template.html +++ b/docs/template.html @@ -136,11 +136,16 @@ Configuration Environment Variables - DNS Configuration Domain Configuration Nginx Configuration SSL Configuration + Network Management + + DNS Configuration + Network Management + Proxy and Port Management + Advanced Usage Backup and Recovery @@ -149,7 +154,6 @@ Event Logs Persistent Storage Plugin Management - Proxy and Port Management Repository Management Community Contributions diff --git a/docs/viewdocs.json b/docs/viewdocs.json index 873566feb..e37e59f74 100644 --- a/docs/viewdocs.json +++ b/docs/viewdocs.json @@ -15,16 +15,19 @@ "deployment/images": "deployment/methods/images/", "configuration-management": "configuration/environment-variables/", "deployment/ssl-configuration": "configuration/ssl/", - "dns": "configuration/dns/", "nginx": "configuration/nginx/", + "dns": "networking/dns/", + "configuration/dns": "networking/dns/", + "proxy": "networking/proxy-management/", + "advanced-usage/proxy-management": "networking/proxy-management/", + "backup-recovery": "advanced-usage/backup-recovery/", "deployment-tasks": "advanced-usage/deployment-tasks/", "deployment/deployment-tasks": "advanced-usage/deployment-tasks/", "docker-options": "advanced-usage/docker-options/", "dokku-events-logs": "advanced-usage/event-logs/", "dokku-storage": "advanced-usage/persistent-storage/", - "proxy": "advanced-usage/proxy-management/", "plugins": "community/plugins/" } diff --git a/plugins/20_events/network-build-config b/plugins/20_events/network-build-config new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-build-config @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-compute-ports b/plugins/20_events/network-compute-ports new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-compute-ports @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-config-exists b/plugins/20_events/network-config-exists new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-config-exists @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-get-ipaddr b/plugins/20_events/network-get-ipaddr new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-get-ipaddr @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-get-listeners b/plugins/20_events/network-get-listeners new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-get-listeners @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-get-port b/plugins/20_events/network-get-port new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-get-port @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-get-property b/plugins/20_events/network-get-property new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-get-property @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-write-ipaddr b/plugins/20_events/network-write-ipaddr new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-write-ipaddr @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/network-write-port b/plugins/20_events/network-write-port new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/network-write-port @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/proxy-build-config b/plugins/20_events/proxy-build-config new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/proxy-build-config @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/proxy-disable b/plugins/20_events/proxy-disable new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/proxy-disable @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/proxy-enable b/plugins/20_events/proxy-enable new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/proxy-enable @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/checks/subcommands/run b/plugins/checks/subcommands/run index 9f9b9f2f9..d1f9bdfc7 100755 --- a/plugins/checks/subcommands/run +++ b/plugins/checks/subcommands/run @@ -45,18 +45,21 @@ check_process_type() { check_process() { local APP="$1" PROC_TYPE="$2" CONTAINER_INDEX="$3" - local DOKKU_CONTAINER_ID_FILE="$DOKKU_ROOT/$APP/CONTAINER.$PROC_TYPE.$CONTAINER_INDEX" - local DOKKU_IP_FILE="$DOKKU_ROOT/$APP/IP.$PROC_TYPE.$CONTAINER_INDEX" - local DOKKU_PORT_FILE="$DOKKU_ROOT/$APP/PORT.$PROC_TYPE.$CONTAINER_INDEX" + local CONTAINER_ID DOKKU_CONTAINER_ID_FILE IMAGE IP IS_HEROKUISH_CONTAINER PORT + DOKKU_CONTAINER_ID_FILE="$DOKKU_ROOT/$APP/CONTAINER.$PROC_TYPE.$CONTAINER_INDEX" if [[ ! -f "$DOKKU_CONTAINER_ID_FILE" ]]; then dokku_log_fail "Invalid container index specified ($APP.$PROC_TYPE.$CONTAINER_INDEX)" fi + IS_HEROKUISH_CONTAINER=false + IMAGE=$(get_app_image_name "$APP") + is_image_herokuish_based "$IMAGE" && IS_HEROKUISH_CONTAINER=true + dokku_log_info1 "Running checks for app ($APP.$PROC_TYPE.$CONTAINER_INDEX)" - local CONTAINER_ID=$(< "$DOKKU_CONTAINER_ID_FILE") - local IP=$(< "$DOKKU_IP_FILE") - local PORT=$(< "$DOKKU_PORT_FILE") + CONTAINER_ID=$(< "$DOKKU_CONTAINER_ID_FILE") + IP="$(plugn trigger network-get-ipaddr "$APP" "$PROC_TYPE" "$CONTAINER_ID")" + PORT="$(plugn trigger network-get-port "$APP" "$PROC_TYPE" "$IS_HEROKUISH_CONTAINER" "$CONTAINER_ID")" plugn trigger check-deploy "$APP" "$CONTAINER_ID" "$PROC_TYPE" "$PORT" "$IP" } diff --git a/plugins/common/common.go b/plugins/common/common.go index 31060a760..d8faf3510 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -1,11 +1,14 @@ package common import ( + "bufio" "fmt" + "io/ioutil" "os" "os/exec" "regexp" "strings" + "unicode" sh "github.com/codeskyblue/go-sh" ) @@ -43,42 +46,24 @@ func (sc *ShellCmd) Execute() bool { sc.Command.Stdout = os.Stdout sc.Command.Stderr = os.Stderr } - err := sc.Command.Run() - if err != nil { + if err := sc.Command.Run(); err != nil { return false } return true } -// VerifyAppName verifies app name format and app existence" -func VerifyAppName(appName string) (err error) { - dokkuRoot := MustGetEnv("DOKKU_ROOT") - appRoot := strings.Join([]string{dokkuRoot, appName}, "/") - _, err = os.Stat(appRoot) - if os.IsNotExist(err) { - return fmt.Errorf("App %s does not exist: %v\n", appName, err) +// Output is a lightweight wrapper around exec.Command.Output() +func (sc *ShellCmd) Output() ([]byte, error) { + env := os.Environ() + for k, v := range sc.Env { + env = append(env, fmt.Sprintf("%s=%s", k, v)) } - r, _ := regexp.Compile("^[a-z].*") - if !r.MatchString(appName) { - return fmt.Errorf("App name (%s) must begin with lowercase alphanumeric character\n", appName) + sc.Command.Env = env + if sc.ShowOutput { + sc.Command.Stdout = os.Stdout + sc.Command.Stderr = os.Stderr } - return err -} - -// MustGetEnv returns env variable or fails if it's not set -func MustGetEnv(key string) string { - value := os.Getenv(key) - if value == "" { - LogFail(fmt.Sprintf("%s not set!", key)) - } - return value -} - -// LogFail is the failure log formatter -// prints text to stderr and exits with status 1 -func LogFail(text string) { - fmt.Fprintln(os.Stderr, fmt.Sprintf("FAILED: %s", text)) - os.Exit(1) + return sc.Command.Output() } // GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented @@ -130,12 +115,231 @@ func GetAppImageRepo(appName string) string { return strings.Join([]string{"dokku", appName}, "/") } +// ContainerIsRunning checks to see if a container is running +func ContainerIsRunning(containerID string) bool { + b, err := DockerInspect(containerID, "'{{.State.Running}}'") + if err != nil { + return false + } + return strings.TrimSpace(string(b[:])) == "true" +} + +// DirectoryExists returns if a path exists and is a directory +func DirectoryExists(filePath string) bool { + fi, err := os.Stat(filePath) + if err != nil { + return false + } + + return fi.IsDir() +} + +// DockerInspect runs an inspect command with a given format against a container id +func DockerInspect(containerID, format string) (output string, err error) { + b, err := sh.Command("docker", "inspect", "--format", format, containerID).Output() + if err != nil { + return "", err + } + output = strings.TrimSpace(string(b[:])) + if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") { + output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'") + } + return +} + +// DokkuApps returns a list of all local apps +func DokkuApps() (apps []string, err error) { + dokkuRoot := MustGetEnv("DOKKU_ROOT") + files, err := ioutil.ReadDir(dokkuRoot) + if err != nil { + err = fmt.Errorf("You haven't deployed any applications yet") + return + } + + for _, f := range files { + appRoot := strings.Join([]string{dokkuRoot, f.Name()}, "/") + if !DirectoryExists(appRoot) { + continue + } + if f.Name() == "tls" || strings.HasPrefix(f.Name(), ".") { + continue + } + apps = append(apps, f.Name()) + } + + if len(apps) == 0 { + err = fmt.Errorf("You haven't deployed any applications yet") + return + } + + return +} + +// FileToSlice reads in all the lines from a file into a string slice +func FileToSlice(filePath string) (lines []string, err error) { + f, err := os.Open(filePath) + if err != nil { + return + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + if text == "" { + continue + } + lines = append(lines, text) + } + err = scanner.Err() + return +} + +// FileExists returns if a path exists and is a file +func FileExists(filePath string) bool { + fi, err := os.Stat(filePath) + if err != nil { + return false + } + + return fi.Mode().IsRegular() +} + +// GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented +func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { + err := VerifyAppName(appName) + if err != nil { + LogFail(err.Error()) + } + + if imageRepo == "" { + imageRepo = GetAppImageRepo(appName) + } + + if imageTag == "" { + imageName = fmt.Sprintf("%v:latest", imageRepo) + } else { + imageName = fmt.Sprintf("%v:%v", imageRepo, imageTag) + if !VerifyImage(imageName) { + LogFail(fmt.Sprintf("app image (%s) not found", imageName)) + } + } + return +} + +// IsDeployed returns true if given app has a running container +func IsDeployed(appName string) bool { + dokkuRoot := MustGetEnv("DOKKU_ROOT") + appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + files, err := ioutil.ReadDir(appRoot) + if err != nil { + return false + } + + for _, f := range files { + if f.Name() == "CONTAINER" || strings.HasPrefix(f.Name(), "CONTAINER.") { + return true + } + } + return false +} + +// IsImageHerokuishBased returns true if app image is based on herokuish +func IsImageHerokuishBased(image string) bool { + // circleci can't support --rm as they run lxc in lxc + dockerArgs := "" + if !FileExists("/home/ubuntu/.circlerc") { + dockerArgs = "--rm" + } + + dockerGlobalArgs := os.Getenv("DOKKU_GLOBAL_RUN_ARGS") + parts := []string{"docker", "run", dockerGlobalArgs, "--entrypoint=\"/bin/sh\"", dockerArgs, image, "-c", "\"test -f /exec\""} + + var dockerCmdParts []string + for _, str := range parts { + if str != "" { + dockerCmdParts = append(dockerCmdParts, str) + } + } + + dockerCmd := NewShellCmd(strings.Join(dockerCmdParts, " ")) + dockerCmd.ShowOutput = false + return dockerCmd.Execute() +} + +// MustGetEnv returns env variable or fails if it's not set +func MustGetEnv(key string) (val string) { + val = os.Getenv(key) + if val == "" { + LogFail(fmt.Sprintf("%s not set!", key)) + } + return +} + +// ReadFirstLine gets the first line of a file that has contents and returns it +// if there are no contents, an empty string is returned +// will also return an empty string if the file does not exist +func ReadFirstLine(filename string) (text string) { + if !FileExists(filename) { + return + } + f, err := os.Open(filename) + if err != nil { + return + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if text = strings.TrimSpace(scanner.Text()); text == "" { + continue + } + return + } + return +} + +// StripInlineComments removes bash-style comment from input line +func StripInlineComments(text string) string { + bytes := []byte(text) + re := regexp.MustCompile("(?s)#.*") + bytes = re.ReplaceAll(bytes, nil) + return strings.TrimSpace(string(bytes)) +} + +// ToBool returns a bool value for a given string +func ToBool(s string) bool { + return s == "true" +} + +// UcFirst uppercases the first character in a string +func UcFirst(str string) string { + for i, v := range str { + return string(unicode.ToUpper(v)) + str[i+1:] + } + return "" +} + +// VerifyAppName verifies app name format and app existence" +func VerifyAppName(appName string) (err error) { + if appName == "" { + return fmt.Errorf("App name must not be null") + } + dokkuRoot := MustGetEnv("DOKKU_ROOT") + appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + if !DirectoryExists(appRoot) { + return fmt.Errorf("app %s does not exist: %v", appName, err) + } + r, _ := regexp.Compile("^[a-z].*") + if !r.MatchString(appName) { + return fmt.Errorf("app name (%s) must begin with lowercase alphanumeric character", appName) + } + return err +} + // VerifyImage returns true if docker image exists in local repo func VerifyImage(image string) bool { imageCmd := NewShellCmd(strings.Join([]string{"docker inspect", image}, " ")) imageCmd.ShowOutput = false - if imageCmd.Execute() { - return true - } - return false + return imageCmd.Execute() } diff --git a/plugins/common/common_test.go b/plugins/common/common_test.go index 9d9a9a5b1..535ba0f01 100644 --- a/plugins/common/common_test.go +++ b/plugins/common/common_test.go @@ -1,38 +1,106 @@ package common import ( + "io/ioutil" "os" + "strings" "testing" . "github.com/onsi/gomega" ) -func TestGetEnv(t *testing.T) { +var ( + testAppName = "test-app-1" + testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "") + testEnvFile = strings.Join([]string{testAppDir, "/ENV"}, "") + testEnvLine = "export testKey=TESTING" +) + +func setupTestApp() (err error) { + Expect(os.MkdirAll(testAppDir, 0644)).To(Succeed()) + b := []byte(testEnvLine + "\n") + if err = ioutil.WriteFile(testEnvFile, b, 0644); err != nil { + return + } + return +} + +func teardownTestApp() { + os.RemoveAll(testAppDir) +} + +func TestCommonGetEnv(t *testing.T) { RegisterTestingT(t) Expect(MustGetEnv("DOKKU_ROOT")).To(Equal("/home/dokku")) } -func TestGetAppImageRepo(t *testing.T) { +func TestCommonGetAppImageRepo(t *testing.T) { RegisterTestingT(t) Expect(GetAppImageRepo("testapp")).To(Equal("dokku/testapp")) } -func TestVerifyImageInvalid(t *testing.T) { +func TestCommonVerifyImageInvalid(t *testing.T) { RegisterTestingT(t) Expect(VerifyImage("testapp")).To(Equal(false)) } -func TestVerifyAppNameInvalid(t *testing.T) { +func TestCommonVerifyAppNameInvalid(t *testing.T) { RegisterTestingT(t) err := VerifyAppName("1994testApp") Expect(err).To(HaveOccurred()) } -func TestVerifyAppName(t *testing.T) { +func TestCommonVerifyAppName(t *testing.T) { RegisterTestingT(t) - dir := "/home/dokku/testApp" - os.MkdirAll(dir, 0644) - err := VerifyAppName("testApp") - Expect(err).NotTo(HaveOccurred()) - os.RemoveAll(dir) + Expect(setupTestApp()).To(Succeed()) + Expect(VerifyAppName(testAppName)).To(Succeed()) + teardownTestApp() +} + +func TestCommonDokkuAppsError(t *testing.T) { + RegisterTestingT(t) + _, err := DokkuApps() + Expect(err).To(HaveOccurred()) +} + +func TestCommonDokkuApps(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + apps, err := DokkuApps() + Expect(err).NotTo(HaveOccurred()) + Expect(apps).To(HaveLen(1)) + Expect(apps[0]).To(Equal(testAppName)) + teardownTestApp() +} + +func TestCommonFileToSlice(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + lines, err := FileToSlice(testEnvFile) + Expect(err).NotTo(HaveOccurred()) + Expect(lines).To(Equal([]string{testEnvLine})) + teardownTestApp() +} + +func TestCommonFileExists(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + Expect(FileExists(testEnvFile)).To(BeTrue()) + teardownTestApp() +} + +func TestCommonReadFirstLine(t *testing.T) { + RegisterTestingT(t) + line := ReadFirstLine(testEnvFile) + Expect(line).To(Equal("")) + Expect(setupTestApp()).To(Succeed()) + line = ReadFirstLine(testEnvFile) + Expect(line).To(Equal(testEnvLine)) + teardownTestApp() +} + +func TestCommonStripInlineComments(t *testing.T) { + RegisterTestingT(t) + text := StripInlineComments(strings.Join([]string{testEnvLine, "# testing comment"}, " ")) + Expect(text).To(Equal(testEnvLine)) } diff --git a/plugins/common/functions b/plugins/common/functions index c9f28250a..07563d647 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -440,6 +440,7 @@ dokku_build() { source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$1"; local IMAGE_SOURCE_TYPE="$2"; local TMP_WORK_DIR="$3"; local IMAGE=$(get_app_image_name "$APP") + local cid verify_app_name "$APP" local CACHE_DIR="$DOKKU_ROOT/$APP/cache" @@ -450,19 +451,19 @@ dokku_build() { case "$IMAGE_SOURCE_TYPE" in herokuish) DOKKU_IMAGE="$(config_get "$APP" DOKKU_IMAGE || echo "$DOKKU_IMAGE")" - local id=$(tar -c . | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$DOKKU_IMAGE" /bin/bash -c "mkdir -p /app && tar -xC /app") - test "$(docker wait "$id")" -eq 0 - docker commit "$id" "$IMAGE" > /dev/null + cid=$(tar -c . | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$DOKKU_IMAGE" /bin/bash -c "mkdir -p /app && tar -xC /app") + test "$(docker wait "$cid")" -eq 0 + docker commit "$cid" "$IMAGE" > /dev/null [[ -d $CACHE_DIR ]] || mkdir -p "$CACHE_DIR" plugn trigger pre-build-buildpack "$APP" local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE") [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true " # shellcheck disable=SC2086 - local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -v $CACHE_DIR:/cache -e CACHE_PATH=/cache $DOCKER_ARGS $IMAGE /build) - docker attach "$id" - test "$(docker wait "$id")" -eq 0 - docker commit "$id" "$IMAGE" > /dev/null + cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -v $CACHE_DIR:/cache -e CACHE_PATH=/cache $DOCKER_ARGS $IMAGE /build) + docker attach "$cid" + test "$(docker wait "$cid")" -eq 0 + docker commit "$cid" "$IMAGE" > /dev/null plugn trigger post-build-buildpack "$APP" ;; @@ -501,20 +502,21 @@ dokku_release() { source "$PLUGIN_AVAILABLE_PATH/config/functions" local APP="$1"; local IMAGE_SOURCE_TYPE="$2"; local IMAGE_TAG="$3"; local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") + local cid verify_app_name "$APP" case "$IMAGE_SOURCE_TYPE" in herokuish) plugn trigger pre-release-buildpack "$APP" "$IMAGE_TAG" if [[ -n $(config_export global) ]]; then - local id=$(config_export global | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/00-global-env.sh") - test "$(docker wait "$id")" -eq 0 - docker commit "$id" "$IMAGE" > /dev/null + cid=$(config_export global | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/00-global-env.sh") + test "$(docker wait "$cid")" -eq 0 + docker commit "$cid" "$IMAGE" > /dev/null fi if [[ -n $(config_export app "$APP") ]]; then - local id=$(config_export app "$APP" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/01-app-env.sh") - test "$(docker wait "$id")" -eq 0 - docker commit "$id" "$IMAGE" > /dev/null + cid=$(config_export app "$APP" | docker run "$DOKKU_GLOBAL_RUN_ARGS" -i -a stdin "$IMAGE" /bin/bash -c "mkdir -p /app/.profile.d && cat > /app/.profile.d/01-app-env.sh") + test "$(docker wait "$cid")" -eq 0 + docker commit "$cid" "$IMAGE" > /dev/null fi plugn trigger post-release-buildpack "$APP" "$IMAGE_TAG" ;; @@ -536,19 +538,21 @@ dokku_deploy_cmd() { local cmd="deploy" source "$PLUGIN_AVAILABLE_PATH/checks/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" - source "$PLUGIN_AVAILABLE_PATH/proxy/functions" [[ -z $1 ]] && dokku_log_fail "Please specify an app to run the command on" - local APP="$1"; local IMAGE_TAG="$2"; local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG") + local APP="$1" IMAGE_TAG="$2" + local DOKKU_DOCKER_STOP_TIMEOUT DOKKU_HEROKUISH DOKKU_NETWORK_BIND_ALL IMAGE + DOKKU_HEROKUISH=false + IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG") verify_app_name "$APP" plugn trigger pre-deploy "$APP" "$IMAGE_TAG" - is_image_herokuish_based "$IMAGE" && local DOKKU_HEROKUISH=true + is_image_herokuish_based "$IMAGE" && DOKKU_HEROKUISH=true local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE" local oldids=$(get_app_container_ids "$APP") - local DOKKU_IS_APP_PROXY_ENABLED="$(is_app_proxy_enabled "$APP")" - local DOKKU_DOCKER_STOP_TIMEOUT="$(config_get "$APP" DOKKU_DOCKER_STOP_TIMEOUT || true)" + DOKKU_NETWORK_BIND_ALL="$(plugn trigger network-get-property "$APP" bind-all-interfaces)" + DOKKU_DOCKER_STOP_TIMEOUT="$(config_get "$APP" DOKKU_DOCKER_STOP_TIMEOUT || true)" [[ $DOKKU_DOCKER_STOP_TIMEOUT ]] && DOCKER_STOP_TIME_ARG="--time=${DOKKU_DOCKER_STOP_TIMEOUT}" local line; local PROC_TYPE; local PROC_COUNT; local CONTAINER_INDEX @@ -572,10 +576,8 @@ dokku_deploy_cmd() { fi while [[ $CONTAINER_INDEX -le $PROC_COUNT ]]; do - local id=""; local port=""; local ipaddr="" + local cid=""; local port=""; local ipaddr="" local DOKKU_CONTAINER_ID_FILE="$DOKKU_ROOT/$APP/CONTAINER.$PROC_TYPE.$CONTAINER_INDEX" - local DOKKU_IP_FILE="$DOKKU_ROOT/$APP/IP.$PROC_TYPE.$CONTAINER_INDEX" - local DOKKU_PORT_FILE="$DOKKU_ROOT/$APP/PORT.$PROC_TYPE.$CONTAINER_INDEX" # start the app local DOCKER_ARGS @@ -583,69 +585,60 @@ dokku_deploy_cmd() { DOCKER_ARGS+=" -e DYNO=$PROC_TYPE.$CONTAINER_INDEX " [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true " - [[ -n "$DOKKU_HEROKUISH" ]] && local START_CMD="/start $PROC_TYPE" + [[ "$DOKKU_HEROKUISH" == "true" ]] && local START_CMD="/start $PROC_TYPE" - if [[ -z "$DOKKU_HEROKUISH" ]]; then - local DOKKU_DOCKERFILE_PORTS=($(config_get "$APP" DOKKU_DOCKERFILE_PORTS || true)) + if [[ "$DOKKU_HEROKUISH" == "false" ]]; then local DOKKU_DOCKERFILE_START_CMD=$(config_get "$APP" DOKKU_DOCKERFILE_START_CMD || true) local DOKKU_PROCFILE_START_CMD=$(get_cmd_from_procfile "$APP" "$PROC_TYPE") local START_CMD=${DOKKU_DOCKERFILE_START_CMD:-$DOKKU_PROCFILE_START_CMD} fi if [[ "$PROC_TYPE" == "web" ]]; then - if [[ -z "${DOKKU_DOCKERFILE_PORTS[*]}" ]]; then - local port=5000 - local DOKKU_DOCKER_PORT_ARGS+="-p $port" - else - local p - for p in ${DOKKU_DOCKERFILE_PORTS[*]};do - if [[ ! "$p" =~ .*udp.* ]]; then - # set port to first non-udp port - local p=${p//\/tcp} - local port=${port:="$p"} - fi - local DOKKU_DOCKER_PORT_ARGS+=" -p $p " - done - fi - if [[ "$DOKKU_IS_APP_PROXY_ENABLED" == "true" ]]; then - # shellcheck disable=SC2086 - local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d -e PORT=$port $DOCKER_ARGS $IMAGE $START_CMD) - local ipaddr=$(docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$id") - # Docker < 1.9 compatibility - if [[ -z $ipaddr ]]; then - local ipaddr=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}' "$id") + ports=($(plugn trigger network-compute-ports "$APP" "$PROC_TYPE" "$DOKKU_HEROKUISH")) + local DOKKU_DOCKER_PORT_ARGS="" + local DOKKU_PORT="" + for p in "${ports[@]}"; do + if [[ ! "$p" =~ .*udp.* ]]; then + DOKKU_PORT=${DOKKU_PORT:="$p"} fi + DOKKU_DOCKER_PORT_ARGS+=" -p $p " + done + + 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) else # shellcheck disable=SC2086 - local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOKKU_DOCKER_PORT_ARGS -e PORT=$port $DOCKER_ARGS $IMAGE $START_CMD) - local port=$(docker port "$id" "$port" | sed 's/[0-9.]*://') - local ipaddr=127.0.0.1 + cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOKKU_DOCKER_PORT_ARGS -e PORT=$DOKKU_PORT $DOCKER_ARGS $IMAGE $START_CMD) fi else # shellcheck disable=SC2086 - local id=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOCKER_ARGS $IMAGE $START_CMD) + cid=$(docker run $DOKKU_GLOBAL_RUN_ARGS -d $DOCKER_ARGS $IMAGE $START_CMD) fi + ipaddr=$(plugn trigger network-get-ipaddr "$APP" "$PROC_TYPE" "$cid") + port=$(plugn trigger network-get-port "$APP" "$PROC_TYPE" "$DOKKU_HEROKUISH" "$cid") + kill_new() { declare desc="wrapper function to kill newly started app container" - local id="$1" - docker inspect "$id" &> /dev/null && docker stop "$id" > /dev/null && docker kill "$id" &> /dev/null + local cid="$1" + docker inspect "$cid" &> /dev/null && docker stop "$cid" > /dev/null && docker kill "$cid" &> /dev/null trap - INT TERM EXIT kill -9 $$ } # run checks first, then post-deploy hooks, which switches proxy traffic - trap 'kill_new $id' INT TERM EXIT + trap 'kill_new $cid' INT TERM EXIT if [[ "$(is_app_proctype_checks_disabled "$APP" "$PROC_TYPE")" == "false" ]]; then dokku_log_info1 "Attempting pre-flight checks" - plugn trigger check-deploy "$APP" "$id" "$PROC_TYPE" "$port" "$ipaddr" + plugn trigger check-deploy "$APP" "$cid" "$PROC_TYPE" "$port" "$ipaddr" fi trap - INT TERM EXIT # now using the new container - [[ -n "$id" ]] && echo "$id" > "$DOKKU_CONTAINER_ID_FILE" - [[ -n "$ipaddr" ]] && echo "$ipaddr" > "$DOKKU_IP_FILE" - [[ -n "$port" ]] && echo "$port" > "$DOKKU_PORT_FILE" + [[ -n "$cid" ]] && echo "$cid" > "$DOKKU_CONTAINER_ID_FILE" + [[ -n "$ipaddr" ]] && plugn trigger network-write-ipaddr "$APP" "$PROC_TYPE" "$CONTAINER_INDEX" "$ipaddr" + [[ -n "$port" ]] && plugn trigger network-write-port "$APP" "$PROC_TYPE" "$CONTAINER_INDEX" "$port" # cleanup pre-migration files rm -f "$DOKKU_ROOT/$APP/CONTAINER" "$DOKKU_ROOT/$APP/IP" "$DOKKU_ROOT/$APP/PORT" @@ -1046,9 +1039,11 @@ get_app_urls() { echo "$(< "$DOKKU_ROOT/HOSTNAME"):$app_port (container)" done else - shopt -s nullglob - for PORT_FILE in $DOKKU_ROOT/$APP/PORT.*; do - echo "$SCHEME://$(< "$DOKKU_ROOT/HOSTNAME"):$(< "$PORT_FILE") (container)" + local DOKKU_APP_LISTENERS PORT + DOKKU_APP_LISTENERS="$(plugn trigger network-get-listeners "$APP" | xargs)" + for DOKKU_APP_LISTENER in $DOKKU_APP_LISTENERS; do + PORT="$(echo "$DOKKU_APP_LISTENER" | cut -d ':' -f2)" + echo "$SCHEME://$(< "$DOKKU_ROOT/HOSTNAME"):$PORT (container)" done shopt -u nullglob fi diff --git a/plugins/common/log.go b/plugins/common/log.go new file mode 100644 index 000000000..dd3992e83 --- /dev/null +++ b/plugins/common/log.go @@ -0,0 +1,56 @@ +package common + +import ( + "fmt" + "os" +) + +// LogFail is the failure log formatter +// prints text to stderr and exits with status 1 +func LogFail(text string) { + fmt.Fprintln(os.Stderr, fmt.Sprintf("FAILED: %s", text)) + os.Exit(1) +} + +// LogInfo1 is the info1 header formatter +func LogInfo1(text string) { + fmt.Fprintln(os.Stdout, fmt.Sprintf("-----> %s", text)) +} + +// LogInfo1Quiet is the info1 header formatter (with quiet option) +func LogInfo1Quiet(text string) { + if os.Getenv("DOKKU_QUIET_OUTPUT") != "" { + LogInfo1(text) + } +} + +// LogInfo2 is the info2 header formatter +func LogInfo2(text string) { + fmt.Fprintln(os.Stdout, fmt.Sprintf("=====> %s", text)) +} + +// LogInfo2Quiet is the info2 header formatter (with quiet option) +func LogInfo2Quiet(text string) { + if os.Getenv("DOKKU_QUIET_OUTPUT") == "" { + LogInfo2(text) + } +} + +// LogVerbose is the verbose log formatter +// prints indented text to stdout +func LogVerbose(text string) { + fmt.Fprintln(os.Stdout, fmt.Sprintf(" %s", text)) +} + +// LogVerboseQuiet is the verbose log formatter +// prints indented text to stdout (with quiet option) +func LogVerboseQuiet(text string) { + if os.Getenv("DOKKU_QUIET_OUTPUT") != "" { + LogVerbose(text) + } +} + +// LogWarn is the warning log formatter +func LogWarn(text string) { + fmt.Fprintln(os.Stderr, fmt.Sprintf(" ! %s", text)) +} diff --git a/plugins/common/properties.go b/plugins/common/properties.go new file mode 100644 index 000000000..88717fcc2 --- /dev/null +++ b/plugins/common/properties.go @@ -0,0 +1,164 @@ +package common + +import ( + "fmt" + "io/ioutil" + "os" + "os/user" + "reflect" + "strconv" + "strings" +) + +// CommandPropertySet is a generic function that will set a property for a given plugin/app combination +func CommandPropertySet(pluginName, appName, property, value string, properties map[string]string) { + if err := VerifyAppName(appName); err != nil { + LogFail(err.Error()) + } + if property == "" { + LogFail("No property specified") + } + + if _, ok := properties[property]; !ok { + properties := reflect.ValueOf(properties).MapKeys() + validPropertyList := make([]string, len(properties)) + for i := 0; i < len(properties); i++ { + validPropertyList[i] = properties[i].String() + } + + LogFail(fmt.Sprintf("Invalid property specified, valid properties include: %s", strings.Join(validPropertyList, ", "))) + } + + if value != "" { + LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", property, value)) + PropertyWrite(pluginName, appName, property, value) + } else { + LogInfo2Quiet(fmt.Sprintf("Unsetting %s", property)) + PropertyDelete(pluginName, appName, property) + } +} + +// PropertyDelete deletes a property from the plugin properties for an app +func PropertyDelete(pluginName string, appName string, property string) { + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/") + if err := os.Remove(propertyPath); err != nil { + LogFail(fmt.Sprintf("Unable to remove %s property %s.%s", pluginName, appName, property)) + } +} + +// PropertyDestroy destroys the plugin properties for an app +func PropertyDestroy(pluginName string, appName string) { + if appName == "_all_" { + pluginConfigPath := getPluginConfigPath(pluginName) + os.RemoveAll(pluginConfigPath) + } else { + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + os.RemoveAll(pluginAppConfigRoot) + } +} + +// PropertyExists returns whether a property exists or not +func PropertyExists(pluginName string, appName string, property string) bool { + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/") + _, err := os.Stat(propertyPath) + return !os.IsNotExist(err) +} + +// PropertyGet returns the value for a given property +func PropertyGet(pluginName string, appName string, property string) string { + return PropertyGetDefault(pluginName, appName, property, "") +} + +// PropertyGetDefault returns the value for a given property with a specified default value +func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val string) { + if !PropertyExists(pluginName, appName, property) { + return + } + + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/") + + b, err := ioutil.ReadFile(propertyPath) + if err != nil { + LogWarn(fmt.Sprintf("Unable to read %s property %s.%s", pluginName, appName, property)) + return + } + val = string(b) + return +} + +// PropertyWrite writes a value for a given application property +func PropertyWrite(pluginName string, appName string, property string, value string) { + if err := makePropertyPath(pluginName, appName); err != nil { + LogFail(fmt.Sprintf("Unable to create %s config directory for %s: %s", pluginName, appName, err.Error())) + } + + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + propertyPath := strings.Join([]string{pluginAppConfigRoot, property}, "/") + file, err := os.Create(propertyPath) + if err != nil { + LogFail(fmt.Sprintf("Unable to write %s config value %s.%s: %s", pluginName, appName, property, err.Error())) + } + defer file.Close() + + fmt.Fprintf(file, value) + file.Chmod(0600) + setPermissions(propertyPath, 0600) +} + +// PropertySetup creates the plugin config root +func PropertySetup(pluginName string) (err error) { + pluginConfigRoot := getPluginConfigPath(pluginName) + if err = os.MkdirAll(pluginConfigRoot, 0755); err != nil { + return + } + return setPermissions(pluginConfigRoot, 0755) +} + +// getPluginAppPropertyPath returns the plugin property path for a given plugin/app combination +func getPluginAppPropertyPath(pluginName string, appName string) string { + return strings.Join([]string{getPluginConfigPath(pluginName), appName}, "/") +} + +// getPluginConfigPath returns the plugin property path for a given plugin +func getPluginConfigPath(pluginName string) string { + return strings.Join([]string{MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName}, "/") +} + +// makePropertyPath ensures that a property path exists +func makePropertyPath(pluginName string, appName string) (err error) { + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + if err = os.MkdirAll(pluginAppConfigRoot, 0755); err != nil { + return + } + return setPermissions(pluginAppConfigRoot, 0755) +} + +// setPermissions sets the proper owner and filemode for a given file +func setPermissions(path string, fileMode os.FileMode) (err error) { + if err = os.Chmod(path, fileMode); err != nil { + return err + } + + group, err := user.LookupGroup("dokku") + if err != nil { + return + } + user, err := user.Lookup("dokku") + if err != nil { + return + } + + uid, err := strconv.Atoi(user.Uid) + if err != nil { + return + } + + gid, err := strconv.Atoi(group.Gid) + if err != nil { + return + } + return os.Chown(path, uid, gid) +} diff --git a/plugins/config/config.go b/plugins/config/config.go new file mode 100644 index 000000000..328cccaca --- /dev/null +++ b/plugins/config/config.go @@ -0,0 +1,30 @@ +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 + + envFile := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName, "ENV"}, "/") + lines, err := common.FileToSlice(envFile) + 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, "'"), "'") + } + } + return +} diff --git a/plugins/config/config_test.go b/plugins/config/config_test.go new file mode 100644 index 000000000..dbe660065 --- /dev/null +++ b/plugins/config/config_test.go @@ -0,0 +1,36 @@ +package config + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +var ( + testAppName = "test-app-1" + testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "") +) + +func setupTestApp() (err error) { + Expect(os.MkdirAll(testAppDir, 0644)).To(Succeed()) + b := []byte("export testKey=TESTING\n") + if err = ioutil.WriteFile(strings.Join([]string{testAppDir, "/ENV"}, ""), b, 0644); err != nil { + return + } + return +} + +func teardownTestApp() { + os.RemoveAll(testAppDir) +} + +func TestConfigGetWithDefault(t *testing.T) { + RegisterTestingT(t) + Expect(setupTestApp()).To(Succeed()) + Expect(GetWithDefault(testAppName, "unknownKey", "UNKNOWN")).To(Equal("UNKNOWN")) + Expect(GetWithDefault(testAppName, "testKey", "testKey")).To(Equal("TESTING")) + teardownTestApp() +} diff --git a/plugins/network/.gitignore b/plugins/network/.gitignore new file mode 100644 index 000000000..14fca6773 --- /dev/null +++ b/plugins/network/.gitignore @@ -0,0 +1,6 @@ +/commands +/subcommands/* +/network-* +/install +/post-create +/post-delete diff --git a/plugins/network/Makefile b/plugins/network/Makefile new file mode 100644 index 000000000..c3017156f --- /dev/null +++ b/plugins/network/Makefile @@ -0,0 +1,68 @@ +include ../../common.mk + +build-in-docker: clean + docker run --rm \ + -v $$PWD/../..:$(GO_REPO_ROOT) \ + -w $(GO_REPO_ROOT)/plugins/network \ + $(BUILD_IMAGE) \ + bash -c "make build" || exit $$? + +build: commands subcommands triggers +subcommands: subcommands/rebuild subcommands/rebuildall subcommands/report subcommands/set +triggers: install network-build-config network-compute-ports network-config-exists network-get-ipaddr network-get-listeners network-get-port network-get-property network-write-ipaddr network-write-port post-create post-delete +commands: **/**/commands.go + go build -a -o commands src/commands/commands.go + +subcommands/rebuild: **/**/**/rebuild.go + go build -a -o subcommands/rebuild src/subcommands/rebuild/rebuild.go + +subcommands/rebuildall: **/**/**/rebuildall.go + go build -a -o subcommands/rebuildall src/subcommands/rebuildall/rebuildall.go + +subcommands/report: **/**/**/report.go + go build -a -o subcommands/report src/subcommands/report/report.go + +subcommands/set: **/**/**/set.go + go build -a -o subcommands/set src/subcommands/set/set.go + +install: **/**/**/install.go + go build -a -o install src/triggers/install/install.go + +network-build-config: **/**/**/network-build-config.go + go build -a -o network-build-config src/triggers/network-build-config/network-build-config.go + +network-compute-ports: **/**/**/network-compute-ports.go + go build -a -o network-compute-ports src/triggers/network-compute-ports/network-compute-ports.go + +network-config-exists: **/**/**/network-config-exists.go + go build -a -o network-config-exists src/triggers/network-config-exists/network-config-exists.go + +network-get-ipaddr: **/**/**/network-get-ipaddr.go + go build -a -o network-get-ipaddr src/triggers/network-get-ipaddr/network-get-ipaddr.go + +network-get-listeners: **/**/**/network-get-listeners.go + go build -a -o network-get-listeners src/triggers/network-get-listeners/network-get-listeners.go + +network-get-port: **/**/**/network-get-port.go + go build -a -o network-get-port src/triggers/network-get-port/network-get-port.go + +network-get-property: **/**/**/network-get-property.go + go build -a -o network-get-property src/triggers/network-get-property/network-get-property.go + +network-write-ipaddr: **/**/**/network-write-ipaddr.go + go build -a -o network-write-ipaddr src/triggers/network-write-ipaddr/network-write-ipaddr.go + +network-write-port: **/**/**/network-write-port.go + go build -a -o network-write-port src/triggers/network-write-port/network-write-port.go + +post-create: **/**/**/post-create.go + go build -a -o post-create src/triggers/post-create/post-create.go + +post-delete: **/**/**/post-delete.go + go build -a -o post-delete src/triggers/post-delete/post-delete.go + +clean: + rm -rf commands subcommands network-* install post-create post-delete + +src-clean: + rm -rf .gitignore src vendor Makefile diff --git a/plugins/network/glide.yaml b/plugins/network/glide.yaml new file mode 100644 index 000000000..a720d6c11 --- /dev/null +++ b/plugins/network/glide.yaml @@ -0,0 +1,4 @@ +package: . +import: +- package: github.com/codeskyblue/go-sh +- package: github.com/ryanuber/columnize diff --git a/plugins/network/network.go b/plugins/network/network.go new file mode 100644 index 000000000..08a8d21e1 --- /dev/null +++ b/plugins/network/network.go @@ -0,0 +1,176 @@ +package network + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/config" + + sh "github.com/codeskyblue/go-sh" +) + +var ( + // DefaultProperties is a map of all valid network properties with corresponding default property values + DefaultProperties = map[string]string{ + "bind-all-interfaces": "false", + } +) + +// BuildConfig builds network config files +func BuildConfig(appName string) { + if err := common.VerifyAppName(appName); err != nil { + common.LogFail(err.Error()) + } + if !common.IsDeployed(appName) { + return + } + appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + scaleFile := strings.Join([]string{appRoot, "DOKKU_SCALE"}, "/") + if !common.FileExists(scaleFile) { + return + } + + image := common.GetAppImageName(appName, "", "") + isHerokuishContainer := common.IsImageHerokuishBased(image) + common.LogInfo1(fmt.Sprintf("Ensuring network configuration is in sync for %s", appName)) + lines, err := common.FileToSlice(scaleFile) + if err != nil { + return + } + for _, line := range lines { + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + procParts := strings.SplitN(line, "=", 2) + if len(procParts) != 2 { + continue + } + procType := procParts[0] + procCount, err := strconv.Atoi(procParts[1]) + if err != nil { + continue + } + + containerIndex := 0 + for containerIndex < procCount { + containerIndex++ + containerIndexString := strconv.Itoa(containerIndex) + containerIDFile := fmt.Sprintf("%v/CONTAINER.%v.%v", appRoot, procType, containerIndex) + + containerID := common.ReadFirstLine(containerIDFile) + if containerID == "" || !common.ContainerIsRunning(containerID) { + continue + } + + ipAddress := GetContainerIpaddress(appName, procType, containerID) + port := GetContainerPort(appName, procType, isHerokuishContainer, containerID) + + if ipAddress != "" { + _, err := sh.Command("plugn", "trigger", "network-write-ipaddr", appName, procType, containerIndexString, ipAddress).Output() + if err != nil { + common.LogWarn(err.Error()) + } + } + + if port != "" { + _, err := sh.Command("plugn", "trigger", "network-write-port", appName, procType, containerIndexString, port).Output() + if err != nil { + common.LogWarn(err.Error()) + } + } + } + } +} + +// GetContainerIpaddress returns the ipaddr for a given app container +func GetContainerIpaddress(appName, procType, containerID string) (ipAddr string) { + if procType != "web" { + return + } + + b, err := common.DockerInspect(containerID, "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'") + if err != nil || len(b) == 0 { + // docker < 1.9 compatibility + b, err = common.DockerInspect(containerID, "'{{ .NetworkSettings.IPAddress }}'") + } + + if err == nil { + return string(b[:]) + } + + return +} + +// GetContainerPort returns the port for a given app container +func GetContainerPort(appName, procType string, isHerokuishContainer bool, containerID string) (port string) { + if procType != "web" { + return + } + + dockerfilePorts := make([]string, 0) + if !isHerokuishContainer { + configValue := config.GetWithDefault(appName, "DOKKU_DOCKERFILE_PORTS", "") + if configValue != "" { + dockerfilePorts = strings.Split(configValue, " ") + } + } + + if len(dockerfilePorts) > 0 { + for _, p := range dockerfilePorts { + if strings.HasSuffix(p, "/udp") { + continue + } + port = strings.TrimSuffix(p, "/tcp") + if port != "" { + break + } + } + b, err := sh.Command("docker", "port", containerID, port).Output() + if err == nil { + port = strings.Split(string(b[:]), ":")[1] + } + } else { + port = "5000" + } + + return +} + +// GetDefaultValue returns the default value for a given property +func GetDefaultValue(property string) (value string) { + value, ok := DefaultProperties[property] + if ok { + return + } + return +} + +// GetListeners returns a string array of app listeners +func GetListeners(appName string) []string { + dokkuRoot := common.MustGetEnv("DOKKU_ROOT") + appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + + files, _ := filepath.Glob(appRoot + "/IP.web.*") + + var listeners []string + for _, ipfile := range files { + portfile := strings.Replace(ipfile, "/IP.web.", "/PORT.web.", 1) + ipAddress := common.ReadFirstLine(ipfile) + port := common.ReadFirstLine(portfile) + listeners = append(listeners, fmt.Sprintf("%s:%s", ipAddress, port)) + } + return listeners +} + +// HasNetworkConfig returns whether the network configuration for a given app exists +func HasNetworkConfig(appName string) bool { + appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + ipfile := fmt.Sprintf("%v/IP.web.1", appRoot) + portfile := fmt.Sprintf("%v/PORT.web.1", appRoot) + + return common.FileExists(ipfile) && common.FileExists(portfile) +} diff --git a/plugins/network/network_test.go b/plugins/network/network_test.go new file mode 100644 index 000000000..9aed75050 --- /dev/null +++ b/plugins/network/network_test.go @@ -0,0 +1,12 @@ +package network + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestNetworkGetDefaultValue(t *testing.T) { + RegisterTestingT(t) + Expect(GetDefaultValue("bind-all-interfaces")).To(Equal("false")) +} diff --git a/plugins/network/plugin.toml b/plugins/network/plugin.toml new file mode 100644 index 000000000..d6da0716a --- /dev/null +++ b/plugins/network/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core network plugin" +version = "0.9.4" +[plugin.config] diff --git a/plugins/network/src/commands/commands.go b/plugins/network/src/commands/commands.go new file mode 100644 index 000000000..61ec0efba --- /dev/null +++ b/plugins/network/src/commands/commands.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + columnize "github.com/ryanuber/columnize" +) + +const ( + helpHeader = `Usage: dokku network[:COMMAND] + +Manages network settings for an app + +Additional commands:` + + helpContent = ` + network:report [] [], Displays a network report for one or more apps + network:rebuild , Rebuilds network settings for an app + network:rebuildall, Rebuild network settings for all apps + network:set (), Set or clear a network property for an app +` +) + +func main() { + flag.Usage = usage + flag.Parse() + + cmd := flag.Arg(0) + switch cmd { + case "network", "network:help": + usage() + case "help": + fmt.Print(helpContent) + 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)) +} diff --git a/plugins/network/src/glide.lock b/plugins/network/src/glide.lock new file mode 100644 index 000000000..fe4a98f07 --- /dev/null +++ b/plugins/network/src/glide.lock @@ -0,0 +1,6 @@ +hash: 1ddab5de41d1514c2722bd7e24758ad4b60bf6956eb5b9b925fa071a1427f149 +updated: 2017-01-03T17:16:50.97156327-08:00 +imports: +- name: github.com/ryanuber/columnize + version: 0fbbb3f0e3fbdc5bae7c6cd5f6c1887ebfb76360 +testImports: [] diff --git a/plugins/network/src/glide.yaml b/plugins/network/src/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/network/src/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/network/src/subcommands/rebuild/rebuild.go b/plugins/network/src/subcommands/rebuild/rebuild.go new file mode 100644 index 000000000..b2d129532 --- /dev/null +++ b/plugins/network/src/subcommands/rebuild/rebuild.go @@ -0,0 +1,15 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/network" +) + +// rebuilds network settings for an app +func main() { + flag.Parse() + appName := flag.Arg(1) + + network.BuildConfig(appName) +} diff --git a/plugins/network/src/subcommands/rebuildall/rebuildall.go b/plugins/network/src/subcommands/rebuildall/rebuildall.go new file mode 100644 index 000000000..01c09c670 --- /dev/null +++ b/plugins/network/src/subcommands/rebuildall/rebuildall.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/network" +) + +// rebuilds network settings for all apps +func main() { + apps, err := common.DokkuApps() + if err != nil { + common.LogFail(err.Error()) + } + for _, appName := range apps { + network.BuildConfig(appName) + } +} diff --git a/plugins/network/src/subcommands/report/report.go b/plugins/network/src/subcommands/report/report.go new file mode 100644 index 000000000..382245cb8 --- /dev/null +++ b/plugins/network/src/subcommands/report/report.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "os" + "reflect" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/network" +) + +func reportSingleApp(appName, infoFlag string) { + infoFlags := map[string]string{ + "--network-bind-all-interfaces": common.PropertyGet("network", appName, "bind-all-interfaces"), + "--network-listeners": strings.Join(network.GetListeners(appName), " "), + } + + if len(infoFlag) == 0 { + common.LogInfo2Quiet(fmt.Sprintf("%s network information", appName)) + for k, v := range infoFlags { + key := common.UcFirst(strings.Replace(strings.TrimPrefix(k, "--"), "-", " ", -1)) + common.LogVerbose(fmt.Sprintf("%s: %s", key, v)) + } + return + } + + for k, v := range infoFlags { + if infoFlag == k { + fmt.Fprintln(os.Stdout, v) + return + } + } + + keys := reflect.ValueOf(infoFlags).MapKeys() + strkeys := make([]string, len(keys)) + for i := 0; i < len(keys); i++ { + strkeys[i] = keys[i].String() + } + common.LogFail(fmt.Sprintf("Invalid flag passed, valid flags: %s", strings.Join(strkeys, ", "))) +} + +// set or clear a network property for an app +func main() { + flag.Parse() + appName := flag.Arg(1) + infoFlag := flag.Arg(2) + + if strings.HasPrefix(appName, "--") { + infoFlag = appName + appName = "" + } + + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + return + } + for _, appName := range apps { + reportSingleApp(appName, infoFlag) + } + return + } + + reportSingleApp(appName, infoFlag) +} diff --git a/plugins/network/src/subcommands/set/set.go b/plugins/network/src/subcommands/set/set.go new file mode 100644 index 000000000..cd8eb73ef --- /dev/null +++ b/plugins/network/src/subcommands/set/set.go @@ -0,0 +1,22 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/network" +) + +// set or clear a network property for an app +func main() { + flag.Parse() + appName := flag.Arg(1) + property := flag.Arg(2) + value := flag.Arg(3) + + if property == "bind-all-interfaces" && value == "" { + value = "false" + } + + common.CommandPropertySet("network", appName, property, value, network.DefaultProperties) +} diff --git a/plugins/network/src/triggers/install/install.go b/plugins/network/src/triggers/install/install.go new file mode 100644 index 000000000..f78133512 --- /dev/null +++ b/plugins/network/src/triggers/install/install.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/proxy" +) + +// runs the install step for the network plugin +func main() { + if err := common.PropertySetup("network"); err != nil { + common.LogFail(fmt.Sprintf("Unable to install the network plugin: %s", err.Error())) + } + + apps, err := common.DokkuApps() + if err != nil { + return + } + for _, appName := range apps { + if common.PropertyExists("network", appName, "bind-all-interfaces") { + continue + } + if proxy.IsAppProxyEnabled(appName) { + common.LogVerboseQuiet("Setting %s network property 'bind-all-interfaces' to false") + common.PropertyWrite("network", appName, "bind-all-interfaces", "false") + } else { + common.LogVerboseQuiet("Setting %s network property 'bind-all-interfaces' to true") + common.PropertyWrite("network", appName, "bind-all-interfaces", "true") + } + } +} diff --git a/plugins/network/src/triggers/network-build-config/network-build-config.go b/plugins/network/src/triggers/network-build-config/network-build-config.go new file mode 100644 index 000000000..6c2bc2af8 --- /dev/null +++ b/plugins/network/src/triggers/network-build-config/network-build-config.go @@ -0,0 +1,15 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/network" +) + +// rebuilds network settings for an app +func main() { + flag.Parse() + appName := flag.Arg(0) + + network.BuildConfig(appName) +} diff --git a/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go b/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go new file mode 100644 index 000000000..b2edbbbde --- /dev/null +++ b/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/config" +) + +// computes the ports for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + procType := flag.Arg(1) + isHerokuishContainer := common.ToBool(flag.Arg(2)) + + if procType != "web" { + return + } + + var dockerfilePorts []string + if !isHerokuishContainer { + dockerfilePorts = strings.Split(config.GetWithDefault(appName, "DOKKU_DOCKERFILE_PORTS", ""), " ") + } + + var ports []string + if len(dockerfilePorts) == 0 { + ports = append(ports, "5000") + } else { + for _, port := range dockerfilePorts { + port = strings.TrimSuffix(strings.TrimSpace(port), "/tcp") + if port == "" || strings.HasSuffix(port, "/udp") { + continue + } + ports = append(ports, port) + } + } + fmt.Fprint(os.Stdout, strings.Join(ports, " ")) +} diff --git a/plugins/network/src/triggers/network-config-exists/network-config-exists.go b/plugins/network/src/triggers/network-config-exists/network-config-exists.go new file mode 100644 index 000000000..6ba8cd737 --- /dev/null +++ b/plugins/network/src/triggers/network-config-exists/network-config-exists.go @@ -0,0 +1,22 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/network" +) + +// write the ipaddress to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + + if network.HasNetworkConfig(appName) { + fmt.Fprintln(os.Stdout, "true") + return + } + + fmt.Fprintln(os.Stdout, "false") +} diff --git a/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go b/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go new file mode 100644 index 000000000..cfdb3bbfd --- /dev/null +++ b/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go @@ -0,0 +1,20 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/network" +) + +// write the ipaddress to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + procType := flag.Arg(1) + containerID := flag.Arg(2) + + ipAddress := network.GetContainerIpaddress(appName, procType, containerID) + fmt.Fprintln(os.Stdout, ipAddress) +} diff --git a/plugins/network/src/triggers/network-get-listeners/network-get-listeners.go b/plugins/network/src/triggers/network-get-listeners/network-get-listeners.go new file mode 100644 index 000000000..9c999fe4e --- /dev/null +++ b/plugins/network/src/triggers/network-get-listeners/network-get-listeners.go @@ -0,0 +1,19 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/network" +) + +// returns the listeners (host:port combinations) for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + + listeners := network.GetListeners(appName) + fmt.Fprint(os.Stdout, strings.Join(listeners, " ")) +} diff --git a/plugins/network/src/triggers/network-get-port/network-get-port.go b/plugins/network/src/triggers/network-get-port/network-get-port.go new file mode 100644 index 000000000..801bc88ff --- /dev/null +++ b/plugins/network/src/triggers/network-get-port/network-get-port.go @@ -0,0 +1,22 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/common" + network "github.com/dokku/dokku/plugins/network" +) + +// write the port to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + procType := flag.Arg(1) + isHerokuishContainer := common.ToBool(flag.Arg(2)) + containerID := flag.Arg(3) + + port := network.GetContainerPort(appName, procType, isHerokuishContainer, containerID) + fmt.Fprintln(os.Stdout, port) +} diff --git a/plugins/network/src/triggers/network-get-property/network-get-property.go b/plugins/network/src/triggers/network-get-property/network-get-property.go new file mode 100644 index 000000000..f7486b93a --- /dev/null +++ b/plugins/network/src/triggers/network-get-property/network-get-property.go @@ -0,0 +1,21 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/common" + network "github.com/dokku/dokku/plugins/network" +) + +// write the port to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + property := flag.Arg(1) + + defaultValue := network.GetDefaultValue(property) + value := common.PropertyGetDefault("network", appName, property, defaultValue) + fmt.Fprintln(os.Stdout, value) +} diff --git a/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go b/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go new file mode 100644 index 000000000..b76ceb3af --- /dev/null +++ b/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// writes the ip to disk +func main() { + flag.Parse() + appName := flag.Arg(0) + procType := flag.Arg(1) + containerIndex := flag.Arg(2) + ip := flag.Arg(3) + + if appName == "" { + common.LogFail("Please specify an app to run the command on") + } + err := common.VerifyAppName(appName) + if err != nil { + common.LogFail(err.Error()) + } + + appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + filename := fmt.Sprintf("%v/IP.%v.%v", appRoot, procType, containerIndex) + f, err := os.Create(filename) + if err != nil { + common.LogFail(err.Error()) + } + defer f.Close() + + ipBytes := []byte(ip) + _, err = f.Write(ipBytes) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/network/src/triggers/network-write-port/network-write-port.go b/plugins/network/src/triggers/network-write-port/network-write-port.go new file mode 100644 index 000000000..2a8d45dff --- /dev/null +++ b/plugins/network/src/triggers/network-write-port/network-write-port.go @@ -0,0 +1,41 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// writes the port to disk +func main() { + flag.Parse() + appName := flag.Arg(0) + procType := flag.Arg(1) + containerIndex := flag.Arg(2) + port := flag.Arg(3) + + if appName == "" { + common.LogFail("Please specify an app to run the command on") + } + err := common.VerifyAppName(appName) + if err != nil { + common.LogFail(err.Error()) + } + + appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + filename := fmt.Sprintf("%v/PORT.%v.%v", appRoot, procType, containerIndex) + f, err := os.Create(filename) + if err != nil { + common.LogFail(err.Error()) + } + defer f.Close() + + portBytes := []byte(port) + _, err = f.Write(portBytes) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/network/src/triggers/post-create/post-create.go b/plugins/network/src/triggers/post-create/post-create.go new file mode 100644 index 000000000..4d03f0df3 --- /dev/null +++ b/plugins/network/src/triggers/post-create/post-create.go @@ -0,0 +1,15 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/common" +) + +// set bind-all-interfaces to false by default +func main() { + flag.Parse() + appName := flag.Arg(0) + + common.PropertyWrite("network", appName, "bind-all-interfaces", "false") +} diff --git a/plugins/network/src/triggers/post-delete/post-delete.go b/plugins/network/src/triggers/post-delete/post-delete.go new file mode 100644 index 000000000..bd620ceb8 --- /dev/null +++ b/plugins/network/src/triggers/post-delete/post-delete.go @@ -0,0 +1,15 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/common" +) + +// write the port to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + + common.PropertyDestroy("network", appName) +} diff --git a/plugins/network/src/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/network/src/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/network/src/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/network/src/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/network/src/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/network/src/vendor/github.com/ryanuber/columnize/LICENSE @@ -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. diff --git a/plugins/network/src/vendor/github.com/ryanuber/columnize/README.md b/plugins/network/src/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/network/src/vendor/github.com/ryanuber/columnize/README.md @@ -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. diff --git a/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize.go @@ -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) +} diff --git a/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/network/src/vendor/github.com/ryanuber/columnize/columnize_test.go @@ -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 = "" + output := Format(input, config) + + expected := "Column A Column B Column C\n" + expected += "x 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) + } +} diff --git a/plugins/network/vendor/github.com/codegangsta/inject/.gitignore b/plugins/network/vendor/github.com/codegangsta/inject/.gitignore new file mode 100644 index 000000000..df3df8a90 --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/.gitignore @@ -0,0 +1,2 @@ +inject +inject.test diff --git a/plugins/network/vendor/github.com/codegangsta/inject/LICENSE b/plugins/network/vendor/github.com/codegangsta/inject/LICENSE new file mode 100644 index 000000000..eb68a0e05 --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Jeremy Saenz + +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. diff --git a/plugins/network/vendor/github.com/codegangsta/inject/README.md b/plugins/network/vendor/github.com/codegangsta/inject/README.md new file mode 100644 index 000000000..679abe01a --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/README.md @@ -0,0 +1,92 @@ +# inject +-- + import "github.com/codegangsta/inject" + +Package inject provides utilities for mapping and injecting dependencies in +various ways. + +Language Translations: +* [简体中文](translations/README_zh_cn.md) + +## Usage + +#### func InterfaceOf + +```go +func InterfaceOf(value interface{}) reflect.Type +``` +InterfaceOf dereferences a pointer to an Interface type. It panics if value is +not an pointer to an interface. + +#### type Applicator + +```go +type Applicator interface { + // Maps dependencies in the Type map to each field in the struct + // that is tagged with 'inject'. Returns an error if the injection + // fails. + Apply(interface{}) error +} +``` + +Applicator represents an interface for mapping dependencies to a struct. + +#### type Injector + +```go +type Injector interface { + Applicator + Invoker + TypeMapper + // SetParent sets the parent of the injector. If the injector cannot find a + // dependency in its Type map it will check its parent before returning an + // error. + SetParent(Injector) +} +``` + +Injector represents an interface for mapping and injecting dependencies into +structs and function arguments. + +#### func New + +```go +func New() Injector +``` +New returns a new Injector. + +#### type Invoker + +```go +type Invoker interface { + // Invoke attempts to call the interface{} provided as a function, + // providing dependencies for function arguments based on Type. Returns + // a slice of reflect.Value representing the returned values of the function. + // Returns an error if the injection fails. + Invoke(interface{}) ([]reflect.Value, error) +} +``` + +Invoker represents an interface for calling functions via reflection. + +#### type TypeMapper + +```go +type TypeMapper interface { + // Maps the interface{} value based on its immediate type from reflect.TypeOf. + Map(interface{}) TypeMapper + // Maps the interface{} value based on the pointer of an Interface provided. + // This is really only useful for mapping a value as an interface, as interfaces + // cannot at this time be referenced directly without a pointer. + MapTo(interface{}, interface{}) TypeMapper + // Provides a possibility to directly insert a mapping based on type and value. + // This makes it possible to directly map type arguments not possible to instantiate + // with reflect like unidirectional channels. + Set(reflect.Type, reflect.Value) TypeMapper + // Returns the Value that is mapped to the current type. Returns a zeroed Value if + // the Type has not been mapped. + Get(reflect.Type) reflect.Value +} +``` + +TypeMapper represents an interface for mapping interface{} values based on type. diff --git a/plugins/network/vendor/github.com/codegangsta/inject/inject.go b/plugins/network/vendor/github.com/codegangsta/inject/inject.go new file mode 100644 index 000000000..3ff713c8a --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/inject.go @@ -0,0 +1,187 @@ +// Package inject provides utilities for mapping and injecting dependencies in various ways. +package inject + +import ( + "fmt" + "reflect" +) + +// Injector represents an interface for mapping and injecting dependencies into structs +// and function arguments. +type Injector interface { + Applicator + Invoker + TypeMapper + // SetParent sets the parent of the injector. If the injector cannot find a + // dependency in its Type map it will check its parent before returning an + // error. + SetParent(Injector) +} + +// Applicator represents an interface for mapping dependencies to a struct. +type Applicator interface { + // Maps dependencies in the Type map to each field in the struct + // that is tagged with 'inject'. Returns an error if the injection + // fails. + Apply(interface{}) error +} + +// Invoker represents an interface for calling functions via reflection. +type Invoker interface { + // Invoke attempts to call the interface{} provided as a function, + // providing dependencies for function arguments based on Type. Returns + // a slice of reflect.Value representing the returned values of the function. + // Returns an error if the injection fails. + Invoke(interface{}) ([]reflect.Value, error) +} + +// TypeMapper represents an interface for mapping interface{} values based on type. +type TypeMapper interface { + // Maps the interface{} value based on its immediate type from reflect.TypeOf. + Map(interface{}) TypeMapper + // Maps the interface{} value based on the pointer of an Interface provided. + // This is really only useful for mapping a value as an interface, as interfaces + // cannot at this time be referenced directly without a pointer. + MapTo(interface{}, interface{}) TypeMapper + // Provides a possibility to directly insert a mapping based on type and value. + // This makes it possible to directly map type arguments not possible to instantiate + // with reflect like unidirectional channels. + Set(reflect.Type, reflect.Value) TypeMapper + // Returns the Value that is mapped to the current type. Returns a zeroed Value if + // the Type has not been mapped. + Get(reflect.Type) reflect.Value +} + +type injector struct { + values map[reflect.Type]reflect.Value + parent Injector +} + +// InterfaceOf dereferences a pointer to an Interface type. +// It panics if value is not an pointer to an interface. +func InterfaceOf(value interface{}) reflect.Type { + t := reflect.TypeOf(value) + + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + if t.Kind() != reflect.Interface { + panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)") + } + + return t +} + +// New returns a new Injector. +func New() Injector { + return &injector{ + values: make(map[reflect.Type]reflect.Value), + } +} + +// Invoke attempts to call the interface{} provided as a function, +// providing dependencies for function arguments based on Type. +// Returns a slice of reflect.Value representing the returned values of the function. +// Returns an error if the injection fails. +// It panics if f is not a function +func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) { + t := reflect.TypeOf(f) + + var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func + for i := 0; i < t.NumIn(); i++ { + argType := t.In(i) + val := inj.Get(argType) + if !val.IsValid() { + return nil, fmt.Errorf("Value not found for type %v", argType) + } + + in[i] = val + } + + return reflect.ValueOf(f).Call(in), nil +} + +// Maps dependencies in the Type map to each field in the struct +// that is tagged with 'inject'. +// Returns an error if the injection fails. +func (inj *injector) Apply(val interface{}) error { + v := reflect.ValueOf(val) + + for v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return nil // Should not panic here ? + } + + t := v.Type() + + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + structField := t.Field(i) + if f.CanSet() && (structField.Tag == "inject" || structField.Tag.Get("inject") != "") { + ft := f.Type() + v := inj.Get(ft) + if !v.IsValid() { + return fmt.Errorf("Value not found for type %v", ft) + } + + f.Set(v) + } + + } + + return nil +} + +// Maps the concrete value of val to its dynamic type using reflect.TypeOf, +// It returns the TypeMapper registered in. +func (i *injector) Map(val interface{}) TypeMapper { + i.values[reflect.TypeOf(val)] = reflect.ValueOf(val) + return i +} + +func (i *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper { + i.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val) + return i +} + +// Maps the given reflect.Type to the given reflect.Value and returns +// the Typemapper the mapping has been registered in. +func (i *injector) Set(typ reflect.Type, val reflect.Value) TypeMapper { + i.values[typ] = val + return i +} + +func (i *injector) Get(t reflect.Type) reflect.Value { + val := i.values[t] + + if val.IsValid() { + return val + } + + // no concrete types found, try to find implementors + // if t is an interface + if t.Kind() == reflect.Interface { + for k, v := range i.values { + if k.Implements(t) { + val = v + break + } + } + } + + // Still no type found, try to look it up on the parent + if !val.IsValid() && i.parent != nil { + val = i.parent.Get(t) + } + + return val + +} + +func (i *injector) SetParent(parent Injector) { + i.parent = parent +} diff --git a/plugins/network/vendor/github.com/codegangsta/inject/inject_test.go b/plugins/network/vendor/github.com/codegangsta/inject/inject_test.go new file mode 100644 index 000000000..eb94471d3 --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/inject_test.go @@ -0,0 +1,159 @@ +package inject_test + +import ( + "fmt" + "github.com/codegangsta/inject" + "reflect" + "testing" +) + +type SpecialString interface { +} + +type TestStruct struct { + Dep1 string `inject:"t" json:"-"` + Dep2 SpecialString `inject` + Dep3 string +} + +type Greeter struct { + Name string +} + +func (g *Greeter) String() string { + return "Hello, My name is" + g.Name +} + +/* Test Helpers */ +func expect(t *testing.T, a interface{}, b interface{}) { + if a != b { + t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} + +func refute(t *testing.T, a interface{}, b interface{}) { + if a == b { + t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + } +} + +func Test_InjectorInvoke(t *testing.T) { + injector := inject.New() + expect(t, injector == nil, false) + + dep := "some dependency" + injector.Map(dep) + dep2 := "another dep" + injector.MapTo(dep2, (*SpecialString)(nil)) + dep3 := make(chan *SpecialString) + dep4 := make(chan *SpecialString) + typRecv := reflect.ChanOf(reflect.RecvDir, reflect.TypeOf(dep3).Elem()) + typSend := reflect.ChanOf(reflect.SendDir, reflect.TypeOf(dep4).Elem()) + injector.Set(typRecv, reflect.ValueOf(dep3)) + injector.Set(typSend, reflect.ValueOf(dep4)) + + _, err := injector.Invoke(func(d1 string, d2 SpecialString, d3 <-chan *SpecialString, d4 chan<- *SpecialString) { + expect(t, d1, dep) + expect(t, d2, dep2) + expect(t, reflect.TypeOf(d3).Elem(), reflect.TypeOf(dep3).Elem()) + expect(t, reflect.TypeOf(d4).Elem(), reflect.TypeOf(dep4).Elem()) + expect(t, reflect.TypeOf(d3).ChanDir(), reflect.RecvDir) + expect(t, reflect.TypeOf(d4).ChanDir(), reflect.SendDir) + }) + + expect(t, err, nil) +} + +func Test_InjectorInvokeReturnValues(t *testing.T) { + injector := inject.New() + expect(t, injector == nil, false) + + dep := "some dependency" + injector.Map(dep) + dep2 := "another dep" + injector.MapTo(dep2, (*SpecialString)(nil)) + + result, err := injector.Invoke(func(d1 string, d2 SpecialString) string { + expect(t, d1, dep) + expect(t, d2, dep2) + return "Hello world" + }) + + expect(t, result[0].String(), "Hello world") + expect(t, err, nil) +} + +func Test_InjectorApply(t *testing.T) { + injector := inject.New() + + injector.Map("a dep").MapTo("another dep", (*SpecialString)(nil)) + + s := TestStruct{} + err := injector.Apply(&s) + expect(t, err, nil) + + expect(t, s.Dep1, "a dep") + expect(t, s.Dep2, "another dep") + expect(t, s.Dep3, "") +} + +func Test_InterfaceOf(t *testing.T) { + iType := inject.InterfaceOf((*SpecialString)(nil)) + expect(t, iType.Kind(), reflect.Interface) + + iType = inject.InterfaceOf((**SpecialString)(nil)) + expect(t, iType.Kind(), reflect.Interface) + + // Expecting nil + defer func() { + rec := recover() + refute(t, rec, nil) + }() + iType = inject.InterfaceOf((*testing.T)(nil)) +} + +func Test_InjectorSet(t *testing.T) { + injector := inject.New() + typ := reflect.TypeOf("string") + typSend := reflect.ChanOf(reflect.SendDir, typ) + typRecv := reflect.ChanOf(reflect.RecvDir, typ) + + // instantiating unidirectional channels is not possible using reflect + // http://golang.org/src/pkg/reflect/value.go?s=60463:60504#L2064 + chanRecv := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0) + chanSend := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, typ), 0) + + injector.Set(typSend, chanSend) + injector.Set(typRecv, chanRecv) + + expect(t, injector.Get(typSend).IsValid(), true) + expect(t, injector.Get(typRecv).IsValid(), true) + expect(t, injector.Get(chanSend.Type()).IsValid(), false) +} + +func Test_InjectorGet(t *testing.T) { + injector := inject.New() + + injector.Map("some dependency") + + expect(t, injector.Get(reflect.TypeOf("string")).IsValid(), true) + expect(t, injector.Get(reflect.TypeOf(11)).IsValid(), false) +} + +func Test_InjectorSetParent(t *testing.T) { + injector := inject.New() + injector.MapTo("another dep", (*SpecialString)(nil)) + + injector2 := inject.New() + injector2.SetParent(injector) + + expect(t, injector2.Get(inject.InterfaceOf((*SpecialString)(nil))).IsValid(), true) +} + +func TestInjectImplementors(t *testing.T) { + injector := inject.New() + g := &Greeter{"Jeremy"} + injector.Map(g) + + expect(t, injector.Get(inject.InterfaceOf((*fmt.Stringer)(nil))).IsValid(), true) +} diff --git a/plugins/network/vendor/github.com/codegangsta/inject/translations/README_zh_cn.md b/plugins/network/vendor/github.com/codegangsta/inject/translations/README_zh_cn.md new file mode 100644 index 000000000..0ac3d3f55 --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/translations/README_zh_cn.md @@ -0,0 +1,85 @@ +# inject +-- + import "github.com/codegangsta/inject" + +inject包提供了多种对实体的映射和依赖注入方式。 + +## 用法 + +#### func InterfaceOf + +```go +func InterfaceOf(value interface{}) reflect.Type +``` +函数InterfaceOf返回指向接口类型的指针。如果传入的value值不是指向接口的指针,将抛出一个panic异常。 + +#### type Applicator + +```go +type Applicator interface { + // 在Type map中维持对结构体中每个域的引用并用'inject'来标记 + // 如果注入失败将会返回一个error. + Apply(interface{}) error +} +``` + +Applicator接口表示到结构体的依赖映射关系。 + +#### type Injector + +```go +type Injector interface { + Applicator + Invoker + TypeMapper + // SetParent用来设置父injector. 如果在当前injector的Type map中找不到依赖, + // 将会继续从它的父injector中找,直到返回error. + SetParent(Injector) +} +``` + +Injector接口表示对结构体、函数参数的映射和依赖注入。 + +#### func New + +```go +func New() Injector +``` +New创建并返回一个Injector. + +#### type Invoker + +```go +type Invoker interface { + // Invoke尝试将interface{}作为一个函数来调用,并基于Type为函数提供参数。 + // 它将返回reflect.Value的切片,其中存放原函数的返回值。 + // 如果注入失败则返回error. + Invoke(interface{}) ([]reflect.Value, error) +} +``` + +Invoker接口表示通过反射进行函数调用。 + +#### type TypeMapper + +```go +type TypeMapper interface { + // 基于调用reflect.TypeOf得到的类型映射interface{}的值。 + Map(interface{}) TypeMapper + // 基于提供的接口的指针映射interface{}的值。 + // 该函数仅用来将一个值映射为接口,因为接口无法不通过指针而直接引用到。 + MapTo(interface{}, interface{}) TypeMapper + // 为直接插入基于类型和值的map提供一种可能性。 + // 它使得这一类直接映射成为可能:无法通过反射直接实例化的类型参数,如单向管道。 + Set(reflect.Type, reflect.Value) TypeMapper + // 返回映射到当前类型的Value. 如果Type没被映射,将返回对应的零值。 + Get(reflect.Type) reflect.Value +} +``` + +TypeMapper接口用来表示基于类型到接口值的映射。 + + +## 译者 + +张强 (qqbunny@yeah.net) \ No newline at end of file diff --git a/plugins/network/vendor/github.com/codegangsta/inject/update_readme.sh b/plugins/network/vendor/github.com/codegangsta/inject/update_readme.sh new file mode 100644 index 000000000..497f9a577 --- /dev/null +++ b/plugins/network/vendor/github.com/codegangsta/inject/update_readme.sh @@ -0,0 +1,3 @@ +#!/bin/bash +go get github.com/robertkrimen/godocdown/godocdown +godocdown > README.md diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/LICENSE b/plugins/network/vendor/github.com/codeskyblue/go-sh/LICENSE new file mode 100644 index 000000000..e06d20818 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/OLD_README.md b/plugins/network/vendor/github.com/codeskyblue/go-sh/OLD_README.md new file mode 100644 index 000000000..7e899402d --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/OLD_README.md @@ -0,0 +1,69 @@ +## OLD README +First give you a full example, I will explain every command below. + + session := sh.NewSession() + session.Env["PATH"] = "/usr/bin:/bin" + session.Stdout = os.Stdout + session.Stderr = os.Stderr + session.Alias("ll", "ls", "-l") + session.ShowCMD = true // enable for debug + var err error + err = session.Call("ll", "/") + if err != nil { + log.Fatal(err) + } + ret, err := session.Capture("pwd", sh.Dir("/home")) # wraper of session.Call + if err != nil { + log.Fatal(err) + } + # ret is "/home\n" + fmt.Println(ret) + +create a new Session + + session := sh.NewSession() + +use alias like this + + session.Alias("ll", "ls", "-l") # like alias ll='ls -l' + +set current env like this + + session.Env["BUILD_ID"] = "123" # like export BUILD_ID=123 + +set current directory + + session.Set(sh.Dir("/")) # like cd / + +pipe is also supported + + session.Command("echo", "hello\tworld").Command("cut", "-f2") + // output should be "world" + session.Run() + +test, the build in command support + + session.Test("d", "dir") // test dir + session.Test("f", "file) // test regular file + +with `Alias Env Set Call Capture Command` a shell scripts can be easily converted into golang program. below is a shell script. + + #!/bin/bash - + # + export PATH=/usr/bin:/bin + alias ll='ls -l' + cd /usr + if test -d "local" + then + ll local | awk '{print $1, $NF}' + fi + +convert to golang, will be + + s := sh.NewSession() + s.Env["PATH"] = "/usr/bin:/bin" + s.Set(sh.Dir("/usr")) + s.Alias("ll", "ls", "-l") + if s.Test("d", "local") { + s.Command("ll", "local").Command("awk", "{print $1, $NF}").Run() + } diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/README.md b/plugins/network/vendor/github.com/codeskyblue/go-sh/README.md new file mode 100644 index 000000000..28614708d --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/README.md @@ -0,0 +1,85 @@ +## go-sh +[![wercker status](https://app.wercker.com/status/009acbd4f00ccc6de7e2554e12a50d84/s "wercker status")](https://app.wercker.com/project/bykey/009acbd4f00ccc6de7e2554e12a50d84) +[![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/codeskyblue/go-sh) + +*If you depend on the old api, see tag: v.0.1* + +install: `go get github.com/codeskyblue/go-sh` + +Pipe Example: + + package main + + import "github.com/codeskyblue/go-sh" + + func main() { + sh.Command("echo", "hello\tworld").Command("cut", "-f2").Run() + } + +Because I like os/exec, `go-sh` is very much modelled after it. However, `go-sh` provides a better experience. + +These are some of its features: + +* keep the variable environment (e.g. export) +* alias support (e.g. alias in shell) +* remember current dir +* pipe command +* shell build-in commands echo & test +* timeout support + +Examples are important: + + sh: echo hello + go: sh.Command("echo", "hello").Run() + + sh: export BUILD_ID=123 + go: s = sh.NewSession().SetEnv("BUILD_ID", "123") + + sh: alias ll='ls -l' + go: s = sh.NewSession().Alias('ll', 'ls', '-l') + + sh: (cd /; pwd) + go: sh.Command("pwd", sh.Dir("/")).Run() + + sh: test -d data || mkdir data + go: if ! sh.Test("dir", "data") { sh.Command("mkdir", "data").Run() } + + sh: cat first second | awk '{print $1}' + go: sh.Command("cat", "first", "second").Command("awk", "{print $1}").Run() + + sh: count=$(echo "one two three" | wc -w) + go: count, err := sh.Echo("one two three").Command("wc", "-w").Output() + + sh(in ubuntu): timeout 1s sleep 3 + go: c := sh.Command("sleep", "3"); c.Start(); c.WaitTimeout(time.Second) # default SIGKILL + go: out, err := sh.Command("sleep", "3").SetTimeout(time.Second).Output() # set session timeout and get output) + + sh: echo hello | cat + go: out, err := sh.Command("cat").SetInput("hello").Output() + + sh: cat # read from stdin + go: out, err := sh.Command("cat").SetStdin(os.Stdin).Output() + +If you need to keep env and dir, it is better to create a session + + session := sh.NewSession() + session.SetEnv("BUILD_ID", "123") + session.SetDir("/") + # then call cmd + session.Command("echo", "hello").Run() + # set ShowCMD to true for easily debug + session.ShowCMD = true + +for more information, it better to see docs. +[![Go Walker](http://gowalker.org/api/v1/badge)](http://gowalker.org/github.com/codeskyblue/go-sh) + +### contribute +If you love this project, starring it will encourage the coder. Pull requests are welcome. + +support the author: [alipay](https://me.alipay.com/goskyblue) + +### thanks +this project is based on . thanks for the author. + +# the reason to use Go shell +Sometimes we need to write shell scripts, but shell scripts are not good at working cross platform, Go, on the other hand, is good at that. Is there a good way to use Go to write shell like scripts? Using go-sh we can do this now. diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/example/example1.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/example1.go new file mode 100644 index 000000000..cdc7f6c39 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/example1.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "log" + + "github.com/codeskyblue/go-sh" +) + +func main() { + sh.Command("echo", "hello").Run() + out, err := sh.Command("echo", "hello").Output() + if err != nil { + log.Fatal(err) + } + fmt.Println("output is", string(out)) + + var a int + sh.Command("echo", "2").UnmarshalJSON(&a) + fmt.Println("a =", a) + + s := sh.NewSession() + s.Alias("hi", "echo", "hi") + s.Command("hi", "boy").Run() + + fmt.Print("pwd = ") + s.Command("pwd", sh.Dir("/")).Run() + + if !sh.Test("dir", "data") { + sh.Command("echo", "mkdir", "data").Run() + } + + sh.Command("echo", "hello", "world"). + Command("awk", `{print "second arg is "$2}`).Run() + s.ShowCMD = true + s.Command("echo", "hello", "world"). + Command("awk", `{print "second arg is "$2}`).Run() + + s.SetEnv("BUILD_ID", "123").Command("bash", "-c", "echo $BUILD_ID").Run() + s.Command("bash", "-c", "echo current shell is $SHELL").Run() +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/example/less/less.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/less/less.go new file mode 100644 index 000000000..ffced2e81 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/less/less.go @@ -0,0 +1,7 @@ +package main + +import "github.com/codeskyblue/go-sh" + +func main() { + sh.Command("less", "less.go").Run() +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/example/tail/tailf.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/tail/tailf.go new file mode 100644 index 000000000..b7ecd3bed --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/tail/tailf.go @@ -0,0 +1,17 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/codeskyblue/go-sh" +) + +func main() { + flag.Parse() + if flag.NArg() != 1 { + fmt.Println("Usage: PROGRAM ") + return + } + sh.Command("tail", "-f", flag.Arg(0)).Run() +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/example/timeout/timeout.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/timeout/timeout.go new file mode 100644 index 000000000..852908a47 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/example/timeout/timeout.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "time" + + sh "github.com/codeskyblue/go-sh" +) + +func main() { + c := sh.Command("sleep", "3") + c.Start() + err := c.WaitTimeout(time.Second * 1) + if err != nil { + fmt.Printf("timeout should happend: %v\n", err) + } + // timeout should be a session + out, err := sh.Command("sleep", "2").SetTimeout(time.Second).Output() + fmt.Printf("output:(%s), err(%v)\n", string(out), err) + + out, err = sh.Command("echo", "hello").SetTimeout(time.Second).Output() + fmt.Printf("output:(%s), err(%v)\n", string(out), err) +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/example_test.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/example_test.go new file mode 100644 index 000000000..5abfed64d --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/example_test.go @@ -0,0 +1,28 @@ +package sh_test + +import ( + "fmt" + + "github.com/codeskyblue/go-sh" +) + +func ExampleCommand() { + out, err := sh.Command("echo", "hello").Output() + fmt.Println(string(out), err) +} + +func ExampleCommandPipe() { + out, err := sh.Command("echo", "-n", "hi").Command("wc", "-c").Output() + fmt.Println(string(out), err) +} + +func ExampleCommandSetDir() { + out, err := sh.Command("pwd", sh.Dir("/")).Output() + fmt.Println(string(out), err) +} + +func ExampleTest() { + if sh.Test("dir", "mydir") { + fmt.Println("mydir exists") + } +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe.go new file mode 100644 index 000000000..c35f4e2a0 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe.go @@ -0,0 +1,148 @@ +package sh + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "errors" + "io" + "os" + "strings" + "syscall" + "time" +) + +var ErrExecTimeout = errors.New("execute timeout") + +// unmarshal shell output to decode json +func (s *Session) UnmarshalJSON(data interface{}) (err error) { + bufrw := bytes.NewBuffer(nil) + s.Stdout = bufrw + if err = s.Run(); err != nil { + return + } + return json.NewDecoder(bufrw).Decode(data) +} + +// unmarshal command output into xml +func (s *Session) UnmarshalXML(data interface{}) (err error) { + bufrw := bytes.NewBuffer(nil) + s.Stdout = bufrw + if err = s.Run(); err != nil { + return + } + return xml.NewDecoder(bufrw).Decode(data) +} + +// start command +func (s *Session) Start() (err error) { + s.started = true + var rd *io.PipeReader + var wr *io.PipeWriter + var length = len(s.cmds) + if s.ShowCMD { + var cmds = make([]string, 0, 4) + for _, cmd := range s.cmds { + cmds = append(cmds, strings.Join(cmd.Args, " ")) + } + s.writePrompt(strings.Join(cmds, " | ")) + } + for index, cmd := range s.cmds { + if index == 0 { + cmd.Stdin = s.Stdin + } else { + cmd.Stdin = rd + } + if index != length { + rd, wr = io.Pipe() // create pipe + cmd.Stdout = wr + cmd.Stderr = os.Stderr + } + if index == length-1 { + cmd.Stdout = s.Stdout + cmd.Stderr = s.Stderr + } + err = cmd.Start() + if err != nil { + return + } + } + return +} + +// Should be call after Start() +// only catch the last command error +func (s *Session) Wait() (err error) { + for _, cmd := range s.cmds { + err = cmd.Wait() + wr, ok := cmd.Stdout.(*io.PipeWriter) + if ok { + wr.Close() + } + } + return err +} + +func (s *Session) Kill(sig os.Signal) { + for _, cmd := range s.cmds { + if cmd.Process != nil { + cmd.Process.Signal(sig) + } + } +} + +func (s *Session) WaitTimeout(timeout time.Duration) (err error) { + select { + case <-time.After(timeout): + s.Kill(syscall.SIGKILL) + return ErrExecTimeout + case err = <-Go(s.Wait): + return err + } +} + +func Go(f func() error) chan error { + ch := make(chan error) + go func() { + ch <- f() + }() + return ch +} + +func (s *Session) Run() (err error) { + if err = s.Start(); err != nil { + return + } + if s.timeout != time.Duration(0) { + return s.WaitTimeout(s.timeout) + } + return s.Wait() +} + +func (s *Session) Output() (out []byte, err error) { + oldout := s.Stdout + defer func() { + s.Stdout = oldout + }() + stdout := bytes.NewBuffer(nil) + s.Stdout = stdout + err = s.Run() + out = stdout.Bytes() + return +} + +func (s *Session) CombinedOutput() (out []byte, err error) { + oldout := s.Stdout + olderr := s.Stderr + defer func() { + s.Stdout = oldout + s.Stderr = olderr + }() + stdout := bytes.NewBuffer(nil) + s.Stdout = stdout + s.Stderr = stdout + + err = s.Run() + out = stdout.Bytes() + return +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe_test.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe_test.go new file mode 100644 index 000000000..8d0e22c0c --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/pipe_test.go @@ -0,0 +1,126 @@ +package sh + +import ( + "encoding/xml" + "io" + "os" + "os/exec" + "strings" + "testing" + "time" +) + +func TestUnmarshalJSON(t *testing.T) { + var a int + s := NewSession() + s.ShowCMD = true + err := s.Command("echo", []string{"1"}).UnmarshalJSON(&a) + if err != nil { + t.Error(err) + } + if a != 1 { + t.Errorf("expect a tobe 1, but got %d", a) + } +} + +func TestUnmarshalXML(t *testing.T) { + s := NewSession() + xmlSample := ` +` + type server struct { + XMLName xml.Name `xml:"server"` + Version string `xml:"version,attr"` + } + data := &server{} + s.Command("echo", xmlSample).UnmarshalXML(data) + if data.Version != "1" { + t.Error(data) + } +} + +func TestPipe(t *testing.T) { + s := NewSession() + s.ShowCMD = true + s.Call("echo", "hello") + err := s.Command("echo", "hi").Command("cat", "-n").Start() + if err != nil { + t.Error(err) + } + err = s.Wait() + if err != nil { + t.Error(err) + } + out, err := s.Command("echo", []string{"hello"}).Output() + if err != nil { + t.Error(err) + } + if string(out) != "hello\n" { + t.Error("capture wrong output:", out) + } + s.Command("echo", []string{"hello\tworld"}).Command("cut", []string{"-f2"}).Run() +} + +func TestPipeCommand(t *testing.T) { + c1 := exec.Command("echo", "good") + rd, wr := io.Pipe() + c1.Stdout = wr + c2 := exec.Command("cat", "-n") + c2.Stdout = os.Stdout + c2.Stdin = rd + c1.Start() + c2.Start() + + c1.Wait() + wc, ok := c1.Stdout.(io.WriteCloser) + if ok { + wc.Close() + } + c2.Wait() +} + +func TestPipeInput(t *testing.T) { + s := NewSession() + s.ShowCMD = true + s.SetInput("first line\nsecond line\n") + out, err := s.Command("grep", "second").Output() + if err != nil { + t.Error(err) + } + if string(out) != "second line\n" { + t.Error("capture wrong output:", out) + } +} + +func TestTimeout(t *testing.T) { + s := NewSession() + err := s.Command("sleep", "2").Start() + if err != nil { + t.Fatal(err) + } + err = s.WaitTimeout(time.Second) + if err != ErrExecTimeout { + t.Fatal(err) + } +} + +func TestSetTimeout(t *testing.T) { + s := NewSession() + s.SetTimeout(time.Second) + defer s.SetTimeout(0) + err := s.Command("sleep", "2").Run() + if err != ErrExecTimeout { + t.Fatal(err) + } +} + +func TestCombinedOutput(t *testing.T) { + s := NewSession() + bytes, err := s.Command("sh", "-c", "echo stderr >&2 ; echo stdout").CombinedOutput() + if err != nil { + t.Error(err) + } + stringOutput := string(bytes) + if !(strings.Contains(stringOutput, "stdout") && strings.Contains(stringOutput, "stderr")) { + t.Errorf("expect output from both output streams, got '%s'", strings.TrimSpace(stringOutput)) + } +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/sh.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/sh.go new file mode 100644 index 000000000..15f1b85d5 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/sh.go @@ -0,0 +1,200 @@ +/* +Package go-sh is intented to make shell call with golang more easily. +Some usage is more similar to os/exec, eg: Run(), Output(), Command(name, args...) + +But with these similar function, pipe is added in and this package also got shell-session support. + +Why I love golang so much, because the usage of golang is simple, but the power is unlimited. I want to make this pakcage got the sample style like golang. + + // just like os/exec + sh.Command("echo", "hello").Run() + + // support pipe + sh.Command("echo", "hello").Command("wc", "-c").Run() + + // create a session to store dir and env + sh.NewSession().SetDir("/").Command("pwd") + + // shell buildin command - "test" + sh.Test("dir", "mydir") + + // like shell call: (cd /; pwd) + sh.Command("pwd", sh.Dir("/")) same with sh.Command(sh.Dir("/"), "pwd") + + // output to json and xml easily + v := map[string] int {} + err = sh.Command("echo", `{"number": 1}`).UnmarshalJSON(&v) +*/ +package sh + +import ( + "fmt" + "io" + "os" + "os/exec" + "reflect" + "strings" + "time" + + "github.com/codegangsta/inject" +) + +type Dir string + +type Session struct { + inj inject.Injector + alias map[string][]string + cmds []*exec.Cmd + dir Dir + started bool + Env map[string]string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + ShowCMD bool // enable for debug + timeout time.Duration +} + +func (s *Session) writePrompt(args ...interface{}) { + var ps1 = fmt.Sprintf("[golang-sh]$") + args = append([]interface{}{ps1}, args...) + fmt.Fprintln(s.Stderr, args...) +} + +func NewSession() *Session { + env := make(map[string]string) + for _, key := range []string{"PATH"} { + env[key] = os.Getenv(key) + } + s := &Session{ + inj: inject.New(), + alias: make(map[string][]string), + dir: Dir(""), + Stdin: strings.NewReader(""), + Stdout: os.Stdout, + Stderr: os.Stderr, + Env: env, + } + return s +} + +func InteractiveSession() *Session { + s := NewSession() + s.SetStdin(os.Stdin) + return s +} + +func Command(name string, a ...interface{}) *Session { + s := NewSession() + return s.Command(name, a...) +} + +func Echo(in string) *Session { + s := NewSession() + return s.SetInput(in) +} + +func (s *Session) Alias(alias, cmd string, args ...string) { + v := []string{cmd} + v = append(v, args...) + s.alias[alias] = v +} + +func (s *Session) Command(name string, a ...interface{}) *Session { + var args = make([]string, 0) + var sType = reflect.TypeOf("") + + // init cmd, args, dir, envs + // if not init, program may panic + s.inj.Map(name).Map(args).Map(s.dir).Map(map[string]string{}) + for _, v := range a { + switch reflect.TypeOf(v) { + case sType: + args = append(args, v.(string)) + default: + s.inj.Map(v) + } + } + if len(args) != 0 { + s.inj.Map(args) + } + s.inj.Invoke(s.appendCmd) + return s +} + +// combine Command and Run +func (s *Session) Call(name string, a ...interface{}) error { + return s.Command(name, a...).Run() +} + +/* +func (s *Session) Exec(cmd string, args ...string) error { + return s.Call(cmd, args) +} +*/ + +func (s *Session) SetEnv(key, value string) *Session { + s.Env[key] = value + return s +} + +func (s *Session) SetDir(dir string) *Session { + s.dir = Dir(dir) + return s +} + +func (s *Session) SetInput(in string) *Session { + s.Stdin = strings.NewReader(in) + return s +} + +func (s *Session) SetStdin(r io.Reader) *Session { + s.Stdin = r + return s +} + +func (s *Session) SetTimeout(d time.Duration) *Session { + s.timeout = d + return s +} + +func newEnviron(env map[string]string, inherit bool) []string { //map[string]string { + environ := make([]string, 0, len(env)) + if inherit { + for _, line := range os.Environ() { + for k, _ := range env { + if strings.HasPrefix(line, k+"=") { + goto CONTINUE + } + } + environ = append(environ, line) + CONTINUE: + } + } + for k, v := range env { + environ = append(environ, k+"="+v) + } + return environ +} + +func (s *Session) appendCmd(cmd string, args []string, cwd Dir, env map[string]string) { + if s.started { + s.started = false + s.cmds = make([]*exec.Cmd, 0) + } + for k, v := range s.Env { + if _, ok := env[k]; !ok { + env[k] = v + } + } + environ := newEnviron(s.Env, true) // true: inherit sys-env + v, ok := s.alias[cmd] + if ok { + cmd = v[0] + args = append(v[1:], args...) + } + c := exec.Command(cmd, args...) + c.Env = environ + c.Dir = string(cwd) + s.cmds = append(s.cmds, c) +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/sh_test.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/sh_test.go new file mode 100644 index 000000000..be5c27998 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/sh_test.go @@ -0,0 +1,106 @@ +package sh + +import ( + "fmt" + "log" + "runtime" + "strings" + "testing" +) + +func TestAlias(t *testing.T) { + s := NewSession() + s.Alias("gr", "echo", "hi") + out, err := s.Command("gr", "sky").Output() + if err != nil { + t.Error(err) + } + if string(out) != "hi sky\n" { + t.Errorf("expect 'hi sky' but got:%s", string(out)) + } +} + +func ExampleSession_Command() { + s := NewSession() + out, err := s.Command("echo", "hello").Output() + if err != nil { + log.Fatal(err) + } + fmt.Println(string(out)) + // Output: hello +} + +func ExampleSession_Command_pipe() { + s := NewSession() + out, err := s.Command("echo", "hello", "world").Command("awk", "{print $2}").Output() + if err != nil { + log.Fatal(err) + } + fmt.Println(string(out)) + // Output: world +} + +func ExampleSession_Alias() { + s := NewSession() + s.Alias("alias_echo_hello", "echo", "hello") + out, err := s.Command("alias_echo_hello", "world").Output() + if err != nil { + log.Fatal(err) + } + fmt.Println(string(out)) + // Output: hello world +} + +func TestEcho(t *testing.T) { + out, err := Echo("one two three").Command("wc", "-w").Output() + if err != nil { + t.Error(err) + } + if strings.TrimSpace(string(out)) != "3" { + t.Errorf("expect '3' but got:%s", string(out)) + } +} + +func TestSession(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("ignore test on windows") + return + } + session := NewSession() + session.ShowCMD = true + err := session.Call("pwd") + if err != nil { + t.Error(err) + } + out, err := session.SetDir("/").Command("pwd").Output() + if err != nil { + t.Error(err) + } + if string(out) != "/\n" { + t.Errorf("expect /, but got %s", string(out)) + } +} + +/* + #!/bin/bash - + # + export PATH=/usr/bin:/bin + alias ll='ls -l' + cd /usr + if test -d "local" + then + ll local | awk '{print $1, $NF}' | grep bin + fi +*/ +func Example(t *testing.T) { + s := NewSession() + //s.ShowCMD = true + s.Env["PATH"] = "/usr/bin:/bin" + s.SetDir("/bin") + s.Alias("ll", "ls", "-l") + + if s.Test("d", "local") { + //s.Command("ll", []string{"local"}).Command("awk", []string{"{print $1, $NF}"}).Command("grep", []string{"bin"}).Run() + s.Command("ll", "local").Command("awk", "{print $1, $NF}").Command("grep", "bin").Run() + } +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/test.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/test.go new file mode 100644 index 000000000..b9d310527 --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/test.go @@ -0,0 +1,64 @@ +package sh + +import ( + "os" + "path/filepath" +) + +func filetest(name string, modemask os.FileMode) (match bool, err error) { + fi, err := os.Stat(name) + if err != nil { + return + } + match = (fi.Mode() & modemask) == modemask + return +} + +func (s *Session) pwd() string { + dir := string(s.dir) + if dir == "" { + dir, _ = os.Getwd() + } + return dir +} + +func (s *Session) abspath(name string) string { + if filepath.IsAbs(name) { + return name + } + return filepath.Join(s.pwd(), name) +} + +func init() { + //log.SetFlags(log.Lshortfile | log.LstdFlags) +} + +// expression can be dir, file, link +func (s *Session) Test(expression string, argument string) bool { + var err error + var fi os.FileInfo + fi, err = os.Lstat(s.abspath(argument)) + switch expression { + case "d", "dir": + return err == nil && fi.IsDir() + case "f", "file": + return err == nil && fi.Mode().IsRegular() + case "x", "executable": + /* + fmt.Println(expression, argument) + if err == nil { + fmt.Println(fi.Mode()) + } + */ + return err == nil && fi.Mode()&os.FileMode(0100) != 0 + case "L", "link": + return err == nil && fi.Mode()&os.ModeSymlink != 0 + } + return false +} + +// expression can be d,dir, f,file, link +func Test(exp string, arg string) bool { + s := NewSession() + return s.Test(exp, arg) +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/test_test.go b/plugins/network/vendor/github.com/codeskyblue/go-sh/test_test.go new file mode 100644 index 000000000..15225aa9a --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/test_test.go @@ -0,0 +1,58 @@ +package sh_test + +import ( + "testing" + + "github.com/codeskyblue/go-sh" +) + +var s = sh.NewSession() + +type T struct{ *testing.T } + +func NewT(t *testing.T) *T { + return &T{t} +} + +func (t *T) checkTest(exp string, arg string, result bool) { + r := s.Test(exp, arg) + if r != result { + t.Errorf("test -%s %s, %v != %v", exp, arg, r, result) + } +} + +func TestTest(i *testing.T) { + t := NewT(i) + t.checkTest("d", "../go-sh", true) + t.checkTest("d", "./yymm", false) + + // file test + t.checkTest("f", "testdata/hello.txt", true) + t.checkTest("f", "testdata/xxxxx", false) + t.checkTest("f", "testdata/yymm", false) + + // link test + t.checkTest("link", "testdata/linkfile", true) + t.checkTest("link", "testdata/xxxxxlinkfile", false) + t.checkTest("link", "testdata/hello.txt", false) + + // executable test + t.checkTest("x", "testdata/executable", true) + t.checkTest("x", "testdata/xxxxx", false) + t.checkTest("x", "testdata/hello.txt", false) +} + +func ExampleShellTest(t *testing.T) { + // test -L + sh.Test("link", "testdata/linkfile") + sh.Test("L", "testdata/linkfile") + // test -f + sh.Test("file", "testdata/file") + sh.Test("f", "testdata/file") + // test -x + sh.Test("executable", "testdata/binfile") + sh.Test("x", "testdata/binfile") + // test -d + sh.Test("dir", "testdata/dir") + sh.Test("d", "testdata/dir") +} diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/executable b/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/executable new file mode 100755 index 000000000..e69de29bb diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/hello.txt b/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/hello.txt new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/linkfile b/plugins/network/vendor/github.com/codeskyblue/go-sh/testdata/linkfile new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/network/vendor/github.com/codeskyblue/go-sh/wercker.yml b/plugins/network/vendor/github.com/codeskyblue/go-sh/wercker.yml new file mode 100644 index 000000000..72c47020f --- /dev/null +++ b/plugins/network/vendor/github.com/codeskyblue/go-sh/wercker.yml @@ -0,0 +1,28 @@ +box: wercker/golang +# Build definition +build: + # The steps that will be executed on build + steps: + # Sets the go workspace and places you package + # at the right place in the workspace tree + - setup-go-workspace + + # Gets the dependencies + - script: + name: go get + code: | + cd $WERCKER_SOURCE_DIR + go version + go get -t . + + # Build the project + - script: + name: go build + code: | + go build . + + # Test the project + - script: + name: go test + code: | + go test -v ./... diff --git a/plugins/network/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/network/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/network/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/network/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/network/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/network/vendor/github.com/ryanuber/columnize/LICENSE @@ -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. diff --git a/plugins/network/vendor/github.com/ryanuber/columnize/README.md b/plugins/network/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/network/vendor/github.com/ryanuber/columnize/README.md @@ -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. diff --git a/plugins/network/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/network/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/network/vendor/github.com/ryanuber/columnize/columnize.go @@ -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) +} diff --git a/plugins/network/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/network/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/network/vendor/github.com/ryanuber/columnize/columnize_test.go @@ -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 = "" + output := Format(input, config) + + expected := "Column A Column B Column C\n" + expected += "x 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) + } +} diff --git a/plugins/nginx-vhosts/core-post-deploy b/plugins/nginx-vhosts/core-post-deploy index 7bb7db877..493fd2da2 100755 --- a/plugins/nginx-vhosts/core-post-deploy +++ b/plugins/nginx-vhosts/core-post-deploy @@ -9,7 +9,10 @@ nginx_core_post_deploy() { declare desc="nginx-vhosts core-post-deploy plugin trigger" local trigger="nginx_core_post_deploy" local APP="$1" - if [[ -f "$DOKKU_ROOT/$APP/IP.web.1" ]] && [[ -f "$DOKKU_ROOT/$APP/PORT.web.1" ]]; then + local HAS_NETWORK_CONFIG + + HAS_NETWORK_CONFIG="$(plugn trigger network-config-exists "$APP")" + if [[ "$HAS_NETWORK_CONFIG" == "true" ]]; then if [[ "$(is_app_vhost_enabled "$APP")" == "false" ]]; then dokku_log_info1 "VHOST support disabled. Skipping domains setup" elif [[ ! -f "$DOKKU_ROOT/$APP/VHOST" ]]; then diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index 4792550df..8fda14a13 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -4,6 +4,7 @@ source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/certs/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" source "$PLUGIN_AVAILABLE_PATH/domains/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" source "$PLUGIN_AVAILABLE_PATH/ps/functions" get_nginx_location() { @@ -222,22 +223,13 @@ nginx_build_config() { local NGINX_TEMPLATE="$DEFAULT_NGINX_TEMPLATE"; local SCHEME=http local NGINX_TEMPLATE_SOURCE="built-in"; local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" local RAW_TCP_PORTS="$(get_app_raw_tcp_ports "$APP")" + local DOKKU_APP_LISTENERS - local DOKKU_DISABLE_PROXY=$(config_get "$APP" DOKKU_DISABLE_PROXY) local IS_APP_VHOST_ENABLED=$(is_app_vhost_enabled "$APP") - if [[ -z "$DOKKU_DISABLE_PROXY" ]]; then + if [[ "$(is_app_proxy_enabled "$APP")" == "true" ]]; then if [[ -z "$DOKKU_APP_LISTEN_PORT" ]] && [[ -z "$DOKKU_APP_LISTEN_IP" ]]; then - shopt -s nullglob - local DOKKU_APP_IP_FILE - for DOKKU_APP_IP_FILE in $DOKKU_ROOT/$APP/IP.web.*; do - local DOKKU_APP_PORT_FILE="${DOKKU_APP_IP_FILE//IP/PORT}" - local DOKKU_APP_LISTENER_IP=$(< "$DOKKU_APP_IP_FILE") - local DOKKU_APP_LISTENER_PORT=$(< "$DOKKU_APP_PORT_FILE") - local DOKKU_APP_LISTENERS+="$DOKKU_APP_LISTENER_IP:$DOKKU_APP_LISTENER_PORT " - done - local DOKKU_APP_LISTENERS="$(echo "$DOKKU_APP_LISTENERS" | xargs)" - shopt -u nullglob + DOKKU_APP_LISTENERS="$(plugn trigger network-get-listeners "$APP" | xargs)" elif [[ -n "$DOKKU_APP_LISTEN_PORT" ]] && [[ -n "$DOKKU_APP_LISTEN_IP" ]]; then local PASSED_LISTEN_IP_PORT=true fi @@ -341,7 +333,7 @@ nginx_build_config() { xargs -i echo "$SCHEME://{}" <<< "$(echo "${SSL_VHOSTS}" "${NONSSL_VHOSTS}" | tr ' ' '\n' | sort -u)" >> "$URLS_PATH" fi else - # note because this clause is long. if $DOKKU_DISABLE_PROXY is set: + # note because this clause is long. if the proxy is disabled: dokku_log_info1 "nginx support is disabled for app ($APP)." if [[ -f "$DOKKU_ROOT/$APP/nginx.conf" ]]; then dokku_log_info1 "deleting nginx.conf" diff --git a/plugins/nginx-vhosts/post-domains-update b/plugins/nginx-vhosts/post-domains-update index 6201175b7..29a590963 100755 --- a/plugins/nginx-vhosts/post-domains-update +++ b/plugins/nginx-vhosts/post-domains-update @@ -6,9 +6,12 @@ source "$PLUGIN_AVAILABLE_PATH/proxy/functions" nginx_post_domains_update() { declare desc="calls nginx build_config when domains are updated" - local trigger="nginx_post_domains_update" - if [[ "$(get_app_proxy_type "$1")" == "nginx" ]]; then - nginx_build_config "$1" + declare trigger="nginx_post_domains_update" + 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 + nginx_build_config "$APP" fi } diff --git a/plugins/nginx-vhosts/proxy-build-config b/plugins/nginx-vhosts/proxy-build-config new file mode 100755 index 000000000..0253f7181 --- /dev/null +++ b/plugins/nginx-vhosts/proxy-build-config @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" + +nginx_proxy_build_config() { + declare desc="build nginx config to proxy app containers from command line" + declare trigger="nginx_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 + plugn trigger network-build-config "$APP" + nginx_build_config "$APP" + fi +} + +nginx_proxy_build_config "$@" diff --git a/plugins/nginx-vhosts/subcommands/build-config b/plugins/nginx-vhosts/subcommands/build-config index 74b118da2..88b3b6af5 100755 --- a/plugins/nginx-vhosts/subcommands/build-config +++ b/plugins/nginx-vhosts/subcommands/build-config @@ -1,14 +1,21 @@ #!/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/nginx-vhosts/functions" +source "$PLUGIN_AVAILABLE_PATH/proxy/functions" nginx_build_config_cmd() { declare desc="build nginx config to proxy app containers from command line" + declare APP="$2" local cmd="nginx:build-config" - [[ -z $2 ]] && dokku_log_fail "Please specify an app to run the command on" - nginx_build_config "$2" + [[ -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")" + + if [[ "$PROXY_APP_TYPE" == "nginx" ]]; then + plugn trigger proxy-build-config "$APP" + else + dokku_log_fail "Configured proxy for ${APP} is ${PROXY_APP_TYPE}" + fi } nginx_build_config_cmd "$@" diff --git a/plugins/proxy/proxy.go b/plugins/proxy/proxy.go new file mode 100644 index 000000000..423e09f5a --- /dev/null +++ b/plugins/proxy/proxy.go @@ -0,0 +1,21 @@ +package proxy + +import ( + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/config" +) + +// IsAppProxyEnabled returns true if proxy is enabled; otherwise return false +func IsAppProxyEnabled(appName string) bool { + err := common.VerifyAppName(appName) + if err != nil { + common.LogFail(err.Error()) + } + + proxyEnabled := true + disableProxy := config.GetWithDefault(appName, "DOKKU_DISABLE_PROXY", "") + if disableProxy != "" { + proxyEnabled = false + } + return proxyEnabled +} diff --git a/plugins/ps/functions b/plugins/ps/functions index f9f094df8..25f6ce401 100755 --- a/plugins/ps/functions +++ b/plugins/ps/functions @@ -98,6 +98,7 @@ ps_start() { else echo "App $APP already running" fi + plugn trigger proxy-build-config "$APP" else echo "App $APP has not been deployed" fi diff --git a/plugins/repo/src/commands/commands.go b/plugins/repo/src/commands/commands.go index a01d546d5..e0d3862b4 100644 --- a/plugins/repo/src/commands/commands.go +++ b/plugins/repo/src/commands/commands.go @@ -29,7 +29,7 @@ func main() { cmd := flag.Arg(0) switch cmd { - case "repo:help": + case "repo", "repo:help": usage() case "help": fmt.Print(helpContent) diff --git a/plugins/repo/src/subcommands/gc/gc.go b/plugins/repo/src/subcommands/gc/gc.go index 7787385b8..1edcc741a 100644 --- a/plugins/repo/src/subcommands/gc/gc.go +++ b/plugins/repo/src/subcommands/gc/gc.go @@ -4,7 +4,7 @@ import ( "flag" "strings" - common "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/common" ) // runs 'git gc --aggressive' against the application's repo diff --git a/plugins/repo/src/subcommands/purge-cache/purge-cache.go b/plugins/repo/src/subcommands/purge-cache/purge-cache.go index 863d4d46c..29b04cd75 100644 --- a/plugins/repo/src/subcommands/purge-cache/purge-cache.go +++ b/plugins/repo/src/subcommands/purge-cache/purge-cache.go @@ -5,7 +5,7 @@ import ( "os" "strings" - common "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/common" ) // deletes the contents of the build cache stored in the repository diff --git a/tests.mk b/tests.mk index 2c325f064..4db82d12a 100644 --- a/tests.mk +++ b/tests.mk @@ -5,7 +5,9 @@ ifneq ($(shell shellcheck --version > /dev/null 2>&1 ; echo $$?),0) ifeq ($(SYSTEM),Darwin) brew install shellcheck else + sudo apt-key adv --keyserver pgp.mit.edu --recv-keys 5072E1F5 sudo add-apt-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse' + sudo rm -rf /var/lib/apt/lists/* && sudo apt-get clean sudo apt-get update -qq && sudo apt-get install -qq -y shellcheck endif endif @@ -81,7 +83,9 @@ ci-go-coverage: -v $$PWD:$(GO_REPO_ROOT) \ -w $(GO_REPO_ROOT) \ $(BUILD_IMAGE) \ - bash -c "go get github.com/schrej/godacov && godacov -t $$CODACY_TOKEN -r ./coverage.out -c $$CIRCLE_SHA1" || exit $$? + bash -c "go get github.com/onsi/gomega github.com/schrej/godacov github.com/haya14busa/goverage && \ + go list ./... | egrep -v '/vendor/|/tests/apps/' | xargs goverage -v -coverprofile=coverage.out && \ + godacov -t $$CODACY_TOKEN -r ./coverage.out -c $$CIRCLE_SHA1" || exit $$? go-tests: @echo running go unit tests... @@ -90,9 +94,8 @@ go-tests: -v $$PWD:$(GO_REPO_ROOT) \ -w $(GO_REPO_ROOT) \ $(BUILD_IMAGE) \ - bash -c "go get github.com/onsi/gomega github.com/haya14busa/goverage && \ - go list ./... | egrep -v '/vendor/|/tests/apps/' | xargs go test -v -p 1 -race && \ - go list ./... | egrep -v '/vendor/|/tests/apps/' | xargs goverage -v -coverprofile=coverage.out" || exit $$? + bash -c "go get github.com/onsi/gomega && \ + go list ./... | egrep -v '/vendor/|/tests/apps/' | xargs go test -v -p 1 -race" || exit $$? unit-tests: go-tests @echo running bats unit tests... diff --git a/tests/unit/10_ps-herokuish.bats b/tests/unit/10_ps-herokuish.bats index d3c00c001..6be9d85fb 100644 --- a/tests/unit/10_ps-herokuish.bats +++ b/tests/unit/10_ps-herokuish.bats @@ -137,7 +137,7 @@ teardown() { echo "status: "$status assert_success - run bash -c "dokku apps" + run bash -c "dokku apps:list" echo "output: "$output echo "status: "$status assert_success diff --git a/tests/unit/20_network.bats b/tests/unit/20_network.bats new file mode 100644 index 000000000..80e35203e --- /dev/null +++ b/tests/unit/20_network.bats @@ -0,0 +1,69 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + global_setup + [[ -f "$DOKKU_ROOT/VHOST" ]] && cp -fp "$DOKKU_ROOT/VHOST" "$DOKKU_ROOT/VHOST.bak" + [[ -f "$DOKKU_ROOT/HOSTNAME" ]] && cp -fp "$DOKKU_ROOT/HOSTNAME" "$DOKKU_ROOT/HOSTNAME.bak" + create_app +} + +teardown() { + destroy_app 0 $TEST_APP + [[ -f "$DOKKU_ROOT/VHOST.bak" ]] && mv "$DOKKU_ROOT/VHOST.bak" "$DOKKU_ROOT/VHOST" && chown dokku:dokku "$DOKKU_ROOT/VHOST" + [[ -f "$DOKKU_ROOT/HOSTNAME.bak" ]] && mv "$DOKKU_ROOT/HOSTNAME.bak" "$DOKKU_ROOT/HOSTNAME" && chown dokku:dokku "$DOKKU_ROOT/HOSTNAME" + global_teardown +} + +assert_nonssl_domain() { + local domain=$1 + assert_app_domain "${domain}" + assert_http_success "http://${domain}" +} + +assert_app_domain() { + local domain=$1 + run /bin/bash -c "dokku domains $TEST_APP 2> /dev/null | grep -xF ${domain}" + echo "output: "$output + echo "status: "$status + assert_output "${domain}" +} + +assert_external_port() { + local CID="$1"; local exit_status="$2" + local EXTERNAL_PORT_COUNT=$(docker port $CID | wc -l) + run /bin/bash -c "[[ $EXTERNAL_PORT_COUNT -gt 0 ]]" + if [[ "$exit_status" == "success" ]]; then + assert_success + else + assert_failure + fi +} + +@test "(proxy) network:set bind-all-interfaces" { + deploy_app + assert_nonssl_domain "${TEST_APP}.dokku.me" + + run dokku network:set $TEST_APP bind-all-interfaces true + run dokku ps:rebuild $TEST_APP + echo "output: "$output + echo "status: "$status + assert_success + assert_http_success "${TEST_APP}.dokku.me" + + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.web.*; do + assert_external_port $(< $CID_FILE) success + done + + run dokku network:set $TEST_APP bind-all-interfaces false + run dokku ps:rebuild $TEST_APP + echo "output: "$output + echo "status: "$status + assert_success + assert_http_success "${TEST_APP}.dokku.me" + + for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.web.*; do + assert_external_port $(< $CID_FILE) failure + done +} diff --git a/tests/unit/30_client.bats b/tests/unit/30_client.bats index 7f1d97f00..2914688e1 100644 --- a/tests/unit/30_client.bats +++ b/tests/unit/30_client.bats @@ -23,7 +23,7 @@ teardown() { } @test "(client) no args should print help" { - run /bin/bash -c "./contrib/dokku_client.sh | head -1 | egrep -q '^Usage: dokku \[.+\] COMMAND .*'" + run /bin/bash -c "./contrib/dokku_client.sh | head -1 | grep -E 'Usage: dokku \[.+\] COMMAND .*'" echo "output: "$output echo "status: "$status assert_success diff --git a/tests/unit/40_proxy.bats b/tests/unit/40_proxy.bats index 60ea5037f..fc733fe9d 100644 --- a/tests/unit/40_proxy.bats +++ b/tests/unit/40_proxy.bats @@ -51,7 +51,7 @@ assert_external_port() { assert_success for CID_FILE in $DOKKU_ROOT/$TEST_APP/CONTAINER.web.*; do - assert_external_port $(< $CID_FILE) success + assert_external_port $(< $CID_FILE) failure done run dokku proxy:enable $TEST_APP