diff --git a/Makefile b/Makefile index 40715fcdc..61718d955 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ include common.mk .PHONY: all apt-update install version copyfiles copyplugin man-db plugins dependencies sshcommand procfile-util plugn docker aufs stack count dokku-installer vagrant-acl-add vagrant-dokku go-build include tests.mk +include package.mk include deb.mk include rpm.mk include arch.mk diff --git a/deb.mk b/deb.mk index 6cc71853a..016139c4d 100644 --- a/deb.mk +++ b/deb.mk @@ -100,53 +100,20 @@ deb-herokuish: --license 'MIT License' \ . -deb-dokku: - rm -rf /tmp/tmp /tmp/build dokku_*_$(DOKKU_ARCHITECTURE).deb - mkdir -p /tmp/tmp /tmp/build +deb-dokku: /tmp/build-dokku/var/lib/dokku/GIT_REV + rm -f $(BUILD_DIRECTORY)/dokku_*_$(DOKKU_ARCHITECTURE).deb - cp -r debian /tmp/build/DEBIAN - mkdir -p /tmp/build/usr/share/bash-completion/completions - mkdir -p /tmp/build/usr/bin - mkdir -p /tmp/build/usr/share/doc/dokku - mkdir -p /tmp/build/usr/share/dokku/contrib - mkdir -p /tmp/build/usr/share/lintian/overrides - mkdir -p /tmp/build/usr/share/man/man1 - mkdir -p /tmp/build/var/lib/dokku/core-plugins/available - - cp dokku /tmp/build/usr/bin - cp LICENSE /tmp/build/usr/share/doc/dokku/copyright - cp contrib/bash-completion /tmp/build/usr/share/bash-completion/completions/dokku - find . -name ".DS_Store" -depth -exec rm {} \; - $(MAKE) go-build - cp common.mk /tmp/build/var/lib/dokku/core-plugins/common.mk - cp -r plugins/* /tmp/build/var/lib/dokku/core-plugins/available - find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do cd /tmp/build/var/lib/dokku/core-plugins/available/$$plugin && if [ -e Makefile ]; then $(MAKE) src-clean; fi; done - find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do touch /tmp/build/var/lib/dokku/core-plugins/available/$$plugin/.core; done - rm /tmp/build/var/lib/dokku/core-plugins/common.mk - $(MAKE) help2man - $(MAKE) addman - cp /usr/local/share/man/man1/dokku.1 /tmp/build/usr/share/man/man1/dokku.1 - gzip -9 /tmp/build/usr/share/man/man1/dokku.1 - cp contrib/dokku-installer.py /tmp/build/usr/share/dokku/contrib -ifeq ($(DOKKU_VERSION),master) - git describe --tags > /tmp/build/var/lib/dokku/VERSION -else - echo $(DOKKU_VERSION) > /tmp/build/var/lib/dokku/VERSION -endif - cat /tmp/build/var/lib/dokku/VERSION | cut -d '-' -f 1 | cut -d 'v' -f 2 > /tmp/build/var/lib/dokku/STABLE_VERSION + cat /tmp/build-dokku/var/lib/dokku/VERSION | cut -d '-' -f 1 | cut -d 'v' -f 2 > /tmp/build-dokku/var/lib/dokku/STABLE_VERSION ifneq (,$(findstring false,$(IS_RELEASE))) - sed -i.bak -e "s/^/`date +%s`:/" /tmp/build/var/lib/dokku/STABLE_VERSION && rm /tmp/build/var/lib/dokku/STABLE_VERSION.bak + sed -i.bak -e "s/^/`date +%s`:/" /tmp/build-dokku/var/lib/dokku/STABLE_VERSION && rm /tmp/build-dokku/var/lib/dokku/STABLE_VERSION.bak endif - rm -f /tmp/build/DEBIAN/lintian-overrides - cp debian/lintian-overrides /tmp/build/usr/share/lintian/overrides/dokku -ifdef DOKKU_GIT_REV - echo "$(DOKKU_GIT_REV)" > /tmp/build/var/lib/dokku/GIT_REV -else - git rev-parse HEAD > /tmp/build/var/lib/dokku/GIT_REV -endif - sed -i.bak "s/^Version: .*/Version: `cat /tmp/build/var/lib/dokku/STABLE_VERSION`/g" /tmp/build/DEBIAN/control && rm /tmp/build/DEBIAN/control.bak - dpkg-deb --build /tmp/build "$(BUILD_DIRECTORY)/dokku_`cat /tmp/build/var/lib/dokku/VERSION`_$(DOKKU_ARCHITECTURE).deb" - lintian "$(BUILD_DIRECTORY)/dokku_`cat /tmp/build/var/lib/dokku/VERSION`_$(DOKKU_ARCHITECTURE).deb" + + cp -r debian /tmp/build-dokku/DEBIAN + rm -f /tmp/build-dokku/DEBIAN/lintian-overrides + cp debian/lintian-overrides /tmp/build-dokku/usr/share/lintian/overrides/dokku + sed -i.bak "s/^Version: .*/Version: `cat /tmp/build-dokku/var/lib/dokku/STABLE_VERSION`/g" /tmp/build-dokku/DEBIAN/control && rm /tmp/build-dokku/DEBIAN/control.bak + dpkg-deb --build /tmp/build-dokku "$(BUILD_DIRECTORY)/dokku_`cat /tmp/build-dokku/var/lib/dokku/VERSION`_$(DOKKU_ARCHITECTURE).deb" + lintian "$(BUILD_DIRECTORY)/dokku_`cat /tmp/build-dokku/var/lib/dokku/VERSION`_$(DOKKU_ARCHITECTURE).deb" deb-dokku-update: rm -rf /tmp/dokku-update*.deb dokku-update*.deb diff --git a/docs/advanced-usage/resource-management.md b/docs/advanced-usage/resource-management.md new file mode 100644 index 000000000..02227dd88 --- /dev/null +++ b/docs/advanced-usage/resource-management.md @@ -0,0 +1,381 @@ +# Resource Management + +> New as of 0.15.0 + +``` +resource:limit [--process-type ] [RESOURCE_OPTS...] # Limit resources for a given app/process-type combination +resource:limit-clear [--process-type ] # Limit resources for a given app/process-type combination +resource:report [] [] # Displays a resource report for one or more apps +resource:reserve [--process-type ] [RESOURCE_OPTS...] # Reserve resources for a given app/process-type combination +resource:reserve-clear [--process-type ] # Reserve resources for a given app/process-type combination +``` + +The resource plugin is meant to allow users to limit or reserve resources for a given app/process-type combination. + +## Usage + +By default, Dokku allows unlimited resource access to apps deployed on a server. In some cases, it may be desirable to limit this on a per-app or per-process-type basis. The `resource` plugin allows management of both resource "limits" and resource "reservations", where each resource request type has specific meaning to the scheduler in use for a given app. + +> The meaning of a values and it's units are specific to the scheduler in use for a given app. If a scheduler does not support a given resource type combination, it will be ignored. All resource commands require an app rebuild or deploy in order to take effect. + +Valid resource options include: + +- `--cpu` +- `--memory` +- `--memory-swap` +- `--network` +- `--network-ingress` +- `--network-egress` + +Resource limits and reservations are applied only during the `run` and `deploy` phases of an application, and will not impact the `build` phase of an application. + +### Resource Limits + +When specified and supported, a resource limit will ensure that your app does not go _over_ the specified value. If this occurs, the underlying scheduler may either cap resource utilization, or it may decide to terminate and reschedule your process. + +Resource limits may be set via the `resource:limit` command: + +```shell +dokku resource:limit --memory 100 node-js-app +``` + +``` +=====> Setting resource limits for node-js-app + memory: 100 +``` + +Multiple resources can be limited in a single call: + +```shell +dokku resource:limit --cpu 100 --memory 100 node-js-app +``` + +``` +=====> Setting resource limits for node-js-app + cpu: 100 + memory: 100 +``` + +Resources can also be limited on a per-process type basis. If specified, this will override any generic limits set for the app. + +```shell +dokku resource:limit --cpu 100 --memory 100 --process-type worker node-js-app +``` + +``` +=====> Setting resource limits for node-js-app (worker) + cpu: 100 + memory: 100 +``` + +#### Displaying Resource Limits + +Running the `resource:limits` command without any flags will display the currently configured app reservation. + +```shell +dokku resource:limits node-js-app +=====> resource limits node-js-app information + cpu: 100 + memory: 100 + memory-swap: + network: + network-ingress: + network-egress: +``` + +#### Global Resource Limits + +By default, Dokku does not limit any resources, though these can be specified at the server level via the `--global` flag on the `resource:limit` command. + +```shell +dokku resource:limit --global --memory 200 +``` + +``` +=====> Setting default resource limits + memory: 200 +``` + +This may also be combined with the `--process-type` flag to set global limits on a process-type level: + +```shell +dokku resource:limit --global --memory 150 --process-type web node-js-app +``` + +``` +=====> Setting default resource limits (web) + memory: 150 +``` + +#### Displaying Global Resource Limits + +Running the `resource:limit` command with only the `--global` flag will display the currently configured default limits. + +```shell +dokku resource:limit --global +``` + +``` +=====> resource limits default information + cpu: + memory: 200 + memory-swap: + network: + network-ingress: + network-egress: +``` + +This may also be combined with the `--process-type` flag to see global limits on a process-type level: + +```shell +dokku resource:limit --global --process-type web +``` + +``` +=====> resource limits default information (web) + cpu: + memory-swap: + memory: 150 + network-egress: + network-ingress: + network: +``` + +#### Clearing Resource Limits + +In cases where the values are incorrect - or there is no desire to limit resources - resource limits may be cleared using the `resource:limit-clear` command. + +```shell +dokku resource:limit-clear node-js-app +``` + +``` +-----> Clearing resource limit for node-js-app +``` + +Defaults can also be cleared by leaving the app unspecified. + +```shell +dokku resource:limit-clear +``` + +``` +-----> Clearing default resource limits +``` + +### Resource Reservations + +When specified and supported, a resource reservation will ensure that your server has _at least_ the specified resources before placing a given app's process. If there a resource exhaustion, future rebuilds and deploys may fail. + +Resource reservations may be set via the `resource:reserve` command: + +```shell +dokku resource:reserve --memory 100 node-js-app +``` + +``` +=====> Setting resource reservation for node-js-app + memory: 100 +``` + +Multiple resources can be limited in a single call: + +```shell +dokku resource:reserve --cpu 100 --memory 100 node-js-app +``` + +``` +=====> Setting resource reservation for node-js-app + cpu: 100 + memory: 100 +``` + +Resources can also be limited on a per-process type basis. If specified, this will override any generic limits set for the app. + +```shell +dokku resource:reserve --cpu 100 --memory 100 --process-type worker node-js-app +``` + +``` +=====> Setting resource reservation for node-js-app (worker) + cpu: 100 + memory: 100 +``` + +#### Displaying Resource Reservations + +Running the `resource:reserve` command without any flags will display the currently configured app reservation. + +```shell +dokku resource:reserve node-js-app +=====> resource reservation node-js-app information + cpu: 100 + memory: 100 + memory-swap: + network: + network-ingress: + network-egress: +``` + +#### Global Resource Reservations + +By default, Dokku does not reserve any resources, though these can be specified at the server level via the `--global` flag on the `resource:reserve` command. + +```shell +dokku resource:reserve --global --memory 200 +``` + +``` +=====> Setting default resource reservation + memory: 200 +``` + +This may also be combined with the `--process-type` flag to set global reservations on a process-type level: + +```shell +dokku resource:reserve --global --memory 150 --process-type web node-js-app +``` + +``` +=====> Setting default resource reservation (web) + memory: 150 +``` + +#### Displaying Global Resource Reservations + +Running the `resource:reserve` command with only the `--global` flag will display the currently configured default reservation. + +```shell +dokku resource:reserve --global +``` + +``` +=====> resource reservation default information + cpu: + memory: 200 + memory-swap: + network: + network-ingress: + network-egress: +``` + +This may also be combined with the `--process-type` flag to see global reservations on a process-type level: + +```shell +dokku resource:reserve --global --process-type web +``` + +``` +=====> resource reservation default information (web) + cpu: + memory-swap: + memory: 150 + network-egress: + network-ingress: + network: +``` + +#### Clearing Resource Reservations + +In cases where the values are incorrect - or there is no desire to reserve resources - resource reservations may be cleared using the `resource:reserve-clear` command. + +```shell +dokku resource:reserve-clear node-js-app +``` + +``` +-----> Clearing resource reservation for node-js-app +``` + +Defaults can also be cleared by leaving the app unspecified. + +```shell +dokku resource:reserve-clear +``` + +``` +-----> Clearing default resource reservation +``` + +### Displaying resource reports for an app + +You can get a report about the app's resource status using the `resource:report` command: + +```shell +dokku resource:report +``` + +``` +=====> node-js-app resource information + web limit cpu: + web limit memory: 1024 + web limit memory swap: 0 + web limit network: 10 + web limit network ingress: + web limit network egress: + web reservation cpu: + web reservation memory: 512 + web reservation memory swap: + web reservation network: 8 + web reservation network ingress: + web reservation network egress: +=====> python-sample resource information + web limit cpu: + web limit memory: + web limit memory swap: + web limit network: + web limit network ingress: + web limit network egress: + web reservation cpu: + web reservation memory: + web reservation memory swap: + web reservation network: + web reservation network ingress: + web reservation network egress: +=====> ruby-sample resource information + web limit cpu: + web limit memory: + web limit memory swap: + web limit network: + web limit network ingress: + web limit network egress: + web reservation cpu: + web reservation memory: + web reservation memory swap: + web reservation network: + web reservation network ingress: + web reservation network egress: +``` + +You can run the command for a specific app also. + +```shell +dokku resource:report node-js-app +``` + +``` +=====> node-js-app resource information + web limit cpu: + web limit memory: 1024 + web limit memory swap: 0 + web limit network: 10 + web limit network ingress: + web limit network egress: + web reservation cpu: + web reservation memory: 512 + web reservation memory swap: + web reservation network: 8 + web reservation network ingress: + web reservation network egress: +``` + +You can pass flags which will output only the value of the specific information you want. For example: + +```shell +# Note the periods in the flag name +dokku resource:report node-js-app --resource-web.limit.memory +``` + +``` +1024 +``` diff --git a/docs/advanced-usage/schedulers/docker-local.md b/docs/advanced-usage/schedulers/docker-local.md index 574486e36..2394d2029 100644 --- a/docs/advanced-usage/schedulers/docker-local.md +++ b/docs/advanced-usage/schedulers/docker-local.md @@ -53,3 +53,17 @@ This plugin implements various functionality through `plugn` triggers to integra - `scheduler-stop` - `scheduler-tags-create` - `scheduler-tags-destroy` + +## Supported Resource Management Properties + +The `docker-local` scheduler supports a minimal list of resource _limits_ and _reservations_. The following properties are supported: + +### Resource Limits + +- cpu: (docker option: `--cpus`), and is specified in number of CPUs a process can access. +- memory: (docker option: `--memory`) and should be specified with a suffix of `b` (bytes), `k` (kilobytes), `m` (megabytes), `g` (gigabytes) +- memory-swap: (docker option: `--memory-swap`) and should be specified with a suffix of `b` (bytes), `k` (kilobytes), `m` (megabytes), `g` (gigabytes) + +### Resource Reservations + +- memory: (docker option: `--memory-reservation`) and should be specified with a suffix of `b` (bytes), `k` (kilobytes), `m` (megabytes), `g` (gigabytes) diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index 4f4abbf5e..443e0d0be 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -248,6 +248,8 @@ echo 'derp.dkr.ecr.us-east-1.amazonaws.com' ### `docker-args-build` +> Warning: Deprecated, please use `docker-args-process-build` instead + - Description: - Invoked by: `internal function dokku_build() (build phase)` - Arguments: `$APP $IMAGE_SOURCE_TYPE` @@ -271,6 +273,8 @@ echo -n "$STDIN$output" ### `docker-args-deploy` +> Warning: Deprecated, please use `docker-args-process-deploy` instead + - Description: - Invoked by: `dokku deploy` - Arguments: `$APP $IMAGE_TAG [$PROC_TYPE $CONTAINER_INDEX]` @@ -289,6 +293,8 @@ verify_app_name "$APP" ### `docker-args-run` +> Warning: Deprecated, please use `docker-args-process-run` instead + - Description: - Invoked by: `dokku run` - Arguments: `$APP $IMAGE_TAG` @@ -305,6 +311,42 @@ verify_app_name "$APP" # TODO ``` +### `docker-args-process-deploy` + +- Description: `$PROC_TYPE` may be set to magic `_all_` process type to signify global docker deploy options. +- Invoked by: `dokku deploy` +- Arguments: `$APP $IMAGE_TAG $IMAGE_SOURCE_TYPE [$PROC_TYPE $CONTAINER_INDEX]` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +APP="$1"; IMAGE_TAG="$2"; IMAGE=$(get_app_image_name $APP $IMAGE_TAG) +verify_app_name "$APP" + +# TODO +``` + +### `docker-args-process-run` + +- Description: `$PROC_TYPE` may be set to magic `_all_` process type to signify global docker run options. +- Invoked by: `dokku run` +- Arguments: `$APP $IMAGE_TAG $IMAGE_SOURCE_TYPE [$PROC_TYPE $CONTAINER_INDEX]` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +APP="$1"; IMAGE_TAG="$2"; IMAGE=$(get_app_image_name $APP $IMAGE_TAG) +verify_app_name "$APP" + +# TODO +``` + ### `git-post-pull` - Description: @@ -1157,6 +1199,22 @@ fi plugn trigger receive-app $APP $newrev ``` +### `resource-get-property` + +- Description: Fetches a given resource property value +- Invoked by: +- Arguments: `$APP` `$PROC_TYPE` `$RESOURCE_TYPE` `$PROPERTY` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +APP="$1"; PROC_TYPE="$2" RESOURCE_TYPE="$3" PROPERTY="$4" + +# TODO +``` + ### `retire-container-failed` - Description: Allows you to run commands if/when retiring old containers has failed diff --git a/package.mk b/package.mk new file mode 100644 index 000000000..2b9a3dc01 --- /dev/null +++ b/package.mk @@ -0,0 +1,35 @@ +/tmp/build-dokku/var/lib/dokku/GIT_REV: + mkdir -p /tmp/build-dokku + mkdir -p /tmp/build-dokku/usr/share/bash-completion/completions + mkdir -p /tmp/build-dokku/usr/bin + mkdir -p /tmp/build-dokku/usr/share/doc/dokku + mkdir -p /tmp/build-dokku/usr/share/dokku/contrib + mkdir -p /tmp/build-dokku/usr/share/lintian/overrides + mkdir -p /tmp/build-dokku/usr/share/man/man1 + mkdir -p /tmp/build-dokku/var/lib/dokku/core-plugins/available + + cp dokku /tmp/build-dokku/usr/bin + cp LICENSE /tmp/build-dokku/usr/share/doc/dokku/copyright + cp contrib/bash-completion /tmp/build-dokku/usr/share/bash-completion/completions/dokku + find . -name ".DS_Store" -depth -exec rm {} \; + $(MAKE) go-build + cp common.mk /tmp/build-dokku/var/lib/dokku/core-plugins/common.mk + cp -r plugins/* /tmp/build-dokku/var/lib/dokku/core-plugins/available + find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do cd /tmp/build-dokku/var/lib/dokku/core-plugins/available/$$plugin && if [ -e Makefile ]; then $(MAKE) src-clean; fi; done + find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do touch /tmp/build-dokku/var/lib/dokku/core-plugins/available/$$plugin/.core; done + rm /tmp/build-dokku/var/lib/dokku/core-plugins/common.mk + $(MAKE) help2man + $(MAKE) addman + cp /usr/local/share/man/man1/dokku.1 /tmp/build-dokku/usr/share/man/man1/dokku.1 + gzip -9 /tmp/build-dokku/usr/share/man/man1/dokku.1 + cp contrib/dokku-installer.py /tmp/build-dokku/usr/share/dokku/contrib +ifeq ($(DOKKU_VERSION),master) + git describe --tags > /tmp/build-dokku/var/lib/dokku/VERSION +else + echo $(DOKKU_VERSION) > /tmp/build-dokku/var/lib/dokku/VERSION +endif +ifdef DOKKU_GIT_REV + echo "$(DOKKU_GIT_REV)" > /tmp/build-dokku/var/lib/dokku/GIT_REV +else + git rev-parse HEAD > /tmp/build-dokku/var/lib/dokku/GIT_REV +endif diff --git a/plugins/app-json/internal-functions b/plugins/app-json/internal-functions index a3351df47..46feb92d1 100755 --- a/plugins/app-json/internal-functions +++ b/plugins/app-json/internal-functions @@ -83,6 +83,10 @@ execute_script() { # strip --restart args from DOCKER_ARGS local DOCKER_ARGS=$(sed -e "s/--restart=[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") + local IMAGE_SOURCE_TYPE="dockerfile" + is_image_herokuish_based "$IMAGE" && IMAGE_SOURCE_TYPE="herokuish" + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-deploy "$APP" "$IMAGE_TAG" "$IMAGE_SOURCE_TYPE" "$PHASE_SCRIPT_KEY") + # eval args as array to respect escapes declare -a ARG_ARRAY eval "ARG_ARRAY=($DOCKER_ARGS)" diff --git a/plugins/buildpacks/plugin.toml b/plugins/buildpacks/plugin.toml index 0b6f00dab..8804437b6 100644 --- a/plugins/buildpacks/plugin.toml +++ b/plugins/buildpacks/plugin.toml @@ -1,4 +1,4 @@ [plugin] description = "dokku core buildpacks plugin" -version = "0.14.2" +version = "0.14.6" [plugin.config] diff --git a/plugins/common/common.go b/plugins/common/common.go index 2348b534f..99dd24d65 100644 --- a/plugins/common/common.go +++ b/plugins/common/common.go @@ -328,7 +328,7 @@ func VerifyAppName(appName string) (err error) { dokkuRoot := MustGetEnv("DOKKU_ROOT") appRoot := strings.Join([]string{dokkuRoot, appName}, "/") if !DirectoryExists(appRoot) { - return fmt.Errorf("app %s does not exist: %v", appName, err) + return fmt.Errorf("app %s does not exist", appName) } r, _ := regexp.Compile("^[a-z0-9].*") if !r.MatchString(appName) { diff --git a/plugins/common/functions b/plugins/common/functions index 0cf7fc690..c5e9af265 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -533,6 +533,9 @@ dokku_build() { local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE") [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true " + local IMAGE_SOURCE_TYPE="herokuish" + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-run "$APP" "$IMAGE_TAG" "$IMAGE_SOURCE_TYPE" "$PHASE_SCRIPT_KEY") + declare -a ARG_ARRAY eval "ARG_ARRAY=($DOCKER_ARGS)" # shellcheck disable=SC2086 @@ -558,6 +561,9 @@ dokku_build() { [[ "$DOKKU_DOCKERFILE_CACHE_BUILD" == "false" ]] && DOKKU_DOCKER_BUILD_OPTS="$DOKKU_DOCKER_BUILD_OPTS --no-cache" local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$IMAGE_SOURCE_TYPE") + local IMAGE_SOURCE_TYPE="dockerfile" + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-run "$APP" "$IMAGE_TAG" "$IMAGE_SOURCE_TYPE" "$PHASE_SCRIPT_KEY") + # strip --volume and -v args from DOCKER_ARGS local DOCKER_ARGS=$(sed -e "s/--volume=[[:graph:]]\+[[:blank:]]\?//g" -e "s/-v[[:blank:]]\?[[:graph:]]\+[[:blank:]]\?//g" <<<"$DOCKER_ARGS") declare -a ARG_ARRAY diff --git a/plugins/common/properties.go b/plugins/common/properties.go index f09d02b84..d780003ff 100644 --- a/plugins/common/properties.go +++ b/plugins/common/properties.go @@ -77,6 +77,26 @@ func PropertyGet(pluginName string, appName string, property string) string { return PropertyGetDefault(pluginName, appName, property, "") } +// PropertyGetAll returns a map of all properties for a given app +func PropertyGetAll(pluginName string, appName string) (map[string]string, error) { + properties := make(map[string]string) + pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName) + files, err := ioutil.ReadDir(pluginAppConfigRoot) + if err != nil { + return properties, err + } + + for _, file := range files { + if file.IsDir() { + continue + } + property := file.Name() + properties[property] = PropertyGet(pluginName, appName, property) + } + + return properties, nil +} + // PropertyGetDefault returns the value for a given property with a specified default value func PropertyGetDefault(pluginName, appName, property, defaultValue string) (val string) { if !PropertyExists(pluginName, appName, property) { diff --git a/plugins/network/network.go b/plugins/network/network.go index af44450d3..55a32c629 100644 --- a/plugins/network/network.go +++ b/plugins/network/network.go @@ -52,7 +52,7 @@ func BuildConfig(appName string) { if len(procParts) != 2 { continue } - procType := procParts[0] + processType := procParts[0] procCount, err := strconv.Atoi(procParts[1]) if err != nil { continue @@ -62,25 +62,25 @@ func BuildConfig(appName string) { for containerIndex < procCount { containerIndex++ containerIndexString := strconv.Itoa(containerIndex) - containerIDFile := fmt.Sprintf("%v/CONTAINER.%v.%v", appRoot, procType, containerIndex) + containerIDFile := fmt.Sprintf("%v/CONTAINER.%v.%v", appRoot, processType, containerIndex) containerID := common.ReadFirstLine(containerIDFile) if containerID == "" || !common.ContainerIsRunning(containerID) { continue } - ipAddress := GetContainerIpaddress(appName, procType, containerID) - port := GetContainerPort(appName, procType, isHerokuishContainer, containerID) + ipAddress := GetContainerIpaddress(appName, processType, containerID) + port := GetContainerPort(appName, processType, isHerokuishContainer, containerID) if ipAddress != "" { - _, err := sh.Command("plugn", "trigger", "network-write-ipaddr", appName, procType, containerIndexString, ipAddress).Output() + _, err := sh.Command("plugn", "trigger", "network-write-ipaddr", appName, processType, containerIndexString, ipAddress).Output() if err != nil { common.LogWarn(err.Error()) } } if port != "" { - _, err := sh.Command("plugn", "trigger", "network-write-port", appName, procType, containerIndexString, port).Output() + _, err := sh.Command("plugn", "trigger", "network-write-port", appName, processType, containerIndexString, port).Output() if err != nil { common.LogWarn(err.Error()) } @@ -90,8 +90,8 @@ func BuildConfig(appName string) { } // GetContainerIpaddress returns the ipaddr for a given app container -func GetContainerIpaddress(appName, procType, containerID string) (ipAddr string) { - if procType != "web" { +func GetContainerIpaddress(appName, processType, containerID string) (ipAddr string) { + if processType != "web" { return } @@ -109,8 +109,8 @@ func GetContainerIpaddress(appName, procType, containerID string) (ipAddr string } // GetContainerPort returns the port for a given app container -func GetContainerPort(appName, procType string, isHerokuishContainer bool, containerID string) (port string) { - if procType != "web" { +func GetContainerPort(appName, processType string, isHerokuishContainer bool, containerID string) (port string) { + if processType != "web" { return } diff --git a/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go b/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go index e83fc3620..b981c59b0 100644 --- a/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go +++ b/plugins/network/src/triggers/network-compute-ports/network-compute-ports.go @@ -15,10 +15,10 @@ import ( func main() { flag.Parse() appName := flag.Arg(0) - procType := flag.Arg(1) + processType := flag.Arg(1) isHerokuishContainer := common.ToBool(flag.Arg(2)) - if procType != "web" { + if processType != "web" { return } diff --git a/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go b/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go index cfdb3bbfd..237526b64 100644 --- a/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go +++ b/plugins/network/src/triggers/network-get-ipaddr/network-get-ipaddr.go @@ -12,9 +12,9 @@ import ( func main() { flag.Parse() appName := flag.Arg(0) - procType := flag.Arg(1) + processType := flag.Arg(1) containerID := flag.Arg(2) - ipAddress := network.GetContainerIpaddress(appName, procType, containerID) + ipAddress := network.GetContainerIpaddress(appName, processType, containerID) fmt.Fprintln(os.Stdout, ipAddress) } diff --git a/plugins/network/src/triggers/network-get-port/network-get-port.go b/plugins/network/src/triggers/network-get-port/network-get-port.go index 801bc88ff..389265da7 100644 --- a/plugins/network/src/triggers/network-get-port/network-get-port.go +++ b/plugins/network/src/triggers/network-get-port/network-get-port.go @@ -13,10 +13,10 @@ import ( func main() { flag.Parse() appName := flag.Arg(0) - procType := flag.Arg(1) + processType := flag.Arg(1) isHerokuishContainer := common.ToBool(flag.Arg(2)) containerID := flag.Arg(3) - port := network.GetContainerPort(appName, procType, isHerokuishContainer, containerID) + port := network.GetContainerPort(appName, processType, isHerokuishContainer, containerID) fmt.Fprintln(os.Stdout, port) } diff --git a/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go b/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go index b76ceb3af..3cf618120 100644 --- a/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go +++ b/plugins/network/src/triggers/network-write-ipaddr/network-write-ipaddr.go @@ -13,7 +13,7 @@ import ( func main() { flag.Parse() appName := flag.Arg(0) - procType := flag.Arg(1) + processType := flag.Arg(1) containerIndex := flag.Arg(2) ip := flag.Arg(3) @@ -26,7 +26,7 @@ func main() { } appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") - filename := fmt.Sprintf("%v/IP.%v.%v", appRoot, procType, containerIndex) + filename := fmt.Sprintf("%v/IP.%v.%v", appRoot, processType, containerIndex) f, err := os.Create(filename) if err != nil { common.LogFail(err.Error()) diff --git a/plugins/network/src/triggers/network-write-port/network-write-port.go b/plugins/network/src/triggers/network-write-port/network-write-port.go index 2a8d45dff..e263cd786 100644 --- a/plugins/network/src/triggers/network-write-port/network-write-port.go +++ b/plugins/network/src/triggers/network-write-port/network-write-port.go @@ -13,7 +13,7 @@ import ( func main() { flag.Parse() appName := flag.Arg(0) - procType := flag.Arg(1) + processType := flag.Arg(1) containerIndex := flag.Arg(2) port := flag.Arg(3) @@ -26,7 +26,7 @@ func main() { } appRoot := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName}, "/") - filename := fmt.Sprintf("%v/PORT.%v.%v", appRoot, procType, containerIndex) + filename := fmt.Sprintf("%v/PORT.%v.%v", appRoot, processType, containerIndex) f, err := os.Create(filename) if err != nil { common.LogFail(err.Error()) diff --git a/plugins/resource/.gitignore b/plugins/resource/.gitignore new file mode 100644 index 000000000..bdb6a1395 --- /dev/null +++ b/plugins/resource/.gitignore @@ -0,0 +1,9 @@ +/commands +/subcommands/* +/triggers/* +/install +/post-delete +/report +/docker-args-process-deploy +/docker-args-process-run +/resource-get-property diff --git a/plugins/resource/Makefile b/plugins/resource/Makefile new file mode 100644 index 000000000..6859e0283 --- /dev/null +++ b/plugins/resource/Makefile @@ -0,0 +1,38 @@ +include ../../common.mk + +GO_ARGS ?= -a + +SUBCOMMANDS = subcommands/limit subcommands/limit-clear subcommands/report subcommands/reserve subcommands/reserve-clear +TRIGGERS = triggers/docker-args-process-deploy triggers/install triggers/post-delete triggers/report triggers/resource-get-property +build-in-docker: clean + docker run --rm \ + -v $$PWD/../..:$(GO_REPO_ROOT) \ + -w $(GO_REPO_ROOT)/plugins/resource \ + $(BUILD_IMAGE) \ + bash -c "GO_ARGS='$(GO_ARGS)' make -j4 build" || exit $$? + +build: commands subcommands triggers + $(MAKE) triggers-copy + +commands: **/**/commands.go + go build $(GO_ARGS) -o commands src/commands/commands.go + +subcommands: $(SUBCOMMANDS) + +subcommands/%: src/subcommands/*/%.go + go build $(GO_ARGS) -o $@ $< + +clean: + rm -rf commands subcommands triggers docker-args-process-deploy docker-args-process-run install post-delete report resource-get-property + +src-clean: + rm -rf .gitignore src triggers vendor Makefile *.go + +triggers: $(TRIGGERS) + +triggers/%: src/triggers/*/%.go + go build $(GO_ARGS) -o $@ $< + +triggers-copy: + cp triggers/* . + ln -sf docker-args-process-deploy docker-args-process-run diff --git a/plugins/resource/glide.yaml b/plugins/resource/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/resource/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/resource/plugin.toml b/plugins/resource/plugin.toml new file mode 100644 index 000000000..3e302605c --- /dev/null +++ b/plugins/resource/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core resource plugin" +version = "0.14.6" +[plugin.config] diff --git a/plugins/resource/resource.go b/plugins/resource/resource.go new file mode 100644 index 000000000..bd299e58c --- /dev/null +++ b/plugins/resource/resource.go @@ -0,0 +1,117 @@ +package resource + +import ( + "errors" + "fmt" + "os" + "reflect" + "sort" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// Resource is a collection of resource constraints for apps +type Resource struct { + CPU string `json:"cpu"` + Memory string `json:"memory"` + MemorySwap string `json:"memory-swap"` + Network string `json:"network"` + NetworkIngress string `json:"network-ingress"` + NetworkEgress string `json:"network-egress"` +} + +// ReportSingleApp is an internal function that displays the app report for one or more apps +func ReportSingleApp(appName, infoFlag string) { + if err := common.VerifyAppName(appName); err != nil { + common.LogFail(err.Error()) + } + + resources, err := common.PropertyGetAll("resource", appName) + if err != nil { + common.LogFail(err.Error()) + } + + flags := []string{} + infoFlags := map[string]string{} + for key, value := range resources { + flag := fmt.Sprintf("--resource-%v", key) + flags = append(flags, flag) + infoFlags[flag] = value + } + sort.Strings(flags) + + if len(infoFlag) == 0 { + common.LogInfo2Quiet(fmt.Sprintf("%s resource information", appName)) + for _, k := range flags { + v := infoFlags[k] + key := strings.Replace(strings.Replace(strings.TrimPrefix(k, "--resource-"), "-", " ", -1), ".", " ", -1) + common.LogVerbose(fmt.Sprintf("%s%s", Right(fmt.Sprintf("%s:", key), 31, " "), v)) + } + return + } + + for _, k := range flags { + v := infoFlags[k] + if infoFlag == k { + fmt.Fprintln(os.Stdout, v) + return + } + } + + keys := reflect.ValueOf(infoFlags).MapKeys() + strkeys := make([]string, len(keys)) + for i := 0; i < len(keys); i++ { + strkeys[i] = keys[i].String() + } + common.LogFail(fmt.Sprintf("Invalid flag passed, valid flags: %s", strings.Join(strkeys, ", "))) +} + +// GetResourceValue fetches a single value for a given app/process/request/key combination +func GetResourceValue(appName string, processType string, resourceType string, key string) (string, error) { + resources, err := common.PropertyGetAll("resource", appName) + if err != nil { + return "", err + } + + defaultValue := "" + for key, value := range resources { + if key == propertyKey("_default_", resourceType, key) { + defaultValue = value + } + if key == propertyKey(processType, resourceType, key) { + return value, nil + } + } + + return defaultValue, nil +} + +func times(str string, n int) (out string) { + for i := 0; i < n; i++ { + out += str + } + return +} + +// Right right-pads the string with pad up to len runes +func Right(str string, length int, pad string) string { + return str + times(pad, length-len(str)) +} + +func propertyKey(processType string, resourceType string, key string) string { + return fmt.Sprintf("%v.%v.%v", processType, resourceType, key) +} + +func getAppName(args []string) (string, error) { + if len(args) < 1 { + return "", errors.New("Please specify an app to run the command on") + } + + appName := args[0] + if err := common.VerifyAppName(appName); err != nil { + return "", err + } + + return appName, nil +} diff --git a/plugins/resource/src/commands/commands.go b/plugins/resource/src/commands/commands.go new file mode 100644 index 000000000..a998d631d --- /dev/null +++ b/plugins/resource/src/commands/commands.go @@ -0,0 +1,66 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + + "github.com/dokku/dokku/plugins/common" + columnize "github.com/ryanuber/columnize" +) + +const ( + helpHeader = `Usage: dokku resource[:COMMAND] + +Manages resource settings for an app + +Additional commands:` + + helpContent = ` + resource:limit [--process-type ] [RESOURCE_OPTS...] , Limit resources for a given app/process-type combination + resource:limit-clear [--process-type ] , Limit resources for a given app/process-type combination + resource:report [] [], Displays a resource report for one or more apps + resource:reserve [--process-type ] [RESOURCE_OPTS...] , Reserve resources for a given app/process-type combination + resource:reserve-clear [--process-type ] , Reserve resources for a given app/process-type combination +` +) + +func main() { + flag.Usage = usage + flag.Parse() + + cmd := flag.Arg(0) + switch cmd { + case "resource", "resource: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 resource, Manages resource settings for an app\n") + } + default: + dokkuNotImplementExitCode, err := strconv.Atoi(os.Getenv("DOKKU_NOT_IMPLEMENTED_EXIT")) + if err != nil { + fmt.Println("failed to retrieve DOKKU_NOT_IMPLEMENTED_EXIT environment variable") + dokkuNotImplementExitCode = 10 + } + os.Exit(dokkuNotImplementExitCode) + } +} + +func usage() { + config := columnize.DefaultConfig() + config.Delim = "," + config.Prefix = " " + config.Empty = "" + content := strings.Split(helpContent, "\n")[1:] + fmt.Println(helpHeader) + fmt.Println(columnize.Format(content, config)) +} diff --git a/plugins/resource/src/glide.lock b/plugins/resource/src/glide.lock new file mode 100644 index 000000000..fe4a98f07 --- /dev/null +++ b/plugins/resource/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/resource/src/glide.yaml b/plugins/resource/src/glide.yaml new file mode 100644 index 000000000..3f7a96657 --- /dev/null +++ b/plugins/resource/src/glide.yaml @@ -0,0 +1,3 @@ +package: . +import: +- package: github.com/ryanuber/columnize diff --git a/plugins/resource/src/subcommands/limit-clear/limit-clear.go b/plugins/resource/src/subcommands/limit-clear/limit-clear.go new file mode 100644 index 000000000..b794dfc30 --- /dev/null +++ b/plugins/resource/src/subcommands/limit-clear/limit-clear.go @@ -0,0 +1,20 @@ +package main + +import ( + "flag" + "os" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +func main() { + args := flag.NewFlagSet("resource:limit-clear", flag.ExitOnError) + processType := args.String("process-type", "", "process-type: A process type to clear") + args.Parse(os.Args[2:]) + + err := resource.CommandLimitClear(args.Args(), *processType) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/resource/src/subcommands/limit/limit.go b/plugins/resource/src/subcommands/limit/limit.go new file mode 100644 index 000000000..d45438e3a --- /dev/null +++ b/plugins/resource/src/subcommands/limit/limit.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "os" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +func main() { + args := flag.NewFlagSet("resource:limit", flag.ExitOnError) + processType := args.String("process-type", "", "process-type: A process type to manage") + cpu := args.String("cpu", "", "cpu: The amount of cpu to limit processes by") + memory := args.String("memory", "", "memory: The amount of memory to limit processes by") + memorySwap := args.String("memory-swap", "", "memory-swap: The amount of swap memory to limit processes by") + network := args.String("network", "", "network: The amount of network bandwidth to limit processes by") + networkIngress := args.String("network-ingress", "", "network-ingress: The amount of ingress network bandwidth to limit processes by") + networkEgress := args.String("network-egress", "", "network-egress: The amount of egress network bandwidth to limit processes by") + args.Parse(os.Args[2:]) + + resources := resource.Resource{ + CPU: *cpu, + Memory: *memory, + MemorySwap: *memorySwap, + Network: *network, + NetworkIngress: *networkIngress, + NetworkEgress: *networkEgress, + } + + err := resource.CommandLimit(args.Args(), *processType, resources) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/resource/src/subcommands/report/report.go b/plugins/resource/src/subcommands/report/report.go new file mode 100644 index 000000000..f8385c3bd --- /dev/null +++ b/plugins/resource/src/subcommands/report/report.go @@ -0,0 +1,33 @@ +package main + +import ( + "flag" + "strings" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +// displays a resource report for one or more apps +func main() { + flag.Parse() + appName := flag.Arg(1) + infoFlag := flag.Arg(2) + + if strings.HasPrefix(appName, "--") { + common.LogFail("The resource:report command does not support flags without an app name") + } + + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + return + } + for _, appName := range apps { + resource.ReportSingleApp(appName, infoFlag) + } + return + } + + resource.ReportSingleApp(appName, infoFlag) +} diff --git a/plugins/resource/src/subcommands/reserve-clear/reserve-clear.go b/plugins/resource/src/subcommands/reserve-clear/reserve-clear.go new file mode 100644 index 000000000..4f882388d --- /dev/null +++ b/plugins/resource/src/subcommands/reserve-clear/reserve-clear.go @@ -0,0 +1,20 @@ +package main + +import ( + "flag" + "os" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +func main() { + args := flag.NewFlagSet("resource:reserve-clear", flag.ExitOnError) + processType := args.String("process-type", "", "process-type: A process type to clear") + args.Parse(os.Args[2:]) + + err := resource.CommandReserveClear(args.Args(), *processType) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/resource/src/subcommands/reserve/reserve.go b/plugins/resource/src/subcommands/reserve/reserve.go new file mode 100644 index 000000000..afeccb551 --- /dev/null +++ b/plugins/resource/src/subcommands/reserve/reserve.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "os" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +func main() { + args := flag.NewFlagSet("resource:reserve", flag.ExitOnError) + processType := args.String("process-type", "", "process-type: A process type to manage") + cpu := args.String("cpu", "", "cpu: The amount of cpu to reserve for processes") + memory := args.String("memory", "", "memory: The amount of memory to reserve for processes") + memorySwap := args.String("memory-swap", "", "memory-swap: The amount of swap memory to reserve for processes") + network := args.String("network", "", "network: The amount of network bandwidth to reserve for processes") + networkIngress := args.String("network-ingress", "", "network-ingress: The amount of ingress network bandwidth to reserve for processes") + networkEgress := args.String("network-egress", "", "network-egress: The amount of egress network bandwidth to reserve for processes") + args.Parse(os.Args[2:]) + + resources := resource.Resource{ + CPU: *cpu, + Memory: *memory, + MemorySwap: *memorySwap, + Network: *network, + NetworkIngress: *networkIngress, + NetworkEgress: *networkEgress, + } + + err := resource.CommandReserve(args.Args(), *processType, resources) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/resource/src/triggers/docker-args-process-deploy/docker-args-process-deploy.go b/plugins/resource/src/triggers/docker-args-process-deploy/docker-args-process-deploy.go new file mode 100644 index 000000000..76dec47e6 --- /dev/null +++ b/plugins/resource/src/triggers/docker-args-process-deploy/docker-args-process-deploy.go @@ -0,0 +1,80 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +// outputs the process-specific docker options +func main() { + stdin, err := ioutil.ReadAll(os.Stdin) + if err != nil { + common.LogFail(err.Error()) + } + + flag.Parse() + appName := flag.Arg(0) + processType := flag.Arg(3) + + resources, err := common.PropertyGetAll("resource", appName) + if err != nil { + fmt.Print(string(stdin)) + return + } + + limits := make(map[string]string) + reservations := make(map[string]string) + + validLimits := map[string]bool{ + "cpu": true, + "memory": true, + "memory-swap": true, + } + validReservations := map[string]bool{ + "memory": true, + } + validPrefixes := []string{"_default_.", fmt.Sprintf("%s.", processType)} + for _, validPrefix := range validPrefixes { + for key, value := range resources { + if !strings.HasPrefix(key, validPrefix) { + continue + } + parts := strings.SplitN(strings.TrimPrefix(key, validPrefix), ".", 2) + if parts[0] == "limit" { + if !validLimits[parts[1]] { + continue + } + + limits[parts[1]] = value + } + if parts[0] == "reserve" { + if !validReservations[parts[1]] { + continue + } + + reservations[parts[1]] = value + } + } + } + + for key, value := range limits { + if value == "" { + continue + } + fmt.Printf(" --%s=%s ", key, value) + } + + for key, value := range reservations { + if value == "" { + continue + } + fmt.Printf(" --%s-reservation=%s ", key, value) + } + + fmt.Print(string(stdin)) +} diff --git a/plugins/resource/src/triggers/install/install.go b/plugins/resource/src/triggers/install/install.go new file mode 100644 index 000000000..77f7d14ed --- /dev/null +++ b/plugins/resource/src/triggers/install/install.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "github.com/dokku/dokku/plugins/common" +) + +// runs the install step for the resource plugin +func main() { + if err := common.PropertySetup("resource"); err != nil { + common.LogFail(fmt.Sprintf("Unable to install the resource plugin: %s", err.Error())) + } +} diff --git a/plugins/resource/src/triggers/post-delete/post-delete.go b/plugins/resource/src/triggers/post-delete/post-delete.go new file mode 100644 index 000000000..d22ceccea --- /dev/null +++ b/plugins/resource/src/triggers/post-delete/post-delete.go @@ -0,0 +1,18 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/common" +) + +// destroys the resource property for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + + err := common.PropertyDestroy("resource", appName) + if err != nil { + common.LogFail(err.Error()) + } +} diff --git a/plugins/resource/src/triggers/report/report.go b/plugins/resource/src/triggers/report/report.go new file mode 100644 index 000000000..b34ad49e0 --- /dev/null +++ b/plugins/resource/src/triggers/report/report.go @@ -0,0 +1,15 @@ +package main + +import ( + "flag" + + "github.com/dokku/dokku/plugins/resource" +) + +// displays a resource report for one or more apps +func main() { + flag.Parse() + appName := flag.Arg(0) + + resource.ReportSingleApp(appName, "") +} diff --git a/plugins/resource/src/triggers/resource-get-property/resource-get-property.go b/plugins/resource/src/triggers/resource-get-property/resource-get-property.go new file mode 100644 index 000000000..53d22effc --- /dev/null +++ b/plugins/resource/src/triggers/resource-get-property/resource-get-property.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/dokku/dokku/plugins/common" + "github.com/dokku/dokku/plugins/resource" +) + +// writes the resource key to stdout for a given app container +func main() { + flag.Parse() + appName := flag.Arg(0) + processType := flag.Arg(1) + resourceType := flag.Arg(1) + key := flag.Arg(1) + + value, err := resource.GetResourceValue(appName, processType, resourceType, key) + if err != nil { + common.LogFail(err.Error()) + } + + fmt.Fprintln(os.Stdout, value) +} diff --git a/plugins/resource/src/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/resource/src/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/resource/src/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/resource/src/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/resource/src/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/resource/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/resource/src/vendor/github.com/ryanuber/columnize/README.md b/plugins/resource/src/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/resource/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/resource/src/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/resource/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/resource/src/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/resource/src/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/resource/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/resource/subcommands.go b/plugins/resource/subcommands.go new file mode 100644 index 000000000..f93a4672a --- /dev/null +++ b/plugins/resource/subcommands.go @@ -0,0 +1,165 @@ +package resource + +import ( + "fmt" + "github.com/dokku/dokku/plugins/common" +) + +// CommandLimit implements resource:limit +func CommandLimit(args []string, processType string, r Resource) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + return setResourceType(appName, processType, r, "limit") +} + +// CommandLimitClear implements resource:limit-clear +func CommandLimitClear(args []string, processType string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + clearByResourceType(appName, processType, "limit") + return nil +} + +// CommandReserve implements resource:reserve +func CommandReserve(args []string, processType string, r Resource) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + return setResourceType(appName, processType, r, "reserve") +} + +// CommandReserveClear implements resource:reserve-clear +func CommandReserveClear(args []string, processType string) error { + appName, err := getAppName(args) + if err != nil { + return err + } + + clearByResourceType(appName, processType, "reserve") + return nil +} + +func clearByResourceType(appName string, processType string, resourceType string) { + noun := "limits" + if resourceType == "reserve" { + noun = "reservation" + } + + message := fmt.Sprintf("clearing %v %v", appName, noun) + if processType != "_default_" && processType != "" { + message = fmt.Sprintf("%v (%v)", message, processType) + } + common.LogInfo2Quiet(message) + + if processType == "" { + resources, err := common.PropertyGetAll("resource", appName) + if err != nil { + return + } + for key := range resources { + common.PropertyDelete("resource", appName, key) + } + } else { + resources := []string{ + "cpu", + "memory", + "memory-swap", + "network", + "network-ingress", + "network-egress", + } + + for _, key := range resources { + property := propertyKey(processType, resourceType, key) + common.PropertyDelete("resource", appName, property) + } + } +} + +func setResourceType(appName string, processType string, r Resource, resourceType string) error { + if len(processType) == 0 { + processType = "_default_" + } + + resources := map[string]string{ + "cpu": r.CPU, + "memory": r.Memory, + "memory-swap": r.MemorySwap, + "network": r.Network, + "network-ingress": r.NetworkIngress, + "network-egress": r.NetworkEgress, + } + + hasValues := false + for _, value := range resources { + if value != "" { + hasValues = true + } + } + + if !hasValues { + reportResourceType(appName, processType, resourceType) + return nil + } + + noun := "limits" + if resourceType == "reserve" { + noun = "reservation" + } + message := fmt.Sprintf("Setting resource %v for %v", noun, appName) + if processType != "_default_" { + message = fmt.Sprintf("%v (%v)", message, processType) + } + common.LogInfo2Quiet(message) + + for key, value := range resources { + if value != "" { + common.LogVerbose(fmt.Sprintf("%v: %v", key, value)) + } + + property := propertyKey(processType, resourceType, key) + err := common.PropertyWrite("resource", appName, property, value) + if err != nil { + return err + } + } + + return nil +} + +func reportResourceType(appName string, processType string, resourceType string) { + noun := "limits" + if resourceType == "reserve" { + noun = "reservation" + } + + message := fmt.Sprintf("resource %v %v information", noun, appName) + if processType != "_default_" { + message = fmt.Sprintf("%v (%v)", message, processType) + } + common.LogInfo2Quiet(message) + + resources := []string{ + "cpu", + "memory", + "memory-swap", + "network", + "network-ingress", + "network-egress", + } + + for _, key := range resources { + property := propertyKey(processType, resourceType, key) + value := common.PropertyGet("resource", appName, property) + common.LogVerbose(fmt.Sprintf("%v: %v", key, value)) + } + return +} diff --git a/plugins/resource/vendor/github.com/ryanuber/columnize/.travis.yml b/plugins/resource/vendor/github.com/ryanuber/columnize/.travis.yml new file mode 100644 index 000000000..1a0bbea6c --- /dev/null +++ b/plugins/resource/vendor/github.com/ryanuber/columnize/.travis.yml @@ -0,0 +1,3 @@ +language: go +go: + - tip diff --git a/plugins/resource/vendor/github.com/ryanuber/columnize/LICENSE b/plugins/resource/vendor/github.com/ryanuber/columnize/LICENSE new file mode 100644 index 000000000..b9c0e2b68 --- /dev/null +++ b/plugins/resource/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/resource/vendor/github.com/ryanuber/columnize/README.md b/plugins/resource/vendor/github.com/ryanuber/columnize/README.md new file mode 100644 index 000000000..e47634fc6 --- /dev/null +++ b/plugins/resource/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/resource/vendor/github.com/ryanuber/columnize/columnize.go b/plugins/resource/vendor/github.com/ryanuber/columnize/columnize.go new file mode 100644 index 000000000..915716a10 --- /dev/null +++ b/plugins/resource/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/resource/vendor/github.com/ryanuber/columnize/columnize_test.go b/plugins/resource/vendor/github.com/ryanuber/columnize/columnize_test.go new file mode 100644 index 000000000..89dabaa38 --- /dev/null +++ b/plugins/resource/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/scheduler-docker-local/scheduler-deploy b/plugins/scheduler-docker-local/scheduler-deploy index ef042625d..99f73a262 100755 --- a/plugins/scheduler-docker-local/scheduler-deploy +++ b/plugins/scheduler-docker-local/scheduler-deploy @@ -25,6 +25,8 @@ scheduler-docker-local-scheduler-deploy() { plugn trigger pre-deploy "$APP" "$IMAGE_TAG" is_image_herokuish_based "$IMAGE" && DOKKU_HEROKUISH=true + local IMAGE_SOURCE_TYPE="dockerfile" + [[ "$DOKKU_HEROKUISH" == "true" ]] && IMAGE_SOURCE_TYPE="herokuish" local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE" local oldids=$(get_app_container_ids "$APP") @@ -69,6 +71,7 @@ scheduler-docker-local-scheduler-deploy() { local DOCKER_ARGS DOCKER_ARGS=$(: | plugn trigger docker-args-deploy "$APP" "$IMAGE_TAG" "$PROC_TYPE" "$CONTAINER_INDEX") DOCKER_ARGS+=" -e DYNO=$PROC_TYPE.$CONTAINER_INDEX " + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-deploy "$APP" "$IMAGE_TAG" "$IMAGE_SOURCE_TYPE" "$PROC_TYPE" "$CONTAINER_INDEX") [[ "$DOKKU_TRACE" ]] && DOCKER_ARGS+=" -e TRACE=true " declare -a ARG_ARRAY diff --git a/plugins/scheduler-docker-local/scheduler-run b/plugins/scheduler-docker-local/scheduler-run index c28318c60..0b3a228ff 100755 --- a/plugins/scheduler-docker-local/scheduler-run +++ b/plugins/scheduler-docker-local/scheduler-run @@ -27,6 +27,10 @@ scheduler-docker-local-scheduler-run() { local DOCKER_ARGS=$(: | plugn trigger docker-args-run "$APP" "$IMAGE_TAG") [[ "$DOKKU_TRACE" ]] && local DOCKER_ARGS+=" -e TRACE=true " + local IMAGE_SOURCE_TYPE="dockerfile" + is_image_herokuish_based "$IMAGE" && IMAGE_SOURCE_TYPE="herokuish" + DOCKER_ARGS+=$(: | plugn trigger docker-args-process-run "$APP" "$IMAGE_TAG" "$IMAGE_SOURCE_TYPE" "$PHASE_SCRIPT_KEY") + declare -a ARG_ARRAY eval "ARG_ARRAY=($DOCKER_ARGS)" @@ -38,7 +42,7 @@ scheduler-docker-local-scheduler-run() { fi has_tty && DOKKU_RUN_OPTS+=" -i -t" - is_image_herokuish_based "$IMAGE" && local EXEC_CMD="/exec" + [[ "$IMAGE_SOURCE_TYPE" == "herokuish" ]] && local EXEC_CMD="/exec" DOKKU_QUIET_OUTPUT=1 extract_procfile "$APP" diff --git a/rpm.mk b/rpm.mk index 9bb455a60..ddb7fe073 100644 --- a/rpm.mk +++ b/rpm.mk @@ -46,51 +46,17 @@ rpm-herokuish: --license 'MIT License' \ . -rpm-dokku: - rm -rf /tmp/tmp /tmp/build $(BUILD_DIRECTORY)/dokku_*_$(RPM_ARCHITECTURE).rpm - mkdir -p /tmp/tmp /tmp/build +rpm-dokku: /tmp/build-dokku/var/lib/dokku/GIT_REV + rm -f $(BUILD_DIRECTORY)/dokku_*_$(RPM_ARCHITECTURE).rpm - mkdir -p /tmp/build/usr/share/bash-completion/completions - mkdir -p /tmp/build/usr/bin - mkdir -p /tmp/build/usr/share/doc/dokku - mkdir -p /tmp/build/usr/share/dokku/contrib - mkdir -p /tmp/build/usr/share/lintian/overrides - mkdir -p /tmp/build/usr/share/man/man1 - mkdir -p /tmp/build/var/lib/dokku/core-plugins/available - - cp dokku /tmp/build/usr/bin - cp LICENSE /tmp/build/usr/share/doc/dokku/copyright - cp contrib/bash-completion /tmp/build/usr/share/bash-completion/completions/dokku - find . -name ".DS_Store" -depth -exec rm {} \; - $(MAKE) go-build - cp common.mk /tmp/build/var/lib/dokku/core-plugins/common.mk - cp -r plugins/* /tmp/build/var/lib/dokku/core-plugins/available - find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do cd /tmp/build/var/lib/dokku/core-plugins/available/$$plugin && if [ -e Makefile ]; then $(MAKE) src-clean; fi; done - find plugins/ -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | while read plugin; do touch /tmp/build/var/lib/dokku/core-plugins/available/$$plugin/.core; done - rm /tmp/build/var/lib/dokku/core-plugins/common.mk - $(MAKE) help2man - $(MAKE) addman - cp /usr/local/share/man/man1/dokku.1 /tmp/build/usr/share/man/man1/dokku.1 - gzip -9 /tmp/build/usr/share/man/man1/dokku.1 - cp contrib/dokku-installer.py /tmp/build/usr/share/dokku/contrib -ifeq ($(DOKKU_VERSION),master) - git describe --tags > /tmp/build/var/lib/dokku/VERSION -else - echo $(DOKKU_VERSION) > /tmp/build/var/lib/dokku/VERSION -endif - cat /tmp/build/var/lib/dokku/VERSION | cut -d '-' -f 1 | cut -d 'v' -f 2 > /tmp/build/var/lib/dokku/STABLE_VERSION + cat /tmp/build-dokku/var/lib/dokku/VERSION | cut -d '-' -f 1 | cut -d 'v' -f 2 > /tmp/build-dokku/var/lib/dokku/STABLE_VERSION ifneq (,$(findstring false,$(IS_RELEASE))) - sed -i.bak -e "s/^/`date +%s`-/" /tmp/build/var/lib/dokku/STABLE_VERSION && rm /tmp/build/var/lib/dokku/STABLE_VERSION.bak -endif -ifdef DOKKU_GIT_REV - echo "$(DOKKU_GIT_REV)" > /tmp/build/var/lib/dokku/GIT_REV -else - git rev-parse HEAD > /tmp/build/var/lib/dokku/GIT_REV + sed -i.bak -e "s/^/`date +%s`-/" /tmp/build-dokku/var/lib/dokku/STABLE_VERSION && rm /tmp/build-dokku/var/lib/dokku/STABLE_VERSION.bak endif @echo "-> Creating rpm package" - VERSION=$$(cat /tmp/build/var/lib/dokku/STABLE_VERSION); \ - sudo fpm -t rpm -s dir -C /tmp/build -n dokku \ + VERSION=$$(cat /tmp/build-dokku/var/lib/dokku/STABLE_VERSION); \ + sudo fpm -t rpm -s dir -C /tmp/build-dokku -n dokku \ --version "$$VERSION" \ --architecture $(RPM_ARCHITECTURE) \ --package "$(BUILD_DIRECTORY)/$(DOKKU_RPM_PACKAGE_NAME)" \ diff --git a/tests.mk b/tests.mk index 6638e6da6..c42dc22b3 100644 --- a/tests.mk +++ b/tests.mk @@ -69,10 +69,11 @@ else ifeq ($(shell grep dokku.me /root/.ssh/config),) endif ifneq ($(wildcard /etc/ssh/sshd_config),) + sed --in-place "s/^#Port 22$\/Port 22/g" /etc/ssh/sshd_config ifeq ($(shell grep 22333 /etc/ssh/sshd_config),) sed --in-place "s:^Port 22:Port 22 \\nPort 22333:g" /etc/ssh/sshd_config - restart ssh endif + service ssh restart endif @echo "-----> Installing SSH public key..." diff --git a/tests/unit/10_apps.bats b/tests/unit/10_apps.bats old mode 100755 new mode 100644 index e92e07a2c..ccb4bbf44 --- a/tests/unit/10_apps.bats +++ b/tests/unit/10_apps.bats @@ -2,11 +2,11 @@ load test_helper -setup () { +setup() { global_setup } -teardown () { +teardown() { global_teardown } diff --git a/tests/unit/20_resource.bats b/tests/unit/20_resource.bats new file mode 100644 index 000000000..a8f92cc89 --- /dev/null +++ b/tests/unit/20_resource.bats @@ -0,0 +1,185 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + global_setup + create_app +} + +teardown() { + destroy_app + global_teardown +} + +@test "(resource) resource:limit" { + run /bin/bash -c "dokku resource:limit $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_output_contains "resource limits $TEST_APP information" + + deploy_app + run /bin/bash -c "dokku resource:limit --memory 512MB $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "0" + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:limit --memory 1024MB --process-type worker $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:limit-clear --process-type worker $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:limit-clear --process-type web $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:limit-clear $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.Memory}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "0" +} + +@test "(resource) resource:reserve" { + run /bin/bash -c "dokku resource:reserve $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_output_contains "resource reservation $TEST_APP information" + + deploy_app + run /bin/bash -c "dokku resource:reserve --memory 512MB $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "0" + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:reserve --memory 1024MB --process-type worker $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:reserve-clear --process-type worker $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:reserve-clear --process-type web $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "536870912" + + run /bin/bash -c "dokku resource:reserve-clear $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + dokku ps:rebuild "$TEST_APP" + CID=$(< $DOKKU_ROOT/$TEST_APP/CONTAINER.web.1) + run /bin/bash -c "docker inspect --format '{{.HostConfig.MemoryReservation}}' $CID" + echo "output: $output" + echo "status: $status" + assert_output "0" +} + +@test "(resource) resource:limit-clear" { + run /bin/bash -c "dokku resource:limit-clear $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku resource:limit-clear --process-type web $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(resource) resource:reserve-clear" { + run /bin/bash -c "dokku resource:reserve-clear $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku resource:reserve-clear --process-type web $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success +}