Merge pull request #3469 from dokku/resource-management

Resource management
This commit is contained in:
Jose Diaz-Gonzalez
2019-04-08 17:17:42 -04:00
committed by GitHub
52 changed files with 2614 additions and 110 deletions

View File

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

55
deb.mk
View File

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

View File

@@ -0,0 +1,381 @@
# Resource Management
> New as of 0.15.0
```
resource:limit [--process-type <process-type>] [RESOURCE_OPTS...] <app> # Limit resources for a given app/process-type combination
resource:limit-clear [--process-type <process-type>] <app> # Limit resources for a given app/process-type combination
resource:report [<app>] [<flag>] # Displays a resource report for one or more apps
resource:reserve [--process-type <process-type>] [RESOURCE_OPTS...] <app> # Reserve resources for a given app/process-type combination
resource:reserve-clear [--process-type <process-type>] <app> # 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
```

View File

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

View File

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

35
package.mk Normal file
View File

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

View File

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

View File

@@ -1,4 +1,4 @@
[plugin]
description = "dokku core buildpacks plugin"
version = "0.14.2"
version = "0.14.6"
[plugin.config]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

9
plugins/resource/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
/commands
/subcommands/*
/triggers/*
/install
/post-delete
/report
/docker-args-process-deploy
/docker-args-process-run
/resource-get-property

38
plugins/resource/Makefile Normal file
View File

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

View File

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

View File

@@ -0,0 +1,4 @@
[plugin]
description = "dokku core resource plugin"
version = "0.14.6"
[plugin.config]

View File

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

View File

@@ -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 <process-type>] [RESOURCE_OPTS...] <app>, Limit resources for a given app/process-type combination
resource:limit-clear [--process-type <process-type>] <app>, Limit resources for a given app/process-type combination
resource:report [<app>] [<flag>], Displays a resource report for one or more apps
resource:reserve [--process-type <process-type>] [RESOURCE_OPTS...] <app>, Reserve resources for a given app/process-type combination
resource:reserve-clear [--process-type <process-type>] <app>, 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))
}

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

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

View File

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

View File

@@ -0,0 +1,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())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
Copyright (c) 2016 Ryan Uber
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,69 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](https://godoc.org/github.com/ryanuber/columnize)
Columnize is a really small Go package that makes building CLI's a little bit
easier. In some CLI designs, you want to output a number similar items in a
human-readable way with nicely aligned columns. However, figuring out how wide
to make each column is a boring problem to solve and eats your valuable time.
Here is an example:
```go
package main
import (
"fmt"
"github.com/ryanuber/columnize"
)
func main() {
output := []string{
"Name | Gender | Age",
"Bob | Male | 38",
"Sally | Female | 26",
}
result := columnize.SimpleFormat(output)
fmt.Println(result)
}
```
As you can see, you just pass in a list of strings. And the result:
```
Name Gender Age
Bob Male 38
Sally Female 26
```
Columnize is tolerant of missing or empty fields, or even empty lines, so
passing in extra lines for spacing should show up as you would expect.
Configuration
=============
Columnize is configured using a `Config`, which can be obtained by calling the
`DefaultConfig()` method. You can then tweak the settings in the resulting
`Config`:
```
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = ""
config.Empty = ""
```
* `Delim` is the string by which columns of **input** are delimited
* `Glue` is the string by which columns of **output** are delimited
* `Prefix` is a string by which each line of **output** is prefixed
* `Empty` is a string used to replace blank values found in output
You can then pass the `Config` in using the `Format` method (signature below) to
have text formatted to your liking.
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.

View File

@@ -0,0 +1,178 @@
package columnize
import (
"bytes"
"fmt"
"strings"
)
// Config can be used to tune certain parameters which affect the way
// in which Columnize will format output text.
type Config struct {
// The string by which the lines of input will be split.
Delim string
// The string by which columns of output will be separated.
Glue string
// The string by which columns of output will be prefixed.
Prefix string
// A replacement string to replace empty fields
Empty string
}
// DefaultConfig returns a *Config with default values.
func DefaultConfig() *Config {
return &Config{
Delim: "|",
Glue: " ",
Prefix: "",
Empty: "",
}
}
// MergeConfig merges two config objects together and returns the resulting
// configuration. Values from the right take precedence over the left side.
func MergeConfig(a, b *Config) *Config {
var result Config = *a
// Return quickly if either side was nil
if a == nil || b == nil {
return &result
}
if b.Delim != "" {
result.Delim = b.Delim
}
if b.Glue != "" {
result.Glue = b.Glue
}
if b.Prefix != "" {
result.Prefix = b.Prefix
}
if b.Empty != "" {
result.Empty = b.Empty
}
return &result
}
// stringFormat, given a set of column widths and the number of columns in
// the current line, returns a sprintf-style format string which can be used
// to print output aligned properly with other lines using the same widths set.
func stringFormat(c *Config, widths []int, columns int) string {
// Create the buffer with an estimate of the length
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
// Start with the prefix, if any was given. The buffer will not return an
// error so it does not need to be handled
buf.WriteString(c.Prefix)
// Create the format string from the discovered widths
for i := 0; i < columns && i < len(widths); i++ {
if i == columns-1 {
buf.WriteString("%s\n")
} else {
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
}
}
return buf.String()
}
// elementsFromLine returns a list of elements, each representing a single
// item which will belong to a column of output.
func elementsFromLine(config *Config, line string) []interface{} {
seperated := strings.Split(line, config.Delim)
elements := make([]interface{}, len(seperated))
for i, field := range seperated {
value := strings.TrimSpace(field)
// Apply the empty value, if configured.
if value == "" && config.Empty != "" {
value = config.Empty
}
elements[i] = value
}
return elements
}
// runeLen calculates the number of visible "characters" in a string
func runeLen(s string) int {
l := 0
for _ = range s {
l++
}
return l
}
// widthsFromLines examines a list of strings and determines how wide each
// column should be considering all of the elements that need to be printed
// within it.
func widthsFromLines(config *Config, lines []string) []int {
widths := make([]int, 0, 8)
for _, line := range lines {
elems := elementsFromLine(config, line)
for i := 0; i < len(elems); i++ {
l := runeLen(elems[i].(string))
if len(widths) <= i {
widths = append(widths, l)
} else if widths[i] < l {
widths[i] = l
}
}
}
return widths
}
// Format is the public-facing interface that takes a list of strings and
// returns nicely aligned column-formatted text.
func Format(lines []string, config *Config) string {
conf := MergeConfig(DefaultConfig(), config)
widths := widthsFromLines(conf, lines)
// Estimate the buffer size
glueSize := len(conf.Glue)
var size int
for _, w := range widths {
size += w + glueSize
}
size *= len(lines)
// Create the buffer
buf := bytes.NewBuffer(make([]byte, 0, size))
// Create a cache for the string formats
fmtCache := make(map[int]string, 16)
// Create the formatted output using the format string
for _, line := range lines {
elems := elementsFromLine(conf, line)
// Get the string format using cache
numElems := len(elems)
stringfmt, ok := fmtCache[numElems]
if !ok {
stringfmt = stringFormat(conf, widths, numElems)
fmtCache[numElems] = stringfmt
}
fmt.Fprintf(buf, stringfmt, elems...)
}
// Get the string result
result := buf.String()
// Remove trailing newline without removing leading/trailing space
if n := len(result); n > 0 && result[n-1] == '\n' {
result = result[:n-1]
}
return result
}
// SimpleFormat is a convenience function to format text with the defaults.
func SimpleFormat(lines []string) string {
return Format(lines, nil)
}

View File

@@ -0,0 +1,306 @@
package columnize
import (
"fmt"
"testing"
crand "crypto/rand"
)
func TestListOfStringsInput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyLinesOutput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestLeadingSpacePreserved(t *testing.T) {
input := []string{
"| Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := " Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestColumnWidthCalculator(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Longer than A | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Longer than A Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func BenchmarkColumnWidthCalculator(b *testing.B) {
// Generate the input
input := []string{
"UUID A | UUID B | UUID C | Column D | Column E",
}
format := "%s|%s|%s|%s"
short := "short"
uuid := func() string {
buf := make([]byte, 16)
if _, err := crand.Read(buf); err != nil {
panic(fmt.Errorf("failed to read random bytes: %v", err))
}
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
buf[0:4],
buf[4:6],
buf[6:8],
buf[8:10],
buf[10:16])
}
for i := 0; i < 1000; i++ {
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
input = append(input, l)
}
config := DefaultConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Format(input, config)
}
}
func TestVariedInputSpacing(t *testing.T) {
input := []string{
"Column A |Column B| Column C",
"x|y| z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestUnmatchedColumnCounts(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Value A | Value B",
"Value A | Value B | Value C | Value D",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Value A Value B\n"
expected += "Value A Value B Value C Value D"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateDelimiter(t *testing.T) {
input := []string{
"Column | A % Column | B % Column | C",
"Value A % Value B % Value C",
}
config := DefaultConfig()
config.Delim = "%"
output := Format(input, config)
expected := "Column | A Column | B Column | C\n"
expected += "Value A Value B Value C"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateSpacingString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Glue = " "
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestSimpleFormat(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
output := SimpleFormat(input)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternatePrefixString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Prefix = " "
output := Format(input, config)
expected := " Column A Column B Column C\n"
expected += " x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyFieldReplacement(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | | z",
}
config := DefaultConfig()
config.Empty = "<none>"
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x <none> z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyConfigValues(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := Config{}
output := Format(input, &config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestMergeConfig(t *testing.T) {
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
conf3 := &Config{Delim: "c", Prefix: "c"}
m := MergeConfig(conf1, conf2)
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, conf3)
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, nil)
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, &Config{})
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
}

View File

@@ -0,0 +1,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
}

View File

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

View File

@@ -0,0 +1,20 @@
Copyright (c) 2016 Ryan Uber
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,69 @@
Columnize
=========
Easy column-formatted output for golang
[![Build Status](https://travis-ci.org/ryanuber/columnize.svg)](https://travis-ci.org/ryanuber/columnize)
[![GoDoc](https://godoc.org/github.com/ryanuber/columnize?status.svg)](https://godoc.org/github.com/ryanuber/columnize)
Columnize is a really small Go package that makes building CLI's a little bit
easier. In some CLI designs, you want to output a number similar items in a
human-readable way with nicely aligned columns. However, figuring out how wide
to make each column is a boring problem to solve and eats your valuable time.
Here is an example:
```go
package main
import (
"fmt"
"github.com/ryanuber/columnize"
)
func main() {
output := []string{
"Name | Gender | Age",
"Bob | Male | 38",
"Sally | Female | 26",
}
result := columnize.SimpleFormat(output)
fmt.Println(result)
}
```
As you can see, you just pass in a list of strings. And the result:
```
Name Gender Age
Bob Male 38
Sally Female 26
```
Columnize is tolerant of missing or empty fields, or even empty lines, so
passing in extra lines for spacing should show up as you would expect.
Configuration
=============
Columnize is configured using a `Config`, which can be obtained by calling the
`DefaultConfig()` method. You can then tweak the settings in the resulting
`Config`:
```
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = ""
config.Empty = ""
```
* `Delim` is the string by which columns of **input** are delimited
* `Glue` is the string by which columns of **output** are delimited
* `Prefix` is a string by which each line of **output** is prefixed
* `Empty` is a string used to replace blank values found in output
You can then pass the `Config` in using the `Format` method (signature below) to
have text formatted to your liking.
See the [godoc](https://godoc.org/github.com/ryanuber/columnize) page for usage.

View File

@@ -0,0 +1,178 @@
package columnize
import (
"bytes"
"fmt"
"strings"
)
// Config can be used to tune certain parameters which affect the way
// in which Columnize will format output text.
type Config struct {
// The string by which the lines of input will be split.
Delim string
// The string by which columns of output will be separated.
Glue string
// The string by which columns of output will be prefixed.
Prefix string
// A replacement string to replace empty fields
Empty string
}
// DefaultConfig returns a *Config with default values.
func DefaultConfig() *Config {
return &Config{
Delim: "|",
Glue: " ",
Prefix: "",
Empty: "",
}
}
// MergeConfig merges two config objects together and returns the resulting
// configuration. Values from the right take precedence over the left side.
func MergeConfig(a, b *Config) *Config {
var result Config = *a
// Return quickly if either side was nil
if a == nil || b == nil {
return &result
}
if b.Delim != "" {
result.Delim = b.Delim
}
if b.Glue != "" {
result.Glue = b.Glue
}
if b.Prefix != "" {
result.Prefix = b.Prefix
}
if b.Empty != "" {
result.Empty = b.Empty
}
return &result
}
// stringFormat, given a set of column widths and the number of columns in
// the current line, returns a sprintf-style format string which can be used
// to print output aligned properly with other lines using the same widths set.
func stringFormat(c *Config, widths []int, columns int) string {
// Create the buffer with an estimate of the length
buf := bytes.NewBuffer(make([]byte, 0, (6+len(c.Glue))*columns))
// Start with the prefix, if any was given. The buffer will not return an
// error so it does not need to be handled
buf.WriteString(c.Prefix)
// Create the format string from the discovered widths
for i := 0; i < columns && i < len(widths); i++ {
if i == columns-1 {
buf.WriteString("%s\n")
} else {
fmt.Fprintf(buf, "%%-%ds%s", widths[i], c.Glue)
}
}
return buf.String()
}
// elementsFromLine returns a list of elements, each representing a single
// item which will belong to a column of output.
func elementsFromLine(config *Config, line string) []interface{} {
seperated := strings.Split(line, config.Delim)
elements := make([]interface{}, len(seperated))
for i, field := range seperated {
value := strings.TrimSpace(field)
// Apply the empty value, if configured.
if value == "" && config.Empty != "" {
value = config.Empty
}
elements[i] = value
}
return elements
}
// runeLen calculates the number of visible "characters" in a string
func runeLen(s string) int {
l := 0
for _ = range s {
l++
}
return l
}
// widthsFromLines examines a list of strings and determines how wide each
// column should be considering all of the elements that need to be printed
// within it.
func widthsFromLines(config *Config, lines []string) []int {
widths := make([]int, 0, 8)
for _, line := range lines {
elems := elementsFromLine(config, line)
for i := 0; i < len(elems); i++ {
l := runeLen(elems[i].(string))
if len(widths) <= i {
widths = append(widths, l)
} else if widths[i] < l {
widths[i] = l
}
}
}
return widths
}
// Format is the public-facing interface that takes a list of strings and
// returns nicely aligned column-formatted text.
func Format(lines []string, config *Config) string {
conf := MergeConfig(DefaultConfig(), config)
widths := widthsFromLines(conf, lines)
// Estimate the buffer size
glueSize := len(conf.Glue)
var size int
for _, w := range widths {
size += w + glueSize
}
size *= len(lines)
// Create the buffer
buf := bytes.NewBuffer(make([]byte, 0, size))
// Create a cache for the string formats
fmtCache := make(map[int]string, 16)
// Create the formatted output using the format string
for _, line := range lines {
elems := elementsFromLine(conf, line)
// Get the string format using cache
numElems := len(elems)
stringfmt, ok := fmtCache[numElems]
if !ok {
stringfmt = stringFormat(conf, widths, numElems)
fmtCache[numElems] = stringfmt
}
fmt.Fprintf(buf, stringfmt, elems...)
}
// Get the string result
result := buf.String()
// Remove trailing newline without removing leading/trailing space
if n := len(result); n > 0 && result[n-1] == '\n' {
result = result[:n-1]
}
return result
}
// SimpleFormat is a convenience function to format text with the defaults.
func SimpleFormat(lines []string) string {
return Format(lines, nil)
}

View File

@@ -0,0 +1,306 @@
package columnize
import (
"fmt"
"testing"
crand "crypto/rand"
)
func TestListOfStringsInput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyLinesOutput(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestLeadingSpacePreserved(t *testing.T) {
input := []string{
"| Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
output := Format(input, config)
expected := " Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestColumnWidthCalculator(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Longer than A | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Longer than A Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func TestColumnWidthCalculatorNonASCII(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"⌘⌘⌘⌘⌘⌘⌘⌘ | Longer than B | Longer than C",
"short | short | short",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "⌘⌘⌘⌘⌘⌘⌘⌘ Longer than B Longer than C\n"
expected += "short short short"
if output != expected {
printableProof := fmt.Sprintf("\nGot: %+q", output)
printableProof += fmt.Sprintf("\nExpected: %+q", expected)
t.Fatalf("\n%s", printableProof)
}
}
func BenchmarkColumnWidthCalculator(b *testing.B) {
// Generate the input
input := []string{
"UUID A | UUID B | UUID C | Column D | Column E",
}
format := "%s|%s|%s|%s"
short := "short"
uuid := func() string {
buf := make([]byte, 16)
if _, err := crand.Read(buf); err != nil {
panic(fmt.Errorf("failed to read random bytes: %v", err))
}
return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x",
buf[0:4],
buf[4:6],
buf[6:8],
buf[8:10],
buf[10:16])
}
for i := 0; i < 1000; i++ {
l := fmt.Sprintf(format, uuid()[:8], uuid()[:12], uuid(), short, short)
input = append(input, l)
}
config := DefaultConfig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Format(input, config)
}
}
func TestVariedInputSpacing(t *testing.T) {
input := []string{
"Column A |Column B| Column C",
"x|y| z",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestUnmatchedColumnCounts(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"Value A | Value B",
"Value A | Value B | Value C | Value D",
}
config := DefaultConfig()
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "Value A Value B\n"
expected += "Value A Value B Value C Value D"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateDelimiter(t *testing.T) {
input := []string{
"Column | A % Column | B % Column | C",
"Value A % Value B % Value C",
}
config := DefaultConfig()
config.Delim = "%"
output := Format(input, config)
expected := "Column | A Column | B Column | C\n"
expected += "Value A Value B Value C"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternateSpacingString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Glue = " "
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestSimpleFormat(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
output := SimpleFormat(input)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestAlternatePrefixString(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := DefaultConfig()
config.Prefix = " "
output := Format(input, config)
expected := " Column A Column B Column C\n"
expected += " x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyFieldReplacement(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | | z",
}
config := DefaultConfig()
config.Empty = "<none>"
output := Format(input, config)
expected := "Column A Column B Column C\n"
expected += "x <none> z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestEmptyConfigValues(t *testing.T) {
input := []string{
"Column A | Column B | Column C",
"x | y | z",
}
config := Config{}
output := Format(input, &config)
expected := "Column A Column B Column C\n"
expected += "x y z"
if output != expected {
t.Fatalf("\nexpected:\n%s\n\ngot:\n%s", expected, output)
}
}
func TestMergeConfig(t *testing.T) {
conf1 := &Config{Delim: "a", Glue: "a", Prefix: "a", Empty: "a"}
conf2 := &Config{Delim: "b", Glue: "b", Prefix: "b", Empty: "b"}
conf3 := &Config{Delim: "c", Prefix: "c"}
m := MergeConfig(conf1, conf2)
if m.Delim != "b" || m.Glue != "b" || m.Prefix != "b" || m.Empty != "b" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, conf3)
if m.Delim != "c" || m.Glue != "a" || m.Prefix != "c" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, nil)
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
m = MergeConfig(conf1, &Config{})
if m.Delim != "a" || m.Glue != "a" || m.Prefix != "a" || m.Empty != "a" {
t.Fatalf("bad: %#v", m)
}
}

View File

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

View File

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

46
rpm.mk
View File

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

View File

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

4
tests/unit/10_apps.bats Executable file → Normal file
View File

@@ -2,11 +2,11 @@
load test_helper
setup () {
setup() {
global_setup
}
teardown () {
teardown() {
global_teardown
}

185
tests/unit/20_resource.bats Normal file
View File

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