diff --git a/docs/appendices/0.20.0-migration-guide.md b/docs/appendices/0.20.0-migration-guide.md index e217f8d7b..f42c3a4fe 100644 --- a/docs/appendices/0.20.0-migration-guide.md +++ b/docs/appendices/0.20.0-migration-guide.md @@ -45,8 +45,20 @@ Dokku will no longer distribute packages for Debian versions that are not `stabl - Debian 7 (wheezy) - Debian 8 (jessie) +### App report flags + +The following changes on the `apps:report` command were made in this release + +- The `--git-sha` flag was moved from the `apps:report` command to the `git:report` command. +- The `--deploy-source` flag was renamed to `--app-deploy-source`. +- The `--locked` flag was renamed to `--app-locked`. + ## Deprecations - `nginx:show-conf` has been deprecated in favor of `nginx:show-config`. - `proxy#is_app_proxy_enabled()` is deprecated in favor of `plugn trigger proxy-is-enabled` - `proxy#get_app_proxy_type()` is deprecated in favor of `plugn trigger proxy-type` +- `apps#apps_create()` is deprecated in favor of `plugn trigger app-create` +- `apps#apps_destroy()` is deprecated in favor of `plugn trigger app-destroy` +- `apps#apps_exists()` is deprecated in favor of `plugn trigger app-exists` +- `apps#apps_maybe_create()` is deprecated in favor of `plugn trigger app-maybe-create` diff --git a/docs/deployment/application-management.md b/docs/deployment/application-management.md index b738caede..0ba5e7fd1 100644 --- a/docs/deployment/application-management.md +++ b/docs/deployment/application-management.md @@ -236,16 +236,14 @@ dokku apps:report ``` =====> node-js-app App dir: /home/dokku/node-js-app - Git sha: dbddc3f - Deploy source: git - Locked: false + App deploy source: git + App locked: false =====> python-sample not deployed =====> ruby-sample App dir: /home/dokku/ruby-sample - Git sha: a2d477c - Deploy source: git - Locked: false + App deploy source: git + App locked: false ``` You can run the command for a specific app also. @@ -257,13 +255,12 @@ dokku apps:report node-js-app ``` =====> node-js-app App dir: /home/dokku/node-js-app - Git sha: dbddc3f - Deploy source: git - Locked: false + App deploy source: git + App locked: false ``` You can pass flags which will output only the value of the specific information you want. For example: ```shell -dokku apps:report node-js-app --git-sha +dokku apps:report node-js-app --app-dir ``` diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index af37aac05..27780a8a2 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -29,6 +29,66 @@ The following plugin triggers describe those available to a Dokku installation. > The example plugin trigger code is not guaranteed to be implemented as in within dokku, and are merely simplified examples. Please look at the Dokku source for larger, more in-depth examples. +### `app-create` + +- Description: Creates an app +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `app-destroy` + +- Description: Destroys an app (with confirmation if force isn't specified) +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `app-exists` + +- Description: Creates an app +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `app-maybe-create` + +- Description: Creates an app (gated by whether this is globally enabled or not) +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `app-restart` - Description: Triggers an app restart @@ -201,6 +261,36 @@ help_content esac ``` +### `config-get` + +- Description: Fetches the app config value for a key +- Invoked by: +- Arguments: `$APP $KEY` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + +### `config-get-global` + +- Description: Fetches the global config value for a key +- Invoked by: +- Arguments: `$KEY` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `core-post-deploy` > To avoid issues with community plugins, this plugin trigger should be used *only* for core plugins. Please avoid using this trigger in your own plugins. diff --git a/plugins/20_events/app-create b/plugins/20_events/app-create new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/app-create @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/app-destroy b/plugins/20_events/app-destroy new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/app-destroy @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/app-exists b/plugins/20_events/app-exists new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/app-exists @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/app-maybe-create b/plugins/20_events/app-maybe-create new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/app-maybe-create @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/config-get b/plugins/20_events/config-get new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/config-get @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/20_events/config-get-global b/plugins/20_events/config-get-global new file mode 120000 index 000000000..5178a749f --- /dev/null +++ b/plugins/20_events/config-get-global @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/apps/.gitignore b/plugins/apps/.gitignore new file mode 100644 index 000000000..fbeada67e --- /dev/null +++ b/plugins/apps/.gitignore @@ -0,0 +1,7 @@ +/commands +/subcommands/* +/triggers/* +/triggers +/app-* +/post-* +/report diff --git a/plugins/apps/Makefile b/plugins/apps/Makefile new file mode 100644 index 000000000..12aea8f65 --- /dev/null +++ b/plugins/apps/Makefile @@ -0,0 +1,6 @@ +SUBCOMMANDS = subcommands/clone subcommands/create subcommands/destroy subcommands/exists subcommands/list subcommands/lock subcommands/locked subcommands/rename subcommands/report subcommands/unlock +TRIGGERS = triggers/app-create triggers/app-destroy triggers/app-exists triggers/app-maybe-create triggers/post-delete triggers/report +BUILD = commands subcommands triggers +PLUGIN_NAME = apps + +include ../../common.mk diff --git a/plugins/apps/apps.go b/plugins/apps/apps.go new file mode 100644 index 000000000..2c822ddae --- /dev/null +++ b/plugins/apps/apps.go @@ -0,0 +1,32 @@ +package apps + +import ( + "github.com/dokku/dokku/plugins/common" +) + +// ReportSingleApp is an internal function that displays the app report for one or more apps +func ReportSingleApp(appName, infoFlag string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + deploySource := "" + if b, err := common.PlugnTriggerSetup("deploy-source", []string{appName}...).SetInput("").Output(); err != nil { + deploySource = string(b[:]) + } + + locked := "false" + if appIsLocked(appName) { + locked = "true" + } + + infoFlags := map[string]string{ + "--app-dir": common.AppRoot(appName), + "--app-deploy-source": deploySource, + "--app-locked": locked, + } + + trimPrefix := false + uppercaseFirstCharacter := true + return common.ReportSingleApp("app", appName, infoFlag, infoFlags, trimPrefix, uppercaseFirstCharacter) +} diff --git a/plugins/apps/commands b/plugins/apps/commands deleted file mode 100755 index d29019c87..000000000 --- a/plugins/apps/commands +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -[[ " help apps:help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" -source "$PLUGIN_AVAILABLE_PATH/apps/help-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -case "$1" in - help | apps:help) - cmd-apps-help "$@" - ;; - - *) - exit "$DOKKU_NOT_IMPLEMENTED_EXIT" - ;; - -esac diff --git a/plugins/apps/functions b/plugins/apps/functions index abd9521b1..0865982f8 100755 --- a/plugins/apps/functions +++ b/plugins/apps/functions @@ -7,63 +7,35 @@ source "$PLUGIN_AVAILABLE_PATH/config/functions" apps_create() { declare desc="verifies app name and creates an app" declare APP="$1" - is_valid_app_name "$APP" + declare deprecated=true - apps_exists "$APP" >/dev/null 2>&1 && dokku_log_fail "Name is already taken" - - mkdir -p "$DOKKU_ROOT/$APP" - dokku_log_info1_quiet "Creating $APP... done" - plugn trigger post-create "$APP" + dokku_log_warn "Deprecated: plugn#app-create" + plugn trigger app-create "$APP" } apps_destroy() { declare desc="destroys an app" declare APP="$1" - verify_app_name "$APP" - local IMAGE_TAG=$(get_running_image_tag "$APP") + declare deprecated=true - if [[ -z "$DOKKU_APPS_FORCE_DELETE" ]]; then - dokku_log_warn "WARNING: Potentially Destructive Action" - dokku_log_warn "This command will destroy $APP (including all add-ons)." - dokku_log_warn "To proceed, type \"$APP\"" - echo "" - - read -rp "> " app_name - if [[ "$app_name" != "$APP" ]]; then - dokku_log_fail "Confirmation did not match $APP. Aborted." - fi - fi - - dokku_log_info1 "Destroying $APP (including all add-ons)" - plugn trigger pre-delete "$APP" "$IMAGE_TAG" - - local DOKKU_SCHEDULER=$(get_app_scheduler "$APP") - local REMOVE_CONTAINERS="true" - plugn trigger scheduler-stop "$DOKKU_SCHEDULER" "$APP" "$REMOVE_CONTAINERS" - plugn trigger scheduler-post-delete "$DOKKU_SCHEDULER" "$APP" "$IMAGE_TAG" - plugn trigger post-delete "$APP" "$IMAGE_TAG" - docker_cleanup "$APP" "true" + dokku_log_warn "Deprecated: plugn#app-destroy" + plugn trigger app-destroy "$APP" } apps_exists() { declare desc="checks if an app exists" declare APP="$1" - is_valid_app_name "$APP" + declare deprecated=true - [[ -d "$DOKKU_ROOT/$APP" ]] || return 1 + dokku_log_warn "Deprecated: plugn#app-exists" + plugn trigger app-exists "$APP" } apps_maybe_create() { declare desc="creates an app dir if allowed" declare APP="$1" + declare deprecated=true - if ! apps_exists "$APP" >/dev/null 2>&1; then - DOKKU_GLOBAL_DISABLE_AUTOCREATE="$(config_get --global DOKKU_DISABLE_APP_AUTOCREATION || true)" - if [[ "$DOKKU_GLOBAL_DISABLE_AUTOCREATE" == "true" ]]; then - dokku_log_warn "App auto-creation disabled." - dokku_log_fail " ! Re-enable app auto-creation or create an app with 'dokku apps:create ${APP}'" - else - suppress_output apps_create "$APP" - fi - fi + dokku_log_warn "Deprecated: plugn#app-maybe-create" + plugn trigger app-maybe-create "$APP" } diff --git a/plugins/apps/functions.go b/plugins/apps/functions.go new file mode 100644 index 000000000..adc7e960d --- /dev/null +++ b/plugins/apps/functions.go @@ -0,0 +1,172 @@ +package apps + +import ( + "bytes" + "errors" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// checks if an app exists +func appExists(appName string) error { + if err := common.IsValidAppName(appName); err != nil { + return err + } + + if !common.DirectoryExists(common.AppRoot(appName)) { + return fmt.Errorf("App %s does not exist", appName) + } + + return nil +} + +// checks if an app is locked +func appIsLocked(appName string) bool { + lockfilePath := fmt.Sprintf("%v/.deploy.lock", common.AppRoot(appName)) + _, err := os.Stat(lockfilePath) + return !os.IsNotExist(err) +} + +// verifies app name and creates an app +func createApp(appName string) error { + if err := common.IsValidAppName(appName); err != nil { + return err + } + + if err := appExists(appName); err == nil { + return errors.New("Name is already taken") + } + + common.LogInfo1Quiet(fmt.Sprintf("Creating %s...", appName)) + os.MkdirAll(common.AppRoot(appName), 0755) + + if err := common.PlugnTrigger("post-create", []string{appName}...); err != nil { + return err + } + + return nil +} + +// destroys an app +func destroyApp(appName string) error { + if err := common.VerifyAppName(appName); err != nil { + return err + } + + if os.Getenv("DOKKU_APPS_FORCE_DELETE") != "1" { + if err := common.AskForDestructiveConfirmation(appName, "app"); err != nil { + return err + } + } + + common.LogInfo1(fmt.Sprintf("Destroying %s (including all add-ons)", appName)) + + imageTag, _ := common.GetRunningImageTag(appName) + if err := common.PlugnTrigger("pre-delete", []string{appName, imageTag}...); err != nil { + return err + } + + scheduler := common.GetAppScheduler(appName) + removeContainers := "true" + if err := common.PlugnTrigger("scheduler-stop", []string{scheduler, appName, removeContainers}...); err != nil { + return err + } + if err := common.PlugnTrigger("scheduler-post-delete", []string{scheduler, appName, imageTag}...); err != nil { + return err + } + if err := common.PlugnTrigger("post-delete", []string{appName, imageTag}...); err != nil { + return err + } + + forceCleanup := true + common.DockerCleanup(appName, forceCleanup) + return nil +} + +func getAppName(args []string) (appName string, err error) { + if len(args) >= 1 { + appName = args[0] + } else { + err = errors.New("Please specify an app to run the command on") + } + + return +} + +func getNewAppName(args []string) (appName string, err error) { + if len(args) >= 2 { + appName = args[1] + } else { + err = errors.New("Please specify an new app name") + } + + return +} + +func listImagesByAppLabel(appName string) ([]string, error) { + command := []string{ + common.DockerBin(), + "image", + "list", + "--quiet", + "--filter", + fmt.Sprintf("label=com.dokku.app-name=%v", appName), + } + + var stderr bytes.Buffer + listCmd := common.NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +func listImagesByImageRepo(imageRepo string) ([]string, error) { + command := []string{ + common.DockerBin(), + "image", + "list", + "--quiet", + imageRepo, + } + + var stderr bytes.Buffer + listCmd := common.NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +// creates an app if allowed +func maybeCreateApp(appName string) error { + if err := appExists(appName); err == nil { + return nil + } + + b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_DISABLE_APP_AUTOCREATION"}...) + disableAutocreate := string(b[:]) + if disableAutocreate == "true" { + common.LogWarn("App auto-creation disabled.") + return fmt.Errorf("Re-enable app auto-creation or create an app with 'dokku apps:create %s'", appName) + } + + return common.SuppressOutput(func() error { + return createApp(appName) + }) +} diff --git a/plugins/apps/glide.yaml b/plugins/apps/glide.yaml new file mode 100644 index 000000000..a720d6c11 --- /dev/null +++ b/plugins/apps/glide.yaml @@ -0,0 +1,4 @@ +package: . +import: +- package: github.com/codeskyblue/go-sh +- package: github.com/ryanuber/columnize diff --git a/plugins/apps/help-functions b/plugins/apps/help-functions deleted file mode 100755 index b365c03c8..000000000 --- a/plugins/apps/help-functions +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-apps-help() { - declare desc="help command" - declare CMD="$1" - local plugin_name="apps" - local plugin_description="Manage apps" - - if [[ "$CMD" == "$plugin_name:help" ]]; then - echo -e "Usage: dokku $plugin_name[:COMMAND]" - echo '' - echo "$plugin_description" - echo '' - echo 'Additional commands:' - fn-help-content | sort | column -c2 -t -s, - elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then - fn-help-content - else - cat < , Clones an app - apps:create , Create a new app - apps:destroy , Permanently destroy an app - apps:exists , Checks if an app exists - apps:list, List your apps - apps:lock , Locks an app for deployment - apps:locked , Checks if an app is locked for deployment - apps:rename , Rename an app - apps:report [] [], Display report about an app - apps:unlock , Unlocks an app for deployment -help_content -} diff --git a/plugins/apps/internal-functions b/plugins/apps/internal-functions deleted file mode 100755 index 87f5546aa..000000000 --- a/plugins/apps/internal-functions +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-apps-report() { - declare desc="displays the app report for one or more apps" - declare cmd="apps:report" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" INFO_FLAG="$2" - local INSTALLED_APPS=$(dokku_apps) - - if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then - INFO_FLAG="$APP" - APP="" - fi - - if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then - INFO_FLAG="true" - fi - - if [[ -z "$APP" ]]; then - for app in $INSTALLED_APPS; do - cmd-apps-report-single "$app" "$INFO_FLAG" | tee || true - done - else - cmd-apps-report-single "$APP" "$INFO_FLAG" - fi -} - -cmd-apps-report-single() { - declare APP="$1" INFO_FLAG="$2" - local APP_DIR="$DOKKU_ROOT/$APP" - local use_echo - if [[ "$INFO_FLAG" == "true" ]]; then - use_echo=true - INFO_FLAG="" - fi - verify_app_name "$APP" - local flag_map=( - "--app-dir: $APP_DIR" - "--git-sha: $(GIT_DIR="$APP_DIR" git rev-parse --short HEAD 2>/dev/null || false)" - "--deploy-source: $(: | plugn trigger deploy-source "$APP")" - "--locked: $(apps_is_locked "$APP")" - ) - - if [[ -z "$INFO_FLAG" ]]; then - dokku_log_info2_quiet "$APP app information" - if (is_deployed "$APP"); then - for flag in "${flag_map[@]}"; do - key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" - dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" - done - else - if [[ "$use_echo" ]]; then - echo "not deployed" - else - dokku_log_exit_quiet "not deployed" - fi - fi - else - local match=false - local value_exists=false - for flag in "${flag_map[@]}"; do - valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" - if [[ "$flag" == "${INFO_FLAG}:"* ]]; then - value=${flag#*: } - size="${#value}" - if [[ "$size" -ne 0 ]]; then - echo "$value" && match=true && value_exists=true - else - match=true - fi - fi - done - [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" - [[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed" - fi -} - -cmd-apps-list() { - declare desc="lists all apps" - declare cmd="apps" - [[ "$1" == "$cmd" ]] && shift 1 - local app - - dokku_log_info2_quiet "My Apps" - for app in $(dokku_apps); do - echo "$app" - done -} - -apps_is_locked() { - declare desc="check if an app is locked" - declare APP="$1" - local LOCKED=false - - if [[ -f "$DOKKU_ROOT/$APP/.deploy.lock" ]]; then - LOCKED=true - fi - - echo "$LOCKED" -} diff --git a/plugins/apps/post-delete b/plugins/apps/post-delete deleted file mode 100755 index 3d44ed4b8..000000000 --- a/plugins/apps/post-delete +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" - -trigger-apps-post-delete() { - declare desc="apps post-delete plugin trigger" - declare trigger="post-delete" - declare APP="$1" - local IMAGE_REPO=$(get_app_image_repo "$APP") - - if [[ -n "$APP" ]]; then - # remove contents for apps that are symlinks to other folders - rm -rf "${DOKKU_ROOT:?}/$APP/" >/dev/null - # then remove the folder and/or the symlink - rm -rf "${DOKKU_ROOT:?}/$APP" >/dev/null - fi - - # shellcheck disable=SC2046 - "$DOCKER_BIN" image rm $("$DOCKER_BIN" image list --quiet "$IMAGE_REPO" | xargs) &>/dev/null || true -} - -trigger-apps-post-delete "$@" diff --git a/plugins/apps/report b/plugins/apps/report deleted file mode 100755 index 22829f600..000000000 --- a/plugins/apps/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-apps-report-single "$@" diff --git a/plugins/apps/src/commands/commands.go b/plugins/apps/src/commands/commands.go new file mode 100644 index 000000000..e0313de10 --- /dev/null +++ b/plugins/apps/src/commands/commands.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" + columnize "github.com/ryanuber/columnize" +) + +const ( + helpHeader = `Usage: dokku apps[:COMMAND] + +Manage apps + +Additional commands:` + + helpContent = ` + apps:clone , Clones an app + apps:create , Create a new app + apps:destroy , Permanently destroy an app + apps:exists , Checks if an app exists + apps:list, List your apps + apps:lock , Locks an app for deployment + apps:locked , Checks if an app is locked for deployment + apps:rename , Rename an app + apps:report [] [], Display report about an app + apps:unlock , Unlocks an app for deployment +` +) + +func main() { + flag.Usage = usage + flag.Parse() + + cmd := flag.Arg(0) + switch cmd { + case "apps", "apps:help": + usage() + case "help": + command := common.NewShellCmd(fmt.Sprintf("ps -o command= %d", os.Getppid())) + command.ShowOutput = false + output, err := command.Output() + + if err == nil && strings.Contains(string(output), "--all") { + fmt.Println(helpContent) + } else { + fmt.Print("\n apps, Manage apps\n") + } + default: + dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT")) + if err != nil { + fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable") + dokkuNotImplementExitCode = 10 + } + os.Exit(dokkuNotImplementExitCode) + } +} + +func usage() { + config := columnize.DefaultConfig() + config.Delim = "," + config.Prefix = " " + config.Empty = "" + content := strings.Split(helpContent, "\n")[1:] + fmt.Println(helpHeader) + fmt.Println(columnize.Format(content, config)) +} diff --git a/plugins/apps/src/glide.lock b/plugins/apps/src/glide.lock new file mode 100644 index 000000000..fe4a98f07 --- /dev/null +++ b/plugins/apps/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/apps/src/glide.yaml b/plugins/apps/src/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/apps/src/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/apps/src/subcommands/subcommands.go b/plugins/apps/src/subcommands/subcommands.go new file mode 100644 index 000000000..17cd59c60 --- /dev/null +++ b/plugins/apps/src/subcommands/subcommands.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/apps" + "github.com/dokku/dokku/plugins/common" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + case "clone": + args := flag.NewFlagSet("apps:clone", flag.ExitOnError) + skipDeploy := args.Bool("skip-deploy", false, "--skip-deploy: skip deploy of the new app") + ignoreExisting := args.Bool("ignore-existing", false, "--ignore-existing: exit 0 if new app already exists") + args.Parse(os.Args[2:]) + err = apps.CommandClone(args.Args(), *skipDeploy, *ignoreExisting) + case "create": + args := flag.NewFlagSet("apps:create", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandCreate(args.Args()) + case "destroy": + args := flag.NewFlagSet("apps:destroy", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandDestroy(args.Args()) + case "exists": + args := flag.NewFlagSet("apps:exists", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandExists(args.Args()) + case "list": + args := flag.NewFlagSet("apps:list", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandList(args.Args()) + case "lock": + args := flag.NewFlagSet("apps:lock", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandLock(args.Args()) + case "locked": + args := flag.NewFlagSet("apps:locked", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandLocked(args.Args()) + case "rename": + args := flag.NewFlagSet("apps:rename", flag.ExitOnError) + skipDeploy := args.Bool("skip-deploy", false, "--skip-deploy: skip deploy of the new app") + args.Parse(os.Args[2:]) + err = apps.CommandRename(args.Args(), *skipDeploy) + case "report": + args := flag.NewFlagSet("apps:report", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandReport(args.Args()) + case "unlock": + args := flag.NewFlagSet("apps:unlock", flag.ExitOnError) + args.Parse(os.Args[2:]) + err = apps.CommandUnlock(args.Args()) + default: + common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/apps/src/triggers/triggers.go b/plugins/apps/src/triggers/triggers.go new file mode 100644 index 000000000..1952a3b56 --- /dev/null +++ b/plugins/apps/src/triggers/triggers.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/apps" + "github.com/dokku/dokku/plugins/common" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "app-create": + appName := flag.Arg(0) + err = apps.TriggerAppCreate(appName) + case "app-destroy": + appName := flag.Arg(0) + err = apps.TriggerAppDestroy(appName) + case "app-exists": + appName := flag.Arg(0) + err = apps.TriggerAppExists(appName) + case "app-maybe-create": + appName := flag.Arg(0) + err = apps.TriggerAppMaybeCreate(appName) + case "post-delete": + appName := flag.Arg(0) + err = apps.TriggerPostDelete(appName) + case "report": + appName := flag.Arg(0) + err = apps.ReportSingleApp(appName, "") + default: + common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/apps/src/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/apps/src/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/apps/src/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/apps/src/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/apps/src/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/apps/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/apps/src/vendor/github.com/ryanuber/columnize/README.md b/plugins/apps/src/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/apps/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/apps/src/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/apps/src/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/apps/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/apps/src/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/apps/src/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/apps/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/apps/subcommands.go b/plugins/apps/subcommands.go new file mode 100644 index 000000000..6e06573ea --- /dev/null +++ b/plugins/apps/subcommands.go @@ -0,0 +1,271 @@ +package apps + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandClone clones an app +func CommandClone(args []string, skipDeploy bool, ignoreExisting bool) error { + oldAppName, err := getAppName(args) + if err != nil { + return err + } + + newAppName, err := getNewAppName(args) + if err != nil { + return err + } + + if err = common.IsValidAppName(oldAppName); err != nil { + return err + } + + if err = common.IsValidAppName(newAppName); err != nil { + return err + } + + if err = appExists(oldAppName); err != nil { + return err + } + + if err = appExists(newAppName); err == nil { + if ignoreExisting { + common.LogWarn("Name is already taken") + return nil + } + + return errors.New("Name is already taken") + } + + common.LogInfo1Quiet(fmt.Sprintf("Cloning %s to %s", oldAppName, newAppName)) + if err = createApp(newAppName); err != nil { + return err + } + + if err = common.PlugnTrigger("post-app-clone-setup", []string{oldAppName, newAppName}...); err != nil { + return err + } + + if skipDeploy { + os.Setenv("SKIP_REBUILD", "true") + } + + if err = common.PlugnTrigger("post-app-clone", []string{oldAppName, newAppName}...); err != nil { + return err + } + + return nil +} + +// CommandCreate creates app via command line +func CommandCreate(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + return createApp(appName) +} + +// CommandDestroy destroys an app +func CommandDestroy(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + if appName == "tls" { + return errors.New("Unable to destroy tls directory") + } + + if len(args) >= 2 { + force := args[1] + if force == "force" { + os.Setenv("DOKKU_APPS_FORCE_DELETE", "1") + } + } + + return destroyApp(appName) +} + +// CommandExists checks if an app exists +func CommandExists(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + return appExists(appName) +} + +// CommandList lists all apps +func CommandList(args []string) error { + common.LogInfo2Quiet("My Apps") + apps, err := common.DokkuApps() + if err != nil { + return err + } + + for _, appName := range apps { + common.Log(appName) + } + + return nil +} + +// CommandLock locks an app for deployment +func CommandLock(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + lockfilePath := fmt.Sprintf("%v/.deploy.lock", common.AppRoot(appName)) + if _, err = os.Create(lockfilePath); err != nil { + return errors.New("Unable to create deploy lock") + } + + common.LogInfo1("Deploy lock created") + return nil +} + +// CommandLocked checks if an app is locked for deployment +func CommandLocked(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + if appIsLocked(appName) { + common.LogQuiet("Deploy lock exists") + return nil + + } + return errors.New("Deploy lock does not exist") +} + +// CommandRename renames an app +func CommandRename(args []string, skipDeploy bool) error { + oldAppName, err := getAppName(args) + if err != nil { + return err + } + + newAppName, err := getNewAppName(args) + if err != nil { + return err + } + + if err = common.IsValidAppName(oldAppName); err != nil { + return err + } + + if err = common.IsValidAppName(newAppName); err != nil { + return err + } + + if err = appExists(oldAppName); err != nil { + return err + } + + if err = appExists(newAppName); err == nil { + return errors.New("Name is already taken") + } + + common.LogInfo1Quiet(fmt.Sprintf("Renaming %s to %s", oldAppName, newAppName)) + if err = createApp(newAppName); err != nil { + return err + } + + if err = common.PlugnTrigger("post-app-rename-setup", []string{oldAppName, newAppName}...); err != nil { + return err + } + + os.Setenv("DOKKU_APPS_FORCE_DELETE", "1") + if err = destroyApp(oldAppName); err != nil { + return err + } + + if skipDeploy { + os.Setenv("SKIP_REBUILD", "true") + } + + if err = common.PlugnTrigger("post-app-rename", []string{oldAppName, newAppName}...); err != nil { + return err + } + + return nil +} + +// CommandReport displays an app report for one or more apps +func CommandReport(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + infoFlag := "" + if len(args) > 1 { + infoFlag = args[1] + } + + if strings.HasPrefix(appName, "--") { + infoFlag = appName + appName = "" + } + + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, infoFlag) +} + +// CommandUnlock unlocks an app for deployment +func CommandUnlock(args []string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + if err := common.VerifyAppName(appName); err != nil { + return err + } + + lockfilePath := fmt.Sprintf("%v/.deploy.lock", common.AppRoot(appName)) + _, err = os.Stat(lockfilePath) + if !os.IsNotExist(err) { + common.LogWarn("A deploy may be in progress.") + common.LogWarn("Removing the app lock will not stop in progress deploys.") + } + + err = os.Remove(lockfilePath) + if err == nil { + common.LogInfo1("Deploy lock removed") + return nil + } + + return errors.New("Unable to remove deploy lock") +} diff --git a/plugins/apps/subcommands/clone b/plugins/apps/subcommands/clone deleted file mode 100755 index 960b20569..000000000 --- a/plugins/apps/subcommands/clone +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -cmd-apps-clone() { - declare desc="clones an app" - declare cmd="apps:clone" - [[ "$1" == "$cmd" ]] && shift 1 - declare OLD_APP="$1" NEW_APP="$2" - local IGNORE_EXISTING=false SKIP_REBUILD=false - - for arg in "$@"; do - [[ "$arg" == "--skip-deploy" ]] && shift 1 && SKIP_REBUILD=true - [[ "$arg" == "--ignore-existing" ]] && shift 1 && IGNORE_EXISTING=true - [[ "$arg" =~ ^--.* ]] || break - done - - OLD_APP="$1" NEW_APP="$2" - [[ -z "$OLD_APP" ]] && dokku_log_fail "Please specify an app to run the command on" - - is_valid_app_name "$OLD_APP" - is_valid_app_name "$NEW_APP" - apps_exists "$OLD_APP" >/dev/null 2>&1 || dokku_log_fail "App does not exist" - if apps_exists "$NEW_APP" >/dev/null 2>&1; then - if [[ "$IGNORE_EXISTING" == true ]]; then - dokku_log_warn "Name is already taken" - return - fi - - dokku_log_fail "Name is already taken" - fi - - dokku_log_info1_quiet "Cloning $OLD_APP to $NEW_APP" - apps_create "$NEW_APP" - plugn trigger post-app-clone-setup "$OLD_APP" "$NEW_APP" - SKIP_REBUILD=$SKIP_REBUILD plugn trigger post-app-clone "$OLD_APP" "$NEW_APP" -} - -cmd-apps-clone "$@" diff --git a/plugins/apps/subcommands/create b/plugins/apps/subcommands/create deleted file mode 100755 index 0a538f940..000000000 --- a/plugins/apps/subcommands/create +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/apps/functions" - -cmd-apps-create() { - declare desc="creates app via command line" - declare cmd="apps:create" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - apps_create "$APP" -} - -cmd-apps-create "$@" diff --git a/plugins/apps/subcommands/default b/plugins/apps/subcommands/default deleted file mode 100755 index 96cbedb42..000000000 --- a/plugins/apps/subcommands/default +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/apps/help-functions" - -cmd-apps-help "apps:help" diff --git a/plugins/apps/subcommands/destroy b/plugins/apps/subcommands/destroy deleted file mode 100755 index 6accc0a29..000000000 --- a/plugins/apps/subcommands/destroy +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/functions" - -cmd-apps-destroy() { - declare desc="destroys an app" - declare cmd="apps:destroy" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" FORCE="$2" - - [[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on" - [[ "$APP" == "tls" ]] && dokku_log_fail "Unable to destroy tls directory" - [[ "$FORCE" == "force" ]] && export DOKKU_APPS_FORCE_DELETE=1 - - apps_destroy "$APP" -} - -cmd-apps-destroy "$@" diff --git a/plugins/apps/subcommands/exists b/plugins/apps/subcommands/exists deleted file mode 100755 index 1bbc9113e..000000000 --- a/plugins/apps/subcommands/exists +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/apps/functions" - -cmd-apps-exist() { - declare desc="checks if an app exists" - declare cmd="apps:exists" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - if ! apps_exists "$APP"; then - dokku_log_fail "App does not exist" - fi - dokku_log_info1 "App exists" -} - -cmd-apps-exist "$@" diff --git a/plugins/apps/subcommands/list b/plugins/apps/subcommands/list deleted file mode 100755 index b7832c4b7..000000000 --- a/plugins/apps/subcommands/list +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" - -cmd-apps-list "$@" diff --git a/plugins/apps/subcommands/lock b/plugins/apps/subcommands/lock deleted file mode 100755 index 4659902c4..000000000 --- a/plugins/apps/subcommands/lock +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" - -cmd-apps-lock() { - declare desc="locks an app for deployment" - declare cmd="apps:lock" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - verify_app_name "$APP" - touch "$DOKKU_ROOT/$APP/.deploy.lock" || dokku_log_fail "Unable to create deploy lock" - dokku_log_info1 "Deploy lock created" -} - -cmd-apps-lock "$@" diff --git a/plugins/apps/subcommands/locked b/plugins/apps/subcommands/locked deleted file mode 100755 index 6dc51b779..000000000 --- a/plugins/apps/subcommands/locked +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" - -cmd-apps-locked() { - declare desc="checks if an app is locked for deployment" - declare cmd="apps:locked" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - - verify_app_name "$APP" - local LOCKED="$(apps_is_locked "$APP")" - - if [[ "$LOCKED" != "true" ]]; then - dokku_log_fail_quiet "Deploy lock does not exist" - fi - - dokku_log_quiet "Deploy lock exists" -} - -cmd-apps-locked "$@" diff --git a/plugins/apps/subcommands/rename b/plugins/apps/subcommands/rename deleted file mode 100755 index 803e4e8e2..000000000 --- a/plugins/apps/subcommands/rename +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/functions" -source "$PLUGIN_AVAILABLE_PATH/ps/functions" - -cmd-apps-rename() { - declare desc="renames an app" - declare cmd="apps:rename" - [[ "$1" == "$cmd" ]] && shift 1 - declare OLD_APP="$1" NEW_APP="$2" - local SKIP_REBUILD=false - - for arg in "$@"; do - [[ "$arg" == "--skip-deploy" ]] && shift 1 && SKIP_REBUILD=true - [[ "$arg" =~ ^--.* ]] || break - done - - OLD_APP="$1" NEW_APP="$2" - [[ -z "$OLD_APP" ]] && dokku_log_fail "Please specify an app to run the command on" - - is_valid_app_name "$OLD_APP" - is_valid_app_name "$NEW_APP" - apps_exists "$OLD_APP" >/dev/null 2>&1 || dokku_log_fail "App does not exist" - - if apps_exists "$NEW_APP" >/dev/null 2>&1; then - dokku_log_fail "Name is already taken" - fi - - dokku_log_info1_quiet "Renaming $OLD_APP to $NEW_APP" - apps_create "$NEW_APP" - plugn trigger post-app-rename-setup "$OLD_APP" "$NEW_APP" - DOKKU_APPS_FORCE_DELETE=1 apps_destroy "$OLD_APP" - SKIP_REBUILD=$SKIP_REBUILD plugn trigger post-app-rename "$OLD_APP" "$NEW_APP" -} - -cmd-apps-rename "$@" diff --git a/plugins/apps/subcommands/report b/plugins/apps/subcommands/report deleted file mode 100755 index a41a4fd30..000000000 --- a/plugins/apps/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-apps-report "$@" diff --git a/plugins/apps/subcommands/unlock b/plugins/apps/subcommands/unlock deleted file mode 100755 index eba5a1fcf..000000000 --- a/plugins/apps/subcommands/unlock +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/apps/internal-functions" - -cmd-apps-unlock() { - declare desc="unlocks an app for deployment" - declare cmd="apps:unlock" - [[ "$1" == "$cmd" ]] && shift 1 - declare APP="$1" - verify_app_name "$APP" - - if [[ -f "$DOKKU_ROOT/$APP/.deploy.lock" ]]; then - dokku_log_warn "A deploy may be in progress." - dokku_log_warn "Removing the app lock will not stop in progress deploys." - fi - - rm -f "$DOKKU_ROOT/$APP/.deploy.lock" || dokku_log_fail "Unable to remove deploy lock" - - dokku_log_info1 "Deploy lock removed" -} - -cmd-apps-unlock "$@" diff --git a/plugins/apps/triggers.go b/plugins/apps/triggers.go new file mode 100644 index 000000000..45426436f --- /dev/null +++ b/plugins/apps/triggers.go @@ -0,0 +1,58 @@ +package apps + +import ( + "fmt" + "os" + + "github.com/dokku/dokku/plugins/common" +) + +// TriggerAppCreate is a trigger to create an app +func TriggerAppCreate(appName string) error { + return createApp(appName) +} + +// TriggerAppDestroy is a trigger to destroy an app +func TriggerAppDestroy(appName string) error { + return destroyApp(appName) +} + +// TriggerAppExists is a trigger to check if an app exists +func TriggerAppExists(appName string) error { + return appExists(appName) +} + +// TriggerAppMaybeCreate is a trigger to allow gated app creation +func TriggerAppMaybeCreate(appName string) error { + return maybeCreateApp(appName) +} + +// TriggerPostDelete is the apps post-delete plugin trigger +func TriggerPostDelete(appName string) error { + imageRepo := common.GetAppImageRepo(appName) + + // remove contents for apps that are symlinks to other folders + if err := os.RemoveAll(fmt.Sprintf("%v/", common.AppRoot(appName))); err != nil { + common.LogWarn(err.Error()) + } + + // then remove the folder and/or the symlink + if err := os.RemoveAll(common.AppRoot(appName)); err != nil { + common.LogWarn(err.Error()) + } + + imagesByAppLabel, err := listImagesByAppLabel(appName) + if err != nil { + common.LogWarn(err.Error()) + } + + imagesByRepo, err := listImagesByImageRepo(imageRepo) + if err != nil { + common.LogWarn(err.Error()) + } + + images := append(imagesByAppLabel, imagesByRepo...) + common.RemoveImages(images) + + return nil +} diff --git a/plugins/apps/vendor/github.com/codegangsta/inject/.gitignore b/plugins/apps/vendor/github.com/codegangsta/inject/.gitignore new file mode 100644 index 000000000..df3df8a90 --- /dev/null +++ b/plugins/apps/vendor/github.com/codegangsta/inject/.gitignore @@ -0,0 +1,2 @@ +inject +inject.test diff --git a/plugins/apps/vendor/github.com/codegangsta/inject/LICENSE b/plugins/apps/vendor/github.com/codegangsta/inject/LICENSE new file mode 100644 index 000000000..eb68a0e05 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codegangsta/inject/README.md b/plugins/apps/vendor/github.com/codegangsta/inject/README.md new file mode 100644 index 000000000..679abe01a --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codegangsta/inject/inject.go b/plugins/apps/vendor/github.com/codegangsta/inject/inject.go new file mode 100644 index 000000000..3ff713c8a --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codegangsta/inject/inject_test.go b/plugins/apps/vendor/github.com/codegangsta/inject/inject_test.go new file mode 100644 index 000000000..eb94471d3 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codegangsta/inject/translations/README_zh_cn.md b/plugins/apps/vendor/github.com/codegangsta/inject/translations/README_zh_cn.md new file mode 100644 index 000000000..0ac3d3f55 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codegangsta/inject/update_readme.sh b/plugins/apps/vendor/github.com/codegangsta/inject/update_readme.sh new file mode 100644 index 000000000..3cb21111c --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/LICENSE b/plugins/apps/vendor/github.com/codeskyblue/go-sh/LICENSE new file mode 100644 index 000000000..e06d20818 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/OLD_README.md b/plugins/apps/vendor/github.com/codeskyblue/go-sh/OLD_README.md new file mode 100644 index 000000000..7e899402d --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/README.md b/plugins/apps/vendor/github.com/codeskyblue/go-sh/README.md new file mode 100644 index 000000000..28614708d --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/example/example1.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/example/example1.go new file mode 100644 index 000000000..cdc7f6c39 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/example/less/less.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/example/less/less.go new file mode 100644 index 000000000..ffced2e81 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/example/tail/tailf.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/example/tail/tailf.go new file mode 100644 index 000000000..b7ecd3bed --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/example/timeout/timeout.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/example/timeout/timeout.go new file mode 100644 index 000000000..852908a47 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/example_test.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/example_test.go new file mode 100644 index 000000000..5abfed64d --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/pipe.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/pipe.go new file mode 100644 index 000000000..c35f4e2a0 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/pipe_test.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/pipe_test.go new file mode 100644 index 000000000..8d0e22c0c --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/sh.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/sh.go new file mode 100644 index 000000000..15f1b85d5 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/sh_test.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/sh_test.go new file mode 100644 index 000000000..be5c27998 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/test.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/test.go new file mode 100644 index 000000000..b9d310527 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/test_test.go b/plugins/apps/vendor/github.com/codeskyblue/go-sh/test_test.go new file mode 100644 index 000000000..15225aa9a --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/codeskyblue/go-sh/testdata/executable b/plugins/apps/vendor/github.com/codeskyblue/go-sh/testdata/executable new file mode 100755 index 000000000..e69de29bb diff --git a/plugins/apps/vendor/github.com/codeskyblue/go-sh/testdata/hello.txt b/plugins/apps/vendor/github.com/codeskyblue/go-sh/testdata/hello.txt new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/apps/vendor/github.com/codeskyblue/go-sh/testdata/linkfile b/plugins/apps/vendor/github.com/codeskyblue/go-sh/testdata/linkfile new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/apps/vendor/github.com/codeskyblue/go-sh/wercker.yml b/plugins/apps/vendor/github.com/codeskyblue/go-sh/wercker.yml new file mode 100644 index 000000000..72c47020f --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/apps/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/apps/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/apps/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/apps/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/ryanuber/columnize/README.md b/plugins/apps/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/apps/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/apps/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/apps/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/apps/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/apps/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/builder-dockerfile/builder-release b/plugins/builder-dockerfile/builder-release index 5ef84ad14..10493cd9e 100755 --- a/plugins/builder-dockerfile/builder-release +++ b/plugins/builder-dockerfile/builder-release @@ -16,7 +16,7 @@ trigger-builder-dockerfile-builder-release() { plugn trigger pre-release-dockerfile "$APP" "$IMAGE_TAG" local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") - echo "FROM $IMAGE" | "$DOCKER_BIN" image build --label=com.dokku.image-stage=release -t "$IMAGE" - + echo "FROM $IMAGE" | suppress_output "$DOCKER_BIN" image build --label=com.dokku.image-stage=release -t "$IMAGE" - plugn trigger post-release-dockerfile "$APP" "$IMAGE_TAG" } diff --git a/plugins/builder-herokuish/builder-release b/plugins/builder-herokuish/builder-release index b371d186c..c0c201b7f 100755 --- a/plugins/builder-herokuish/builder-release +++ b/plugins/builder-herokuish/builder-release @@ -38,7 +38,7 @@ trigger-builder-herokuish-builder-release() { "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$CID" "$IMAGE" >/dev/null fi - echo "FROM $IMAGE" | "$DOCKER_BIN" image build --label=com.dokku.image-stage=release -t "$IMAGE" - + echo "FROM $IMAGE" | suppress_output "$DOCKER_BIN" image build --label=com.dokku.image-stage=release -t "$IMAGE" - plugn trigger post-release-buildpack "$APP" "$IMAGE_TAG" } diff --git a/plugins/buildpacks/functions.go b/plugins/buildpacks/functions.go new file mode 100644 index 000000000..13f2141be --- /dev/null +++ b/plugins/buildpacks/functions.go @@ -0,0 +1,15 @@ +package buildpacks + +import ( + "errors" +) + +func getAppName(args []string) (appName string, err error) { + if len(args) >= 1 { + appName = args[0] + } else { + err = errors.New("Please specify an app to run the command on") + } + + return +} diff --git a/plugins/buildpacks/subcommands.go b/plugins/buildpacks/subcommands.go index fcb5d1ed2..0a0b4eb44 100644 --- a/plugins/buildpacks/subcommands.go +++ b/plugins/buildpacks/subcommands.go @@ -167,13 +167,3 @@ func CommandSet(args []string, index int) (err error) { return } - -func getAppName(args []string) (appName string, err error) { - if len(args) >= 1 { - appName = args[0] - } else { - err = errors.New("Please specify an app to run the command on") - } - - return -} diff --git a/plugins/common/common.go b/plugins/common/common.go index 308aafd10..83f4f1555 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -2,6 +2,8 @@ package common import ( "bufio" + "bytes" + "errors" "fmt" "io/ioutil" "os" @@ -16,6 +18,8 @@ import ( sh "github.com/codeskyblue/go-sh" ) +type errfunc func() error + // ShellCmd represents a shell command to be run for dokku type ShellCmd struct { Env map[string]string @@ -69,6 +73,110 @@ func (sc *ShellCmd) Output() ([]byte, error) { return sc.Command.Output() } +// AppRoot returns the app root path +func AppRoot(appName string) string { + dokkuRoot := MustGetEnv("DOKKU_ROOT") + return fmt.Sprintf("%v/%v", dokkuRoot, appName) +} + +// AskForDestructiveConfirmation checks for confirmation on destructive actions +func AskForDestructiveConfirmation(name string, objectType string) error { + LogWarn("WARNING: Potentially Destructive Action") + LogWarn(fmt.Sprintf("This command will destroy %v %v.", objectType, name)) + LogWarn(fmt.Sprintf("To proceed, type \"%v\"", name)) + fmt.Print("> ") + var response string + _, err := fmt.Scanln(&response) + if err != nil { + return err + } + + if response != name { + LogStderr("Confirmation did not match test. Aborted.") + os.Exit(1) + return nil + } + + return nil +} + +// DockerCleanup cleans up all exited/dead containers and removes all dangling images +func DockerCleanup(appName string, forceCleanup bool) error { + if !forceCleanup { + skipCleanup := false + if appName != "" { + b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...) + output := string(b[:]) + if output == "true" { + skipCleanup = true + } + } + + if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" { + LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup") + return nil + } + } + + LogInfo1("Cleaning up...") + scheduler := GetAppScheduler(appName) + if appName == "--global" { + appName = "" + } + + forceCleanupArg := "false" + if forceCleanup { + forceCleanupArg = "true" + } + + if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil { + return fmt.Errorf("Failure while cleaning up app: %s", err) + } + + // delete all non-running and dead containers + exitedContainerIDs, _ := listContainers("exited", appName) + deadContainerIDs, _ := listContainers("dead", appName) + containerIDs := append(exitedContainerIDs, deadContainerIDs...) + + if len(containerIDs) > 0 { + removeContainers(containerIDs) + } + + // delete dangling images + imageIDs, _ := listDanglingImages(appName) + if len(imageIDs) > 0 { + RemoveImages(imageIDs) + } + + if appName != "" { + // delete unused images + pruneUnusedImages(appName) + } + + return nil +} + +// GetAppScheduler fetches the scheduler for a given application +func GetAppScheduler(appName string) string { + if appName == "--global" { + appName = "" + } + + b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SCHEDULER"}...) + value := string(b[:]) + if value != "" { + return value + } + + b, _ = PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...) + value = string(b[:]) + if value != "" { + return value + } + + return "docker-local" +} + // GetDeployingAppImageName returns deploying image identifier for a given app, tag tuple. validate if tag is presented func GetDeployingAppImageName(appName, imageTag, imageRepo string) (imageName string) { if appName == "" { @@ -125,8 +233,7 @@ func GetAppContainerIDs(appName string, containerType string) ([]string, error) return containerIDs, err } - dokkuRoot := MustGetEnv("DOKKU_ROOT") - appRoot := fmt.Sprintf("%v/%v", dokkuRoot, appName) + appRoot := AppRoot(appName) containerFilePath := fmt.Sprintf("%v/CONTAINER", appRoot) _, err := os.Stat(containerFilePath) if !os.IsNotExist(err) { @@ -173,6 +280,26 @@ func GetAppRunningContainerIDs(appName string, containerType string) ([]string, return runningContainerIDs, nil } +// GetRunningImageTag retrieves current image tag for a given app and returns empty string if no deployed containers are found +func GetRunningImageTag(appName string) (string, error) { + if err := VerifyAppName(appName); err != nil { + return "", err + } + + containerIDs, err := GetAppContainerIDs(appName, "") + if err != nil { + return "", err + } + + for _, containerID := range containerIDs { + if image, err := DockerInspect(containerID, "{{ .Config.Image }}"); err == nil { + return strings.Split(image, ":")[1], nil + } + } + + return "", errors.New("No image tag found") +} + // ContainerIsRunning checks to see if a container is running func ContainerIsRunning(containerID string) bool { b, err := DockerInspect(containerID, "'{{.State.Running}}'") @@ -215,7 +342,7 @@ func DokkuApps() (apps []string, err error) { } for _, f := range files { - appRoot := strings.Join([]string{dokkuRoot, f.Name()}, "/") + appRoot := AppRoot(f.Name()) if !DirectoryExists(appRoot) { continue } @@ -287,9 +414,7 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) { // 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) + files, err := ioutil.ReadDir(AppRoot(appName)) if err != nil { return false } @@ -410,6 +535,25 @@ func StripInlineComments(text string) string { return strings.TrimSpace(string(bytes)) } +// SuppressOutput suppresses the output of a function unless there is an error +func SuppressOutput(f errfunc) error { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := f() + + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + + if err != nil { + fmt.Printf(string(out[:])) + } + + return err +} + // ToBool returns a bool value for a given string func ToBool(s string) bool { return s == "true" @@ -423,21 +567,37 @@ func UcFirst(str string) string { return "" } -// VerifyAppName verifies app name format and app existence" -func VerifyAppName(appName string) (err error) { +// IsValidAppName verifies app name format +func IsValidAppName(appName string) error { if appName == "" { - return fmt.Errorf("App name must not be null") + return fmt.Errorf("APP must not be null") } - dokkuRoot := MustGetEnv("DOKKU_ROOT") - appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + + r, _ := regexp.Compile("^[a-z0-9][^:A-Z]*$") + if r.MatchString(appName) { + return nil + } + + appRoot := AppRoot(appName) + if DirectoryExists(appRoot) { + os.RemoveAll(appRoot) + } + + return errors.New("App name must begin with lowercase alphanumeric character, and cannot include uppercase characters or colons") +} + +// VerifyAppName verifies app name format and app existence" +func VerifyAppName(appName string) error { + if err := IsValidAppName(appName); err != nil { + return err + } + + appRoot := AppRoot(appName) if !DirectoryExists(appRoot) { - return fmt.Errorf("app %s does not exist", appName) + return fmt.Errorf("App %s does not exist", appName) } - r, _ := regexp.Compile("^[a-z0-9].*") - if !r.MatchString(appName) { - return fmt.Errorf("app name (%s) must begin with lowercase alphanumeric character", appName) - } - return err + + return nil } // VerifyImage returns true if docker image exists in local repo @@ -459,24 +619,23 @@ func DockerBin() string { // PlugnTrigger fire the given plugn trigger with the given args func PlugnTrigger(triggerName string, args ...string) error { - shellArgs := make([]interface{}, len(args)+2) - shellArgs[0] = "trigger" - shellArgs[1] = triggerName - for i, arg := range args { - shellArgs[i+2] = arg - } - return sh.Command("plugn", shellArgs...).Run() + return PlugnTriggerSetup(triggerName, args...).Run() } // PlugnTriggerOutput fire the given plugn trigger with the given args func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) { + return PlugnTriggerSetup(triggerName, args...).Output() +} + +// PlugnTriggerSetup sets up a plugn trigger call +func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session { shellArgs := make([]interface{}, len(args)+2) shellArgs[0] = "trigger" shellArgs[1] = triggerName for i, arg := range args { shellArgs[i+2] = arg } - return sh.Command("plugn", shellArgs...).Output() + return sh.Command("plugn", shellArgs...) } func times(str string, n int) (out string) { @@ -485,3 +644,113 @@ func times(str string, n int) (out string) { } return } + +func listContainers(status string, appName string) ([]string, error) { + command := []string{ + DockerBin(), + "container", + "list", + "--quiet", + "--all", + "--filter", + fmt.Sprintf("status=%v", status), + "--filter", + fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")), + } + + if appName != "" { + command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + } + + var stderr bytes.Buffer + listCmd := NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +func listDanglingImages(appName string) ([]string, error) { + command := []string{ + DockerBin(), + "image", + "list", + "--quiet", + "--filter", + "dangling=true", + } + + if appName != "" { + command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + } + + var stderr bytes.Buffer + listCmd := NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + +func removeContainers(containerIDs []string) { + command := []string{ + DockerBin(), + "container", + "rm", + } + + command = append(command, containerIDs...) + + var stderr bytes.Buffer + rmCmd := NewShellCmd(strings.Join(command, " ")) + rmCmd.ShowOutput = false + rmCmd.Command.Stderr = &stderr + rmCmd.Execute() +} + +// RemoveImages removes images by ID +func RemoveImages(imageIDs []string) { + command := []string{ + DockerBin(), + "image", + "rm", + } + + command = append(command, imageIDs...) + + var stderr bytes.Buffer + rmCmd := NewShellCmd(strings.Join(command, " ")) + rmCmd.ShowOutput = false + rmCmd.Command.Stderr = &stderr + rmCmd.Execute() +} + +func pruneUnusedImages(appName string) { + command := []string{ + DockerBin(), + "image", + "prune", + "--all", + "--force", + "--filter", + fmt.Sprintf("label=com.dokku.app-name=%v", appName), + } + + var stderr bytes.Buffer + pruneCmd := NewShellCmd(strings.Join(command, " ")) + pruneCmd.ShowOutput = false + pruneCmd.Command.Stderr = &stderr + pruneCmd.Execute() +} diff --git a/plugins/common/log.go b/plugins/common/log.go index 1f8cd8593..7aa77b82b 100644 --- a/plugins/common/log.go +++ b/plugins/common/log.go @@ -12,6 +12,27 @@ func LogFail(text string) { os.Exit(1) } +// LogFailQuiet is the failure log formatter (with quiet option) +// prints text to stderr and exits with status 1 +func LogFailQuiet(text string) { + if os.Getenv("DOKKU_QUIET_OUTPUT") == "" { + fmt.Fprintln(os.Stderr, fmt.Sprintf("FAILED: %s", text)) + } + os.Exit(1) +} + +// Log is the log formatter +func Log(text string) { + fmt.Println(text) +} + +// LogQuiet is the log formatter (with quiet option) +func LogQuiet(text string) { + if os.Getenv("DOKKU_QUIET_OUTPUT") == "" { + fmt.Println(text) + } +} + // LogInfo1 is the info1 header formatter func LogInfo1(text string) { fmt.Println(fmt.Sprintf("-----> %s", text)) diff --git a/plugins/config/.gitignore b/plugins/config/.gitignore index cc45caec7..b1c3ae87c 100644 --- a/plugins/config/.gitignore +++ b/plugins/config/.gitignore @@ -2,3 +2,4 @@ /subcommands/* /triggers/* /triggers +/config-* diff --git a/plugins/config/Makefile b/plugins/config/Makefile index fcc94b7d9..2a0a3943b 100644 --- a/plugins/config/Makefile +++ b/plugins/config/Makefile @@ -1,5 +1,6 @@ SUBCOMMANDS = subcommands/export subcommands/get subcommands/set subcommands/unset subcommands/keys subcommands/bundle -BUILD = commands subcommands +TRIGGERS = triggers/config-get triggers/config-get-global +BUILD = commands subcommands triggers PLUGIN_NAME = config include ../../common.mk diff --git a/plugins/config/src/triggers/triggers.go b/plugins/config/src/triggers/triggers.go new file mode 100644 index 000000000..0ad9f4446 --- /dev/null +++ b/plugins/config/src/triggers/triggers.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/config" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "config-get": + appName := flag.Arg(0) + key := flag.Arg(1) + config.TriggerConfigGet(appName, key) + case "config-get-global": + key := flag.Arg(0) + config.TriggerConfigGetGlobal(key) + default: + common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger)) + } + + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/config/triggers.go b/plugins/config/triggers.go new file mode 100644 index 000000000..6bebf8230 --- /dev/null +++ b/plugins/config/triggers.go @@ -0,0 +1,23 @@ +package config + +import "fmt" + +// TriggerConfigGet returns an app config value by key +func TriggerConfigGet(appName string, key string) error { + value, ok := Get(appName, key) + if ok { + fmt.Print(value) + } + + return nil +} + +// TriggerConfigGetGlobal returns a global config value by key +func TriggerConfigGetGlobal(key string) error { + value, ok := Get("--global", key) + if ok { + fmt.Print(value) + } + + return nil +} diff --git a/plugins/git/functions b/plugins/git/functions index 939e5d487..d85f80387 100755 --- a/plugins/git/functions +++ b/plugins/git/functions @@ -38,7 +38,7 @@ git_build_app_repo() { chmod 755 "$GIT_BUILD_APP_REPO_TMP_WORK_DIR" unset GIT_DIR GIT_WORK_TREE - ! apps_exists "$APP" >/dev/null 2>&1 && apps_maybe_create "$APP" + ! plugn trigger app-exists "$APP" >/dev/null 2>&1 && plugn trigger app-maybe-create "$APP" if use_git_worktree; then # git worktree - this method uses git worktree which was introduced in git 2.5 @@ -205,7 +205,7 @@ cmd-git-upload-pack() { APP="$(echo "$APP" | perl -pe 's/(?/dev/null 2>&1 && apps_maybe_create "$APP" + ! plugn trigger app-exists "$APP" >/dev/null 2>&1 && plugn trigger app-maybe-create "$APP" plugn trigger git-pre-pull "$APP" cat | git-upload-pack "$DOKKU_ROOT/$APP" @@ -220,7 +220,7 @@ cmd-git-glob() { is_valid_app_name "$APP" if [[ $1 == "git-receive-pack" && ! -d "$APP_PATH/refs" ]]; then - ! apps_exists "$APP" >/dev/null 2>&1 && apps_maybe_create "$APP" + ! plugn trigger app-exists "$APP" >/dev/null 2>&1 && plugn trigger app-maybe-create "$APP" fn-git-create-hook "$APP" fi diff --git a/plugins/git/internal-functions b/plugins/git/internal-functions index d7a88e8ae..e1ec4189d 100755 --- a/plugins/git/internal-functions +++ b/plugins/git/internal-functions @@ -31,6 +31,7 @@ cmd-git-report() { cmd-git-report-single() { declare APP="$1" INFO_FLAG="$2" + local APP_DIR="$DOKKU_ROOT/$APP" if [[ "$INFO_FLAG" == "true" ]]; then INFO_FLAG="" fi @@ -40,6 +41,7 @@ cmd-git-report-single() { "--git-global-deploy-branch: $(fn-plugin-property-get "git" "--global" "deploy-branch" "master")" "--git-keep-git-dir: $(fn-plugin-property-get "git" "$APP" "keep-git-dir" "false")" "--git-rev-env-var: $(fn-plugin-property-get "git" "$APP" "rev-env-var" "GIT_REV")" + "--git-sha: $(GIT_DIR="$APP_DIR" git rev-parse --short HEAD 2>/dev/null || false)" ) if [[ -z "$INFO_FLAG" ]]; then diff --git a/plugins/network/network.go b/plugins/network/network.go index a34e8c5c8..0ae0bff53 100644 --- a/plugins/network/network.go +++ b/plugins/network/network.go @@ -32,7 +32,7 @@ func BuildConfig(appName string) { if !common.IsDeployed(appName) { return } - appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + appRoot := common.AppRoot(appName) scaleFile := strings.Join([]string{appRoot, "DOKKU_SCALE"}, "/") if !common.FileExists(scaleFile) { return @@ -166,8 +166,7 @@ func GetDefaultValue(property string) (value string) { // GetListeners returns a string array of app listeners func GetListeners(appName string) []string { - dokkuRoot := common.MustGetEnv("DOKKU_ROOT") - appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + appRoot := common.AppRoot(appName) files, _ := filepath.Glob(appRoot + "/IP.web.*") @@ -183,7 +182,7 @@ func GetListeners(appName string) []string { // 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}, "/") + appRoot := common.AppRoot(appName) ipfile := fmt.Sprintf("%v/IP.web.1", appRoot) portfile := fmt.Sprintf("%v/PORT.web.1", appRoot) @@ -192,8 +191,7 @@ func HasNetworkConfig(appName string) bool { // ClearNetworkConfig removes old IP and PORT files for a newly cloned app func ClearNetworkConfig(appName string) bool { - dokkuRoot := common.MustGetEnv("DOKKU_ROOT") - appRoot := strings.Join([]string{dokkuRoot, appName}, "/") + appRoot := common.AppRoot(appName) success := true ipFiles, _ := filepath.Glob(appRoot + "/IP.*") diff --git a/plugins/network/subcommands.go b/plugins/network/subcommands.go index 0e2653c49..12d07e20f 100644 --- a/plugins/network/subcommands.go +++ b/plugins/network/subcommands.go @@ -41,21 +41,9 @@ func CommandDestroy(networkName string, forceDestroy bool) error { } if !forceDestroy { - common.LogWarn("WARNING: Potentially Destructive Action") - common.LogWarn(fmt.Sprintf("This command will destroy network %v.", networkName)) - common.LogWarn(fmt.Sprintf("To proceed, type \"%v\"", networkName)) - fmt.Print("> ") - var response string - _, err := fmt.Scanln(&response) - if err != nil { + if err := common.AskForDestructiveConfirmation(networkName, "network"); err != nil { return err } - - if response != networkName { - common.LogStderr("Confirmation did not match test. Aborted.") - os.Exit(1) - return nil - } } common.LogInfo1Quiet(fmt.Sprintf("Destroying network %v", networkName)) diff --git a/plugins/network/triggers.go b/plugins/network/triggers.go index cdb3d7164..1ce32368d 100644 --- a/plugins/network/triggers.go +++ b/plugins/network/triggers.go @@ -112,7 +112,7 @@ func TriggerNetworkWriteIpaddr(appName string, processType string, containerInde common.LogFail(err.Error()) } - appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + appRoot := common.AppRoot(appName) filename := fmt.Sprintf("%v/IP.%v.%v", appRoot, processType, containerIndex) f, err := os.Create(filename) if err != nil { @@ -137,7 +137,7 @@ func TriggerNetworkWritePort(appName string, processType string, containerIndex common.LogFail(err.Error()) } - appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + appRoot := common.AppRoot(appName) filename := fmt.Sprintf("%v/PORT.%v.%v", appRoot, processType, containerIndex) f, err := os.Create(filename) if err != nil { diff --git a/plugins/nginx-vhosts/command-functions b/plugins/nginx-vhosts/command-functions index bd68eca62..0cf9790ec 100755 --- a/plugins/nginx-vhosts/command-functions +++ b/plugins/nginx-vhosts/command-functions @@ -74,6 +74,7 @@ cmd-nginx-report-single() { cmd-nginx-show-config() { declare desc="display app nginx config" declare cmd="nginx:show-config" + declare deprecated=true [[ "$1" == "$cmd" ]] && shift 1 if [[ "$1" == "nginx:show-conf" ]]; then dokku_log_warn "Deprecated: Please use nginx:show-config" @@ -93,6 +94,7 @@ cmd-nginx-show-config() { cmd-nginx-validate-config() { declare desc="validates and optionally cleans up invalid nginx configurations" declare cmd="nginx:validate-config" + declare deprecated=true [[ "$1" == "$cmd" ]] && shift 1 if [[ "$1" == "nginx:validate" ]]; then dokku_log_warn "Deprecated: Please use nginx:validate-config" diff --git a/plugins/repo/subcommands.go b/plugins/repo/subcommands.go index 238c0b8fc..ab61e4102 100644 --- a/plugins/repo/subcommands.go +++ b/plugins/repo/subcommands.go @@ -2,7 +2,6 @@ package repo import ( "errors" - "strings" "github.com/dokku/dokku/plugins/common" ) @@ -17,7 +16,7 @@ func CommandGc(appName string) error { return err } - appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") + appRoot := common.AppRoot(appName) cmdEnv := map[string]string{ "GIT_DIR": appRoot, } diff --git a/tests/unit/10_apps.bats b/tests/unit/10_apps.bats index e943d809e..210e7a26e 100644 --- a/tests/unit/10_apps.bats +++ b/tests/unit/10_apps.bats @@ -383,7 +383,7 @@ teardown() { @test "(apps) apps:lock/locked/unlock" { create_app - run /bin/bash -c "dokku apps:report $TEST_APP --locked" + run /bin/bash -c "dokku apps:report $TEST_APP --app-locked" echo "output: $output" echo "status: $status" assert_output "false" @@ -398,7 +398,7 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "dokku apps:report $TEST_APP --locked" + run /bin/bash -c "dokku apps:report $TEST_APP --app-locked" echo "output: $output" echo "status: $status" assert_output "true" @@ -413,7 +413,7 @@ teardown() { echo "status: $status" assert_success - run /bin/bash -c "dokku apps:report $TEST_APP --locked" + run /bin/bash -c "dokku apps:report $TEST_APP --app-locked" echo "output: $output" echo "status: $status" assert_output "false" diff --git a/tests/unit/40_report.bats b/tests/unit/40_report.bats index d4ddedfcb..2c3d27f6b 100644 --- a/tests/unit/40_report.bats +++ b/tests/unit/40_report.bats @@ -25,7 +25,7 @@ teardown() { echo "output: $output" echo "status: $status" assert_output_contains "herokuish version:" - assert_output_contains "not deployed" "0" + assert_output_contains "Deployed: false" "0" assert_success run /bin/bash -c "dokku report fake-app-name" @@ -43,7 +43,7 @@ teardown() { run /bin/bash -c "dokku report ${TEST_APP}-2 2>&1" echo "output: $output" echo "status: $status" - assert_output_contains "not deployed" + assert_output_contains "Deployed: false" assert_success dokku --force apps:destroy "${TEST_APP}-2"