Merge pull request #4149 from dokku/go-ps-plugin

Rewrite ps plugin in golang
This commit is contained in:
Jose Diaz-Gonzalez
2020-11-21 23:03:36 -05:00
committed by GitHub
83 changed files with 2650 additions and 1760 deletions

View File

@@ -1,7 +1,7 @@
DOKKU_VERSION ?= master
HEROKUISH_VERSION ?= 0.5.19
PROCFILE_VERSION ?= 0.8.2
PROCFILE_VERSION ?= 0.11.0
PLUGN_VERSION ?= 0.5.0
SIGIL_VERSION ?= 0.6.0
SSHCOMMAND_VERSION ?= 0.11.0

View File

@@ -1,4 +1,5 @@
GO_ARGS ?= -a
GO_PLUGIN_MAKE_TARGET ?= build
GO_REPO_ROOT := /go/src/github.com/dokku/dokku
BUILD_IMAGE := golang:1.14.4
@@ -15,10 +16,10 @@ build-in-docker: clean
-e GO111MODULE=on \
-w $(GO_REPO_ROOT)/plugins/$(PLUGIN_NAME) \
$(BUILD_IMAGE) \
bash -c "GO_ARGS='$(GO_ARGS)' make -j4 build" || exit $$?
bash -c "GO_ARGS='$(GO_ARGS)' make -j4 $(GO_PLUGIN_MAKE_TARGET)" || exit $$?
clean:
rm -rf commands subcommands triggers prop
rm -rf $(BUILD)
find . -xtype l -delete
commands: **/**/commands.go

View File

@@ -140,7 +140,7 @@ main() {
esac
[[ " apps certs help ls nginx shell storage trace version " == *" $CMD "* ]] && unset APP
[[ " certs:chain domains:add-global domains:remove-global domains:set-global ps:rebuildall ps:restartall ps:restore ps:startall ps:stopall " == *" $CMD "* ]] && unset APP
[[ " certs:chain domains:add-global domains:remove-global domains:set-global ps:restore " == *" $CMD "* ]] && unset APP
[[ "$CMD" =~ events*|plugin*|ssh-keys* ]] && unset APP
[[ -n "$APP_ARG" ]] && [[ "$APP_ARG" == "--global" ]] && unset APP
[[ -n "$@" ]] && [[ -n "$APP" ]] && app_arg="--app $APP"

2
debian/control vendored
View File

@@ -3,7 +3,7 @@ Version: 0.21.4
Section: web
Priority: optional
Architecture: amd64
Depends: locales, git, cpio, curl, man-db, netcat, sshcommand (>= 0.10.0), docker-engine-cs (>= 17.05.0) | docker-engine (>= 17.05.0) | docker-io (>= 17.05.0) | docker.io (>= 17.05.0) | docker-ce (>= 17.05.0) | docker-ee (>= 17.05.0) | moby-engine, net-tools, software-properties-common, procfile-util, python-software-properties | python3-software-properties, rsyslog, dos2unix, jq
Depends: locales, git, cpio, curl, man-db, netcat, sshcommand (>= 0.10.0), docker-engine-cs (>= 17.05.0) | docker-engine (>= 17.05.0) | docker-io (>= 17.05.0) | docker.io (>= 17.05.0) | docker-ce (>= 17.05.0) | docker-ee (>= 17.05.0) | moby-engine, net-tools, software-properties-common, procfile-util (>= 0.11.0), python-software-properties | python3-software-properties, rsyslog, dos2unix, jq
Recommends: herokuish (>= 0.3.4), parallel, dokku-update, dokku-event-listener
Pre-Depends: gliderlabs-sigil, nginx (>= 1.8.0) | openresty, dnsutils, cgroupfs-mount | cgroup-lite, plugn (>= 0.3.0), sudo, python3, debconf
Maintainer: Jose Diaz-Gonzalez <dokku@josediazgonzalez.com>

View File

@@ -3,5 +3,19 @@
## Changes
- Underscores are no longer valid characters in app names. Please rename applications before upgrading.
- Process type names specified in Procfile may no longer use characters not valid in DNS records.
- Process type names specified in Procfile may no longer use characters not valid in DNS Label Names ([RFC 1123](https://tools.ietf.org/html/rfc1123)).
- The minimum Docker version is now 17.05.0.
## Removals
The `ps` command has had a few removals as a result of a rewrite to Golang:
- The `ps:set-restart-policy` command has been removed in favor the `ps:set` command.
- The `ps:restart-policy` command has been removed in favor of the `ps:report` command.
- The `ps:rebuildall` command has been removed in favor of calling the `ps:rebuild` with the `--all` flag instead of an app name.
- The `ps:restartall` command has been removed in favor of calling the `ps:restart` with the `--all` flag instead of an app name.
- The `ps:stopall` command has been removed in favor of calling the `ps:stop` with the `--all` flag instead of an app name.
- The `ps:startall` command has been removed in favor of calling the `ps:start` with the `--all` flag instead of an app name.
- The `DOKKU_PROCFILE` file is no longer located in the git directory for a given app on the server. It's location is currently not exposed.
- The `procfile-get-command` plugin trigger no longer takes a `procfile path` as the final argument.
- All `ps` functions have been removed. Please us a trigger as appropriate or file a PR to have a new trigger/wrapper function added.

View File

@@ -11,7 +11,15 @@ nginx:show-config <app> # Display app nginx config
nginx:validate-config [<app>] [--clean] # Validates and optionally cleans up invalid nginx configurations
```
## Binding to specific addresses
## Usage
### Request Proxying
By default, the `web` process is the only process proxied by the nginx proxy implementation. Proxying to other process types may be handled by a custom `nginx.conf.sigil` file, as generally described [below](/docs/configuration/nginx.md#customizing-the-nginx-configuration)
Nginx will proxy the requests in a [round-robin balancing fashion](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream) to the different deployed (scaled) containers running the `web` proctype. This way, the host's resources can be fully leveraged for single-threaded applications (e.g. `dokku ps:scale node-js-app web=4` on a 4-core machine)
### Binding to specific addresses
> New as of 0.19.2
@@ -33,7 +41,7 @@ dokku nginx:set node-js-app bind-address-ipv6
Users with apps that contain a custom `nginx.conf.sigil` file will need to modify the files to respect the new `NGINX_BIND_ADDRESS_IPV4` and `NGINX_BIND_ADDRESS_IPV6` variables.
## HSTS Header
### HSTS Header
> New as of 0.20.0
@@ -53,7 +61,7 @@ The following options are also available via the `nginx:set` command:
Beware that if you enable the header and a subsequent deploy of your application results in an HTTP deploy (for whatever reason), the way the header works means that a browser will not attempt to request the HTTP version of your site if the HTTPS version fails until the max-age is reached.
## Checking access logs
### Checking access logs
You may check nginx access logs via the `nginx:access-logs` command. This assumes that app access logs are being stored in `/var/log/nginx/$APP-access.log`, as is the default in the generated `nginx.conf`.
@@ -67,7 +75,7 @@ You may also follow the logs by specifying the `-t` flag.
dokku nginx:access-logs node-js-app -t
```
## Checking error logs
### Checking error logs
You may check nginx error logs via the `nginx:access-logs` command. This assumes that app error logs are being stored in `/var/log/nginx/$APP-error.log`, as is the default in the generated `nginx.conf`.
@@ -81,7 +89,7 @@ You may also follow the logs by specifying the `-t` flag.
dokku nginx:error-logs node-js-app -t
```
## Changing log path
### Changing log path
> New as of 0.20.1
@@ -106,7 +114,7 @@ dokku nginx:set node-js-app error-log-path
In all cases, the nginx config must be regenerated after setting the above values.
## Specifying a read timeout
### Specifying a read timeout
> New as of 0.21.0
@@ -126,7 +134,7 @@ dokku nginx:set node-js-app proxy-read-timeout
In all cases, the nginx config must be regenerated after setting the above value.
## Showing the nginx config
### Showing the nginx config
For debugging purposes, it may be useful to show the nginx config. This can be achieved via the `nginx:show-config` command.
@@ -134,7 +142,7 @@ For debugging purposes, it may be useful to show the nginx config. This can be a
dokku nginx:show-config node-js-app
```
## Validating nginx configs
### Validating nginx configs
It may be desired to validate an nginx config outside of the deployment process. To do so, run the `nginx:validate-config` command. With no arguments, this will validate all app nginx configs, one at a time. A minimal wrapper nginx config is generated for each app's nginx config, upon which `nginx -t` will be run.
@@ -158,7 +166,7 @@ The `--clean` flag may also be specified for a given app:
dokku nginx:validate-config node-js-app --clean
```
## Customizing the nginx configuration
### Customizing the nginx configuration
> New as of 0.5.0
@@ -187,7 +195,7 @@ dokku nginx:set node-js-app disable-custom-config true
Unsetting this value is the same as enabling custom nginx config usage.
### Available template variables
#### Available template variables
```
{{ .APP }} Application name
@@ -207,7 +215,7 @@ Finally, each process type has it's network listeners - a list of IP:PORT pairs
> Note: Application config variables are available for use in custom templates. To do so, use the form of `{{ var "FOO" }}` to access a variable named `FOO`.
### Customizing via configuration files included by the default templates
#### Customizing via configuration files included by the default templates
The default nginx.conf template will include everything from your apps `nginx.conf.d/` subdirectory in the main `server {}` block (see above):
@@ -240,7 +248,7 @@ Your `nginx.conf` file - not to be confused with Dokku's `nginx.conf.sigil` - wo
Please adjust the `Procfile` and `nginx.conf` file as appropriate.
## Custom Error Pages
### Custom Error Pages
By default, Dokku provides custom error pages for the following three categories of errors:
@@ -250,7 +258,7 @@ By default, Dokku provides custom error pages for the following three categories
These are provided as an alternative to the generic Nginx error page, are shared for _all_ applications, and their contents are located on disk at `/var/lib/dokku/data/nginx-vhosts/dokku-errors`. To customize them for a specific app, create a custom `nginx.conf.sigil` as described above and change the paths to point elsewhere.
## Default site
### Default site
By default, Dokku will route any received request with an unknown HOST header value to the lexicographically first site in the nginx config stack. If this is not the desired behavior, you may want to add the following configuration to the global nginx configuration.
@@ -294,34 +302,36 @@ The configuration file must be loaded before `/etc/nginx/conf.d/dokku.conf`, so
Alternatively, you may push an app to your Dokku host with a name like "00-default". As long as it lists first in `ls /home/dokku/*/nginx.conf | head`, it will be used as the default nginx vhost.
## Domains plugin
## Other
### Domains plugin
See the [domain configuration documentation](/docs/configuration/domains.md) for more information on how to configure domains for your app.
## Customizing hostnames
### Customizing hostnames
See the [customizing hostnames documentation](/docs/configuration/domains.md#customizing-hostnames) for more information on how to configure domains for your app.
## Disabling VHOSTS
### Disabling VHOSTS
See the [disabling vhosts documentation](/docs/configuration/domains.md#disabling-vhosts) for more information on how to disable domain usage for your app.
## Running behind a load balancer
### Running behind a load balancer
See the [load balancer documentation](/docs/configuration/ssl.md#running-behind-a-load-balancer) for more information on how to configure your nginx config for running behind a network load balancer.
## SSL Configuration
### SSL Configuration
See the [ssl documentation](/docs/configuration/ssl.md) for more information on how to configure SSL certificates for your application.
## Disabling Nginx
### Disabling Nginx
See the [proxy documentation](/docs/advanced-usage/proxy-management.md) for more information on how to disable nginx as the proxy implementation for your app.
## Managing Proxy Port mappings
### Managing Proxy Port mappings
See the [proxy documentation](/docs/advanced-usage/proxy-management.md#proxy-port-mapping) for more information on how to manage ports proxied for your app.
## Regenerating nginx config
### Regenerating nginx config
See the [proxy documentation](/docs/advanced-usage/proxy-management.md#regenerating-proxy-config) for more information on how to rebuild the nginx proxy configuration for your app.

View File

@@ -3,73 +3,110 @@
> New as of 0.3.14, Enhanced in 0.7.0
```
ps:inspect <app> # Displays a sanitized version of docker inspect for an app
ps:rebuild <app> # Rebuild an app from source
ps:rebuildall # Rebuild all apps from source
ps:report [<app>] [<flag>] # Displays a process report for one or more apps
ps:restart <app> # Restart app container(s)
ps:restart-policy <app> # Shows the restart-policy for an app
ps:restartall # Restart all deployed app containers
ps:scale <app> <proc>=<count> [<proc>=<count>] # Get/Set how many instances of a given process to run
ps:set-restart-policy <app> <policy> # Sets app restart-policy
ps:start <app> # Start app container(s)
ps:startall # Start all deployed app containers
ps:stop <app> # Stop app container(s)
ps:stopall # Stop all app container(s)
ps:inspect <app> # Displays a sanitized version of docker inspect for an app
ps:rebuild [--parallel count] [--all|<app>] # Rebuilds an app from source
ps:report [<app>] [<flag>] # Displays a process report for one or more apps
ps:restart [--parallel count] [--all|<app>] # Restart an app
ps:restore [<app>] # Start previously running apps e.g. after reboot
ps:scale [--skip-deploy] <app> <proc>=<count> [<proc>=<count>...] # Get/Set how many instances of a given process to run
ps:set <app> <key> <value> # Set or clear a ps property for an app
ps:start [--parallel count] [--all|<app>] # Start an app
ps:stop [--parallel count] [--all|<app>] # Stop an app
```
By default, Dokku will only start a single `web` process - if defined - though process scaling can be managed by the `ps` plugin or [via a custom `DOKKU_SCALE` file](/docs/deployment/process-management.md#manually-managing-process-scaling).
> The `web` proctype is the only proctype that will invoke custom checks as defined by a `CHECKS` file. It is also the only process type that will be launched in a container that is either proxied via nginx or bound to an external port. In the first case, nginx will proxy the requests in a [round-robin balancing fashion](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream) to the different deployed (scaled) containers running the `web` proctype. This way, the host's resources can be fully leveraged for single-threaded applications (e.g. `dokku ps:scale node-js-app web=4` on a 4-core machine)
## Usage
### Inspecting app containers
> New as of 0.13.0
A common administrative task to perform is calling `docker inspect` on the containers that are running for an application. This can be an error-prone task to perform, and may also reveal sensitive environment variables if not done correctly. Dokku provides a wrapper around this command via the `ps:inspect` command:
A common administrative task to perform is calling `docker inspect` on the containers that are running for an app. This can be an error-prone task to perform, and may also reveal sensitive environment variables if not done correctly. Dokku provides a wrapper around this command via the `ps:inspect` command:
```shell
dokku ps:inspect node-js-app
```
This command will gather all the running container IDs for your application and call `docker inspect`, sanitizing the output data so it can be copy-pasted elsewhere safely.
This command will gather all the running container IDs for your app and call `docker inspect`, sanitizing the output data so it can be copy-pasted elsewhere safely.
### Rebuilding applications
### Rebuilding apps
There are some Dokku commands which will not automatically rebuild an application's environment, or which can be told to skip a rebuild. For instance, you may wish to run multiple `config:set` commands without a restart so as to speed up configuration. In these cases, you can ultimately trigger an application rebuild using `ps:rebuild`
> Warning: This section applies only to git-based deploys, and only works deterministically for these apps. Please see the [images documentation](/docs/deployment/methods/images.md) and [tar documentation](/docs/deployment/methods/tar.md)
> in for more information concerning rebuilding those apps.
It may be useful to rebuild an app at will, such as for commands that do not rebuild an app or when skipping a rebuild after setting multiple config values. For these use cases, the `ps:rebuild` function can be used.
```shell
dokku ps:rebuild node-js-app
```
You may also rebuild all applications at once, which is useful when enabling/disabling a plugin that modifies all applications:
All apps may be rebuilt by using the `--all` flag.
```shell
dokku ps:rebuildall
dokku ps:rebuild --all
```
> The `ps:rebuild` and `ps:rebuildall` commands only work for applications for which there is a source, and thus
> will only always work deterministically for Git-deployed application. Please see
> the [images documentation](/docs/deployment/methods/images.md) and [tar documentation](/docs/deployment/methods/tar.md)
> in for more information concerning rebuilding those applications.
By default, rebuilding all apps happens serially. The parallelism may be controlled by the `--parallel` flag.
### Restarting applications
```shell
dokku ps:rebuild --all --parallel 2
```
Applications can be restarted, which is functionally identical to calling the `release_and_deploy` function on an application. Please note that any linked containers *must* be started before the application in order to have a successful boot.
Finally, the number of parallel workers may be automatically set to the number of CPUs available by setting the `--parallel` flag to `-1`
```shell
dokku ps:rebuild --all --parallel -1
```
A missing linked container will result in failure to boot apps. Services should all be started for apps being rebuilt.
### Restarting apps
An app may be restarted using the `ps:restart` command.
```shell
dokku ps:restart node-js-app
```
You may also trigger a restart on all applications at one time:
All apps may be restarted by using the `--all` flag.
```shell
dokku ps:restartall
dokku ps:restart --all
```
### `ps:scale` command
By default, restarting all apps happens serially. The parallelism may be controlled by the `--parallel` flag.
```shell
dokku ps:restart --all --parallel 2
```
Finally, the number of parallel workers may be automatically set to the number of CPUs available by setting the `--parallel` flag to `-1`
```shell
dokku ps:restart --all --parallel -1
```
A missing linked container will result in failure to boot apps. Services should all be started for apps being rebuilt.
### Displaying existing scale properties
Issuing the `ps:scale` command with no arguments will output the current scaling properties for an app.
```shell
dokku ps:scale node-js-app
```
```
-----> Scaling for python
proctype: qty
--------: ---
web: 1
```
### Scaling apps
#### Via CLI
> This functionality is disabled if the formation is managed via a file such as `DOKKU_SCALE`.
Dokku can also manage scaling itself via the `ps:scale` command. This command can be used to scale multiple process types at the same time.
@@ -83,133 +120,121 @@ Multiple process types can be scaled at once:
dokku ps:scale node-js-app web=1 worker=1
```
Issuing the `ps:scale` command with no process type argument will output the current scaling settings for an application:
If desired, the corresponding deploy will be skipped by using the `--skip-deploy` flag:
```shell
dokku ps:scale node-js-app
dokku ps:scale --skip-deploy node-js-app web=1
```
```
-----> Scaling for node-js-app
-----> proctype qty
-----> -------- ---
-----> web 1
-----> worker 1
```
#### Manually managing process scaling
### Stopping applications
> Using a `DOKKU_SCALE` formation file disables the ability to use `ps:scale` for scaling.
Deployed applications can be stopped using the `ps:stop` command. This turns off all running containers for an application, and will result in a **502 Bad Gateway** response for the default nginx proxy implementation.
A `DOKKU_SCALE` file can be committed to the root of the pushed app repository, and must be within the built image artifact in the image's working directory as shown below.
```shell
dokku ps:stop node-js-app
```
- Buildpacks: `/app/DOKKU_SCALE`
- Dockerfile: `WORKDIR/DOKKU_SCALE` or `/DOKKU_SCALE`
- Docker Image: `WORKDIR/DOKKU_SCALE` or `/DOKKU_SCALE`
You may also stop all applications at once:
```shell
dokku ps:stopall
```
### Starting applications
All stopped containers can be started using the `ps:start` command. This is similar to running `ps:restart`, except no action will be taken if the application containers are running.
```shell
dokku ps:start node-js-app
```
### Starting all applications
In some cases, it may be necessary to start all applications from scratch - e.g. if all Docker containers have been manually stopped. This can be executed via the `ps:startall` command, which supports parallelism in the same manner `ps:rebuildall`, `ps:restartall`, and `ps:stopall` do.
Be aware that no action will be taken if the application containers are running.
```shell
dokku ps:startall
```
## Restart policies
> New as of 0.7.0
By default, Dokku will automatically restart containers that exit with a non-zero status up to 10 times via the [on-failure Docker restart policy](https://docs.docker.com/engine/reference/run/#restart-policies-restart).
### Showing the current restart policy
The `ps:restart-policy` command will show the currently configured restart policy for an application. The default policy is `on-failure:10`
```shell
dokku ps:restart-policy node-js-app
```
```
=====> node-js-app restart-policy:
on-failure:10
```
### Setting the restart policy
You can configure this via the `ps:set-restart-policy` command:
```shell
# always restart an exited container
dokku ps:set-restart-policy node-js-app always
```
```
-----> Setting restart policy: always
```
```shell
# never restart an exited container
dokku ps:set-restart-policy node-js-app no
```
```
-----> Setting restart policy: no
```
```shell
# only restart it on Docker restart if it was not manually stopped
dokku ps:set-restart-policy node-js-app unless-stopped
```
```
-----> Setting restart policy: unless-stopped
```
```shell
# restart only on non-zero exit status
dokku ps:set-restart-policy node-js-app on-failure
```
```
-----> Setting restart policy: on-failure
```
```shell
# restart only on non-zero exit status up to 20 times
dokku ps:set-restart-policy node-js-app on-failure:20
```
```
-----> Setting restart policy: on-failure:20
```
Restart policies have no bearing on server reboot, and Dokku will always attempt to restart your applications at that point unless they were manually stopped.
## Manually managing process scaling
You can optionally *commit* a `DOKKU_SCALE` file to the root of your repository - *not* to the `/home/dokku/APP` directory. Dokku expects this file to contain one line for every process defined in your Procfile.
Example:
The `DOKKU_SCALE` file format is as follows:
```Procfile
web=1
worker=2
```
If it is not committed to the repository, the `DOKKU_SCALE` file will otherwise be automatically generated based on your `ps:scale` settings.
Removing the file will result in Dokku respecting the `ps:scale` command for setting scale values.
> *NOTE*: Dokku will always use the `DOKKU_SCALE` file that ships with the repo to override any local settings.
#### The `web` process
For initial app deploys, Dokku will default to starting a single `web` process for each app. This process may be defined within the `Procfile` or as the `CMD` (for Dockerfile or Docker image deploys). Scaling of the `web` process - and others - may be managed via `ps:scale` or a `DOKKU_SCALE` formation file either before or after the initial deploy.
There are also a few other exceptions for the `web` process.
- Custom checks defined by a `CHECKS` file only apply to the `web` process type.
- By default, the built-in nginx proxy implementation only proxies the `web` process (others may be handled via a custom `nginx.conf.sigil`).
- See the [nginx request proxying documentation](/docs/configuration/nginx.md#request-proxying) for more information on how nginx handles proxied requests.
- Only the `web` process may be bound to an external port.
### Stopping apps
Deployed apps can be stopped using the `ps:stop` command. This turns off all running containers for an app, and will result in a **502 Bad Gateway** response for the default nginx proxy implementation.
```shell
dokku ps:stop node-js-app
```
All apps may be stopped by using the `--all` flag.
```shell
dokku ps:stop --all
```
By default, stopping all apps happens serially. The parallelism may be controlled by the `--parallel` flag.
```shell
dokku ps:stop --all --parallel 2
```
Finally, the number of parallel workers may be automatically set to the number of CPUs available by setting the `--parallel` flag to `-1`
```shell
dokku ps:stop --all --parallel -1
```
### Starting apps
All stopped containers can be started using the `ps:start` command. This is similar to running `ps:restart`, except no action will be taken if the app containers are running.
```shell
dokku ps:start node-js-app
```
All apps may be started by using the `--all` flag.
```shell
dokku ps:start --all
```
By default, starting all apps happens serially. The parallelism may be controlled by the `--parallel` flag.
```shell
dokku ps:start --all --parallel 2
```
Finally, the number of parallel workers may be automatically set to the number of CPUs available by setting the `--parallel` flag to `-1`
```shell
dokku ps:start --all --parallel -1
```
### Restart policies
> New as of 0.7.0, Command Changed in 0.22.0
By default, Dokku will automatically restart containers that exit with a non-zero status up to 10 times via the [on-failure Docker restart policy](https://docs.docker.com/engine/reference/run/#restart-policies---restart).
#### Setting the restart policy
> A change in the restart policy must be followed by a `ps:rebuild` call.
You can configure this via the `ps:set` command:
```shell
# always restart an exited container
dokku ps:set node-js-app restart-policy always
# never restart an exited container
dokku ps:set node-js-app restart-policy no
# only restart it on Docker restart if it was not manually stopped
dokku ps:set node-js-app restart-policy unless-stopped
# restart only on non-zero exit status
dokku ps:set node-js-app restart-policy on-failure
# restart only on non-zero exit status up to 20 times
dokku ps:set node-js-app restart-policy on-failure:20
```
Restart policies have no bearing on server reboot, and Dokku will always attempt to restart your apps at that point unless they were manually stopped.

View File

@@ -1254,7 +1254,7 @@ verify_app_name "$APP"
### `post-stop`
- Description: Can be used to run commands after an app is manually stopped
- Invoked by: `dokku ps:stop` and `dokku ps:stopall`
- Invoked by: `dokku ps:stop`
- Arguments: `$APP`
- Example:
@@ -1481,7 +1481,7 @@ curl "https://dokku.me/starting/${APP}" || true
- Description: Extracts a Procfile from a given image to a path
- Invoked by: `internally`
- Arguments: `$APP $IMAGE $FORCE_EXTRACT $PROCFILE_PATH`
- Arguments: `$APP $IMAGE`
- Example:
```shell
@@ -1496,7 +1496,7 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
- Description: Fetches the command for a specific process type
- Invoked by: `internally`
- Arguments: `$APP $PROCESS_TYPE $PORT $PROCFILE_PATH`
- Arguments: `$APP $PROCESS_TYPE $PORT`
- Example:
```shell
@@ -1654,6 +1654,22 @@ fi
plugn trigger receive-app $APP $newrev
```
### `release-and-deploy`
- Description: Triggers a release of the image tag and a subsequent deploy
- Invoked by:
- Arguments: `$APP $IMAGE_TAG`
- Example:
```shell
#!/usr/bin/env bash
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
APP="$1";
# TODO
```
### `report`
- Description: Allows you to report on any custom configuration in use by your application

View File

@@ -36,7 +36,7 @@ All applications should be stopped, and all docker containers and images deleted
```shell
# stop all applications
dokku ps:stopall
dokku ps:stop --all
# cleanup containers and images
dokku cleanup

View File

@@ -45,7 +45,9 @@ Why do we recommend stopping all apps?
may happen in minor or major releases.
```shell
# for 0.11.4 and newer versions, use:
# for 0.22.0 and newer versions, use
# for versions between 0.11.4 and 0.21.4, use
dokku ps:stopall
# for versions between 0.8.1 and 0.11.3, use
@@ -61,10 +63,10 @@ After upgrading, you should rebuild the applications to take advantage of any
new buildpacks that were released:
```shell
dokku ps:rebuildall
dokku ps:rebuild --all
```
> If you have any applications deployed via the `tags` or `tar` commands, do not run the `ps:rebuildall` command,
> If you have any applications deployed via the `tags` or `tar` commands, do not run the `ps:rebuild --all` command,
> and instead trigger `ps:rebuild` manually for each `git`-deployed application:
>
> ```

View File

@@ -0,0 +1 @@
hook

View File

@@ -110,10 +110,8 @@ func getPhaseScript(appName string, image string, phase string) (string, error)
// getReleaseCommand extracts the release command from a given app's procfile
func getReleaseCommand(appName string, image string) string {
forceExtract := "true"
err := common.SuppressOutput(func() error {
return common.PlugnTrigger("procfile-extract", []string{appName, image, forceExtract}...)
return common.PlugnTrigger("procfile-extract", []string{appName, image}...)
})
if err != nil {

View File

@@ -4,7 +4,7 @@ import (
"bufio"
"fmt"
"os"
"path"
"path/filepath"
"github.com/dokku/dokku/plugins/common"
)
@@ -58,7 +58,7 @@ func TriggerPostExtract(appName string, sourceWorkDir string) {
return
}
buildpacksPath := path.Join(sourceWorkDir, ".buildpacks")
buildpacksPath := filepath.Join(sourceWorkDir, ".buildpacks")
file, err := os.OpenFile(buildpacksPath, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600)
if err != nil {
common.LogFail(fmt.Sprintf("Error writing .buildpacks file: %s", err.Error()))

View File

@@ -1,89 +1,23 @@
package common
import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"unicode"
sh "github.com/codeskyblue/go-sh"
columnize "github.com/ryanuber/columnize"
"github.com/ryanuber/columnize"
)
type errfunc func() error
// ShellCmd represents a shell command to be run for dokku
type ShellCmd struct {
Env map[string]string
Command *exec.Cmd
CommandString string
Args []string
ShowOutput bool
}
// NewShellCmd returns a new ShellCmd struct
func NewShellCmd(command string) *ShellCmd {
items := strings.Split(command, " ")
cmd := items[0]
args := items[1:]
return NewShellCmdWithArgs(cmd, args...)
}
// NewShellCmdWithArgs returns a new ShellCmd struct
func NewShellCmdWithArgs(cmd string, args ...string) *ShellCmd {
commandString := strings.Join(append([]string{cmd}, args...), " ")
return &ShellCmd{
Command: exec.Command(cmd, args...),
CommandString: commandString,
Args: args,
ShowOutput: true,
}
}
func (sc *ShellCmd) setup() {
env := os.Environ()
for k, v := range sc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
sc.Command.Env = env
if sc.ShowOutput {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
}
}
// Execute is a lightweight wrapper around exec.Command
func (sc *ShellCmd) Execute() bool {
sc.setup()
if err := sc.Command.Run(); err != nil {
return false
}
return true
}
// Output is a lightweight wrapper around exec.Command.Output()
func (sc *ShellCmd) Output() ([]byte, error) {
sc.setup()
return sc.Command.Output()
}
// CombinedOutput is a lightweight wrapper around exec.Command.CombinedOutput()
func (sc *ShellCmd) CombinedOutput() ([]byte, error) {
sc.setup()
return sc.Command.CombinedOutput()
}
// AppRoot returns the app root path
func AppRoot(appName string) string {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
@@ -128,163 +62,6 @@ func CommandUsage(helpHeader string, helpContent string) {
fmt.Println(columnize.Format(content, config))
}
// CopyFromImage copies a file from named image to destination
func CopyFromImage(appName string, image string, source string, destination string) error {
if err := VerifyAppName(appName); err != nil {
return err
}
if !VerifyImage(image) {
return fmt.Errorf("Invalid docker image for copying content")
}
workDir := ""
if !IsAbsPath(source) {
if IsImageHerokuishBased(image, appName) {
workDir = "/app"
} else {
workDir, _ = DockerInspect(image, "{{.Config.WorkingDir}}")
}
if workDir != "" {
source = fmt.Sprintf("%s/%s", workDir, source)
}
}
tmpFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", MustGetEnv("DOKKU_PID"), "CopyFromImage"))
if err != nil {
return fmt.Errorf("Cannot create temporary file: %v", err)
}
defer tmpFile.Close()
defer os.Remove(tmpFile.Name())
globalRunArgs := MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
createLabelArgs := []string{"--label", fmt.Sprintf("com.dokku.app-name=%s", appName), globalRunArgs}
containerID, err := DockerContainerCreate(image, createLabelArgs)
if err != nil {
return fmt.Errorf("Unable to create temporary container: %v", err)
}
// docker cp exits with status 1 when run as non-root user when it tries to chown the file
// after successfully copying the file. Thus, we suppress stderr.
// ref: https://github.com/dotcloud/docker/issues/3986
containerCopyCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"cp",
fmt.Sprintf("%s:%s", containerID, source),
tmpFile.Name(),
}, " "))
containerCopyCmd.ShowOutput = false
fileCopied := containerCopyCmd.Execute()
containerRemoveCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"rm",
"--force",
containerID,
}, " "))
containerRemoveCmd.ShowOutput = false
containerRemoveCmd.Execute()
if !fileCopied {
return fmt.Errorf("Unable to copy file %s from image", source)
}
fi, err := os.Stat(tmpFile.Name())
if err != nil {
return err
}
if fi.Size() == 0 {
return fmt.Errorf("Unable to copy file %s from image", source)
}
// workaround for CHECKS file when owner is root. seems to only happen when running inside docker
dos2unixCmd := NewShellCmd(strings.Join([]string{
"dos2unix",
"-l",
"-n",
tmpFile.Name(),
destination,
}, " "))
dos2unixCmd.ShowOutput = false
dos2unixCmd.Execute()
// add trailing newline for certain places where file parsing depends on it
b, err := sh.Command("tail", "-c1", destination).Output()
if string(b) != "" {
f, err := os.OpenFile(destination, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("\n"); err != nil {
return fmt.Errorf("Unable to append trailing newline to copied file: %v", err)
}
}
return nil
}
// DockerCleanup cleans up all exited/dead containers and removes all dangling images
func DockerCleanup(appName string, forceCleanup bool) error {
if !forceCleanup {
skipCleanup := false
if appName != "" {
b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...)
output := string(b[:])
if output == "true" {
skipCleanup = true
}
}
if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" {
LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup")
return nil
}
}
LogInfo1("Cleaning up...")
scheduler := GetAppScheduler(appName)
if appName == "--global" {
appName = ""
}
forceCleanupArg := "false"
if forceCleanup {
forceCleanupArg = "true"
}
if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil {
return fmt.Errorf("Failure while cleaning up app: %s", err)
}
// delete all non-running and dead containers
exitedContainerIDs, _ := listContainers("exited", appName)
deadContainerIDs, _ := listContainers("dead", appName)
containerIDs := append(exitedContainerIDs, deadContainerIDs...)
if len(containerIDs) > 0 {
removeContainers(containerIDs)
}
// delete dangling images
imageIDs, _ := listDanglingImages(appName)
if len(imageIDs) > 0 {
RemoveImages(imageIDs)
}
if appName != "" {
// delete unused images
pruneUnusedImages(appName)
}
return nil
}
// GetAppScheduler fetches the scheduler for a given application
func GetAppScheduler(appName string) string {
if appName == "--global" {
@@ -297,8 +74,13 @@ func GetAppScheduler(appName string) string {
return value
}
b, _ = PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...)
value = string(b[:])
return GetGlobalScheduler()
}
// GetGlobalScheduler fetchs the global scheduler
func GetGlobalScheduler() string {
b, _ := PlugnTriggerOutput("config-get-global", []string{"DOKKU_SCHEDULER"}...)
value := string(b[:])
if value != "" {
return value
}
@@ -429,61 +211,6 @@ func GetRunningImageTag(appName string) (string, error) {
return "", errors.New("No image tag found")
}
// ContainerIsRunning checks to see if a container is running
func ContainerIsRunning(containerID string) bool {
b, err := DockerInspect(containerID, "'{{.State.Running}}'")
if err != nil {
return false
}
return strings.TrimSpace(string(b[:])) == "true"
}
// DirectoryExists returns if a path exists and is a directory
func DirectoryExists(filePath string) bool {
fi, err := os.Stat(filePath)
if err != nil {
return false
}
return fi.IsDir()
}
// DockerContainerCreate creates a new container and returns the container ID
func DockerContainerCreate(image string, containerCreateArgs []string) (string, error) {
cmd := []string{
DockerBin(),
"container",
"create",
}
cmd = append(cmd, containerCreateArgs...)
cmd = append(cmd, image)
var stderr bytes.Buffer
containerCreateCmd := NewShellCmd(strings.Join(cmd, " "))
containerCreateCmd.ShowOutput = false
containerCreateCmd.Command.Stderr = &stderr
b, err := containerCreateCmd.Output()
if err != nil {
return "", errors.New(strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(string(b[:])), nil
}
// DockerInspect runs an inspect command with a given format against a container or image ID
func DockerInspect(containerOrImageID, format string) (output string, err error) {
b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output()
if err != nil {
return "", err
}
output = strings.TrimSpace(string(b[:]))
if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") {
output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'")
}
return
}
// DokkuApps returns a list of all local apps
func DokkuApps() (apps []string, err error) {
dokkuRoot := MustGetEnv("DOKKU_ROOT")
@@ -512,36 +239,6 @@ func DokkuApps() (apps []string, err error) {
return
}
// FileToSlice reads in all the lines from a file into a string slice
func FileToSlice(filePath string) (lines []string, err error) {
f, err := os.Open(filePath)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text == "" {
continue
}
lines = append(lines, text)
}
err = scanner.Err()
return
}
// FileExists returns if a path exists and is a file
func FileExists(filePath string) bool {
fi, err := os.Stat(filePath)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
// GetAppImageName returns image identifier for a given app, tag tuple. validate if tag is presented
func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
err := VerifyAppName(appName)
@@ -564,33 +261,11 @@ func GetAppImageName(appName, imageTag, imageRepo string) (imageName string) {
return
}
// IsAbsPath returns 0 if input path is absolute
func IsAbsPath(path string) bool {
return strings.HasPrefix(path, "/")
}
// IsDeployed returns true if given app has a running container
func IsDeployed(appName string) bool {
files, err := ioutil.ReadDir(AppRoot(appName))
if err != nil {
return false
}
for _, f := range files {
if f.Name() == "CONTAINER" || strings.HasPrefix(f.Name(), "CONTAINER.") {
return true
}
}
return false
}
// IsImageHerokuishBased returns true if app image is based on herokuish
func IsImageHerokuishBased(image string, appName string) bool {
output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}")
if err != nil {
return false
}
return output != ""
scheduler := GetAppScheduler(appName)
_, err := PlugnTriggerOutput("scheduler-is-deployed", []string{scheduler, appName}...)
return err == nil
}
// MustGetEnv returns env variable or fails if it's not set
@@ -611,29 +286,6 @@ func GetenvWithDefault(key string, defaultValue string) (val string) {
return
}
// ReadFirstLine gets the first line of a file that has contents and returns it
// if there are no contents, an empty string is returned
// will also return an empty string if the file does not exist
func ReadFirstLine(filename string) (text string) {
if !FileExists(filename) {
return
}
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if text = strings.TrimSpace(scanner.Text()); text == "" {
continue
}
return
}
return
}
// ReportSingleApp is an internal function that displays a report for an app
func ReportSingleApp(reportType string, appName string, infoFlag string, infoFlags map[string]string, trimPrefix bool, uppercaseFirstCharacter bool) error {
flags := []string{}
@@ -695,10 +347,10 @@ func ShiftString(a []string) (string, []string) {
// StripInlineComments removes bash-style comment from input line
func StripInlineComments(text string) string {
bytes := []byte(text)
b := []byte(text)
re := regexp.MustCompile("(?s)#.*")
bytes = re.ReplaceAll(bytes, nil)
return strings.TrimSpace(string(bytes))
b = re.ReplaceAll(b, nil)
return strings.TrimSpace(string(b))
}
// SuppressOutput suppresses the output of a function unless there is an error
@@ -725,6 +377,16 @@ func ToBool(s string) bool {
return s == "true"
}
// ToInt returns an int value for a given string
func ToInt(s string, defaultValue int) int {
i, err := strconv.Atoi(s)
if err != nil {
return defaultValue
}
return i
}
// UcFirst uppercases the first character in a string
func UcFirst(str string) string {
for i, v := range str {
@@ -766,157 +428,9 @@ func VerifyAppName(appName string) error {
return nil
}
// VerifyImage returns true if docker image exists in local repo
func VerifyImage(image string) bool {
imageCmd := NewShellCmd(strings.Join([]string{DockerBin(), "image", "inspect", image}, " "))
imageCmd.ShowOutput = false
return imageCmd.Execute()
}
// DockerBin returns a string which contains a path to the current docker binary
func DockerBin() string {
dockerBin := os.Getenv("DOCKER_BIN")
if dockerBin == "" {
dockerBin = "docker"
}
return dockerBin
}
// PlugnTrigger fire the given plugn trigger with the given args
func PlugnTrigger(triggerName string, args ...string) error {
return PlugnTriggerSetup(triggerName, args...).Run()
}
// PlugnTriggerOutput fire the given plugn trigger with the given args
func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) {
return PlugnTriggerSetup(triggerName, args...).Output()
}
// PlugnTriggerSetup sets up a plugn trigger call
func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session {
shellArgs := make([]interface{}, len(args)+2)
shellArgs[0] = "trigger"
shellArgs[1] = triggerName
for i, arg := range args {
shellArgs[i+2] = arg
}
return sh.Command("plugn", shellArgs...)
}
func times(str string, n int) (out string) {
for i := 0; i < n; i++ {
out += str
}
return
}
func listContainers(status string, appName string) ([]string, error) {
command := []string{
DockerBin(),
"container",
"list",
"--quiet",
"--all",
"--filter",
fmt.Sprintf("status=%v", status),
"--filter",
fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")),
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func listDanglingImages(appName string) ([]string, error) {
command := []string{
DockerBin(),
"image",
"list",
"--quiet",
"--filter",
"dangling=true",
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func removeContainers(containerIDs []string) {
command := []string{
DockerBin(),
"container",
"rm",
}
command = append(command, containerIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}
// RemoveImages removes images by ID
func RemoveImages(imageIDs []string) {
command := []string{
DockerBin(),
"image",
"rm",
}
command = append(command, imageIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}
func pruneUnusedImages(appName string) {
command := []string{
DockerBin(),
"image",
"prune",
"--all",
"--force",
"--filter",
fmt.Sprintf("label=com.dokku.app-name=%v", appName),
}
var stderr bytes.Buffer
pruneCmd := NewShellCmd(strings.Join(command, " "))
pruneCmd.ShowOutput = false
pruneCmd.Command.Stderr = &stderr
pruneCmd.Execute()
}

View File

@@ -10,10 +10,10 @@ import (
)
var (
testAppName = "test-app-1"
testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "")
testEnvFile = strings.Join([]string{testAppDir, "/ENV"}, "")
testEnvLine = "export testKey=TESTING"
testAppName = "test-app-1"
testAppDir = strings.Join([]string{"/home/dokku/", testAppName}, "")
testEnvFile = strings.Join([]string{testAppDir, "/ENV"}, "")
testEnvLine = "export testKey=TESTING"
testAppName2 = "01-test-app-1"
testAppDir2 = strings.Join([]string{"/home/dokku/", testAppName2}, "")
testEnvFile2 = strings.Join([]string{testAppDir2, "/ENV"}, "")
@@ -95,32 +95,6 @@ func TestCommonDokkuApps(t *testing.T) {
teardownTestApp()
}
func TestCommonFileToSlice(t *testing.T) {
RegisterTestingT(t)
Expect(setupTestApp()).To(Succeed())
lines, err := FileToSlice(testEnvFile)
Expect(err).NotTo(HaveOccurred())
Expect(lines).To(Equal([]string{testEnvLine}))
teardownTestApp()
}
func TestCommonFileExists(t *testing.T) {
RegisterTestingT(t)
Expect(setupTestApp()).To(Succeed())
Expect(FileExists(testEnvFile)).To(BeTrue())
teardownTestApp()
}
func TestCommonReadFirstLine(t *testing.T) {
RegisterTestingT(t)
line := ReadFirstLine(testEnvFile)
Expect(line).To(Equal(""))
Expect(setupTestApp()).To(Succeed())
line = ReadFirstLine(testEnvFile)
Expect(line).To(Equal(testEnvLine))
teardownTestApp()
}
func TestCommonStripInlineComments(t *testing.T) {
RegisterTestingT(t)
text := StripInlineComments(strings.Join([]string{testEnvLine, "# testing comment"}, " "))

350
plugins/common/docker.go Normal file
View File

@@ -0,0 +1,350 @@
package common
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/codeskyblue/go-sh"
)
// ContainerIsRunning checks to see if a container is running
func ContainerIsRunning(containerID string) bool {
b, err := DockerInspect(containerID, "'{{.State.Running}}'")
if err != nil {
return false
}
return strings.TrimSpace(string(b[:])) == "true"
}
// CopyFromImage copies a file from named image to destination
func CopyFromImage(appName string, image string, source string, destination string) error {
if err := VerifyAppName(appName); err != nil {
return err
}
if !VerifyImage(image) {
return fmt.Errorf("Invalid docker image for copying content")
}
workDir := ""
if !IsAbsPath(source) {
if IsImageHerokuishBased(image, appName) {
workDir = "/app"
} else {
workDir, _ = DockerInspect(image, "{{.Config.WorkingDir}}")
}
if workDir != "" {
source = fmt.Sprintf("%s/%s", workDir, source)
}
}
tmpFile, err := ioutil.TempFile(os.TempDir(), fmt.Sprintf("dokku-%s-%s", MustGetEnv("DOKKU_PID"), "CopyFromImage"))
if err != nil {
return fmt.Errorf("Cannot create temporary file: %v", err)
}
defer tmpFile.Close()
defer os.Remove(tmpFile.Name())
globalRunArgs := MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
createLabelArgs := []string{"--label", fmt.Sprintf("com.dokku.app-name=%s", appName), globalRunArgs}
containerID, err := DockerContainerCreate(image, createLabelArgs)
if err != nil {
return fmt.Errorf("Unable to create temporary container: %v", err)
}
// docker cp exits with status 1 when run as non-root user when it tries to chown the file
// after successfully copying the file. Thus, we suppress stderr.
// ref: https://github.com/dotcloud/docker/issues/3986
containerCopyCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"cp",
fmt.Sprintf("%s:%s", containerID, source),
tmpFile.Name(),
}, " "))
containerCopyCmd.ShowOutput = false
fileCopied := containerCopyCmd.Execute()
containerRemoveCmd := NewShellCmd(strings.Join([]string{
DockerBin(),
"container",
"rm",
"--force",
containerID,
}, " "))
containerRemoveCmd.ShowOutput = false
containerRemoveCmd.Execute()
if !fileCopied {
return fmt.Errorf("Unable to copy file %s from image", source)
}
fi, err := os.Stat(tmpFile.Name())
if err != nil {
return err
}
if fi.Size() == 0 {
return fmt.Errorf("Unable to copy file %s from image", source)
}
// workaround for CHECKS file when owner is root. seems to only happen when running inside docker
dos2unixCmd := NewShellCmd(strings.Join([]string{
"dos2unix",
"-l",
"-n",
tmpFile.Name(),
destination,
}, " "))
dos2unixCmd.ShowOutput = false
dos2unixCmd.Execute()
// add trailing newline for certain places where file parsing depends on it
b, err := sh.Command("tail", "-c1", destination).Output()
if string(b) != "" {
f, err := os.OpenFile(destination, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := f.WriteString("\n"); err != nil {
return fmt.Errorf("Unable to append trailing newline to copied file: %v", err)
}
}
return nil
}
// DockerBin returns a string which contains a path to the current docker binary
func DockerBin() string {
dockerBin := os.Getenv("DOCKER_BIN")
if dockerBin == "" {
dockerBin = "docker"
}
return dockerBin
}
// DockerCleanup cleans up all exited/dead containers and removes all dangling images
func DockerCleanup(appName string, forceCleanup bool) error {
if !forceCleanup {
skipCleanup := false
if appName != "" {
b, _ := PlugnTriggerOutput("config-get", []string{appName, "DOKKU_SKIP_CLEANUP"}...)
output := string(b[:])
if output == "true" {
skipCleanup = true
}
}
if skipCleanup || os.Getenv("DOKKU_SKIP_CLEANUP") == "true" {
LogInfo1("DOKKU_SKIP_CLEANUP set. Skipping dokku cleanup")
return nil
}
}
LogInfo1("Cleaning up...")
scheduler := GetAppScheduler(appName)
if appName == "--global" {
appName = ""
}
forceCleanupArg := "false"
if forceCleanup {
forceCleanupArg = "true"
}
if err := PlugnTrigger("scheduler-docker-cleanup", []string{scheduler, appName, forceCleanupArg}...); err != nil {
return fmt.Errorf("Failure while cleaning up app: %s", err)
}
// delete all non-running and dead containers
exitedContainerIDs, _ := listContainers("exited", appName)
deadContainerIDs, _ := listContainers("dead", appName)
containerIDs := append(exitedContainerIDs, deadContainerIDs...)
if len(containerIDs) > 0 {
removeContainers(containerIDs)
}
// delete dangling images
imageIDs, _ := listDanglingImages(appName)
if len(imageIDs) > 0 {
RemoveImages(imageIDs)
}
if appName != "" {
// delete unused images
pruneUnusedImages(appName)
}
return nil
}
// DockerContainerCreate creates a new container and returns the container ID
func DockerContainerCreate(image string, containerCreateArgs []string) (string, error) {
cmd := []string{
DockerBin(),
"container",
"create",
}
cmd = append(cmd, containerCreateArgs...)
cmd = append(cmd, image)
var stderr bytes.Buffer
containerCreateCmd := NewShellCmd(strings.Join(cmd, " "))
containerCreateCmd.ShowOutput = false
containerCreateCmd.Command.Stderr = &stderr
b, err := containerCreateCmd.Output()
if err != nil {
return "", errors.New(strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(string(b[:])), nil
}
// DockerInspect runs an inspect command with a given format against a container or image ID
func DockerInspect(containerOrImageID, format string) (output string, err error) {
b, err := sh.Command(DockerBin(), "inspect", "--format", format, containerOrImageID).Output()
if err != nil {
return "", err
}
output = strings.TrimSpace(string(b[:]))
if strings.HasPrefix(output, "'") && strings.HasSuffix(output, "'") {
output = strings.TrimSuffix(strings.TrimPrefix(output, "'"), "'")
}
return
}
// IsImageHerokuishBased returns true if app image is based on herokuish
func IsImageHerokuishBased(image string, appName string) bool {
output, err := DockerInspect(image, "{{range .Config.Env}}{{if eq . \"USER=herokuishuser\" }}{{println .}}{{end}}{{end}}")
if err != nil {
return false
}
return output != ""
}
// RemoveImages removes images by ID
func RemoveImages(imageIDs []string) {
command := []string{
DockerBin(),
"image",
"rm",
}
command = append(command, imageIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}
// VerifyImage returns true if docker image exists in local repo
func VerifyImage(image string) bool {
imageCmd := NewShellCmd(strings.Join([]string{DockerBin(), "image", "inspect", image}, " "))
imageCmd.ShowOutput = false
return imageCmd.Execute()
}
func listContainers(status string, appName string) ([]string, error) {
command := []string{
DockerBin(),
"container",
"list",
"--quiet",
"--all",
"--filter",
fmt.Sprintf("status=%v", status),
"--filter",
fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")),
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func listDanglingImages(appName string) ([]string, error) {
command := []string{
DockerBin(),
"image",
"list",
"--quiet",
"--filter",
"dangling=true",
}
if appName != "" {
command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...)
}
var stderr bytes.Buffer
listCmd := NewShellCmd(strings.Join(command, " "))
listCmd.ShowOutput = false
listCmd.Command.Stderr = &stderr
b, err := listCmd.Output()
if err != nil {
return []string{}, errors.New(strings.TrimSpace(stderr.String()))
}
output := strings.Split(strings.TrimSpace(string(b[:])), "\n")
return output, nil
}
func pruneUnusedImages(appName string) {
command := []string{
DockerBin(),
"image",
"prune",
"--all",
"--force",
"--filter",
fmt.Sprintf("label=com.dokku.app-name=%v", appName),
}
var stderr bytes.Buffer
pruneCmd := NewShellCmd(strings.Join(command, " "))
pruneCmd.ShowOutput = false
pruneCmd.Command.Stderr = &stderr
pruneCmd.Execute()
}
func removeContainers(containerIDs []string) {
command := []string{
DockerBin(),
"container",
"rm",
}
command = append(command, containerIDs...)
var stderr bytes.Buffer
rmCmd := NewShellCmd(strings.Join(command, " "))
rmCmd.ShowOutput = false
rmCmd.Command.Stderr = &stderr
rmCmd.Execute()
}

View File

@@ -634,7 +634,7 @@ dokku_receive() {
dokku_log_info1 "Building $APP from $IMAGE_SOURCE_TYPE..."
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_TYPE="$IMAGE_SOURCE_TYPE"
dokku_build "$APP" "$IMAGE_SOURCE_TYPE" "$TMP_WORK_DIR"
release_and_deploy "$APP"
plugn trigger release-and-deploy "$APP"
}
docker_cleanup() {

227
plugins/common/io.go Normal file
View File

@@ -0,0 +1,227 @@
package common
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"strconv"
"strings"
)
// CatFile cats the contents of a file (if it exists)
func CatFile(filename string) {
slice, err := FileToSlice(filename)
if err != nil {
LogDebug(fmt.Sprintf("Error cat'ing file %s: %s", filename, err.Error()))
return
}
for _, line := range slice {
LogDebug(fmt.Sprintf("line: '%s'", line))
}
}
// CopyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherise, attempt to create a hard link
// between the two files. If that fail, copy the file contents from src to dst.
// FROM: https://stackoverflow.com/a/21067803/1515875
func CopyFile(src, dst string) (err error) {
sfi, err := os.Stat(src)
if err != nil {
return
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories,
// symlinks, devices, etc.)
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return
}
}
if err = os.Link(src, dst); err == nil {
return
}
err = copyFileContents(src, dst)
return
}
// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
// FROM: https://stackoverflow.com/a/21067803/1515875
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
// DirectoryExists returns if a path exists and is a directory
func DirectoryExists(filename string) bool {
fi, err := os.Stat(filename)
if err != nil {
return false
}
return fi.IsDir()
}
// FileToSlice reads in all the lines from a file into a string slice
func FileToSlice(filename string) (lines []string, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
text := strings.TrimSpace(scanner.Text())
if text == "" {
continue
}
lines = append(lines, text)
}
err = scanner.Err()
return
}
// FileExists returns if a path exists and is a file
func FileExists(filename string) bool {
fi, err := os.Stat(filename)
if err != nil {
return false
}
return fi.Mode().IsRegular()
}
// IsAbsPath returns 0 if input path is absolute
func IsAbsPath(path string) bool {
return strings.HasPrefix(path, "/")
}
// ListFilesWithPrefix lists files within a given path that have a given prefix
func ListFilesWithPrefix(path string, prefix string) []string {
names, err := ioutil.ReadDir(path)
if err != nil {
return []string{}
}
files := []string{}
for _, f := range names {
if prefix != "" && !strings.HasPrefix(f.Name(), prefix) {
continue
}
if f.Mode().IsRegular() {
files = append(files, fmt.Sprintf("%s/%s", path, f.Name()))
}
}
return files
}
// ReadFirstLine gets the first line of a file that has contents and returns it
// if there are no contents, an empty string is returned
// will also return an empty string if the file does not exist
func ReadFirstLine(filename string) (text string) {
if !FileExists(filename) {
return
}
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if text = strings.TrimSpace(scanner.Text()); text == "" {
continue
}
return
}
return
}
// SetPermissions sets the proper owner and filemode for a given file
func SetPermissions(path string, fileMode os.FileMode) error {
if err := os.Chmod(path, fileMode); err != nil {
return err
}
systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku")
systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku")
group, err := user.LookupGroup(systemGroup)
if err != nil {
return err
}
user, err := user.Lookup(systemUser)
if err != nil {
return err
}
uid, err := strconv.Atoi(user.Uid)
if err != nil {
return err
}
gid, err := strconv.Atoi(group.Gid)
if err != nil {
return err
}
return os.Chown(path, uid, gid)
}
// WriteSliceToFile writes a slice of strings to a file
func WriteSliceToFile(filename string, lines []string) error {
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
w := bufio.NewWriter(file)
for _, line := range lines {
fmt.Fprintln(w, line)
}
if err = w.Flush(); err != nil {
return err
}
file.Chmod(0600)
SetPermissions(filename, 0600)
return nil
}

33
plugins/common/io_test.go Normal file
View File

@@ -0,0 +1,33 @@
package common
import (
"testing"
. "github.com/onsi/gomega"
)
func TestCommonFileToSlice(t *testing.T) {
RegisterTestingT(t)
Expect(setupTestApp()).To(Succeed())
lines, err := FileToSlice(testEnvFile)
Expect(err).NotTo(HaveOccurred())
Expect(lines).To(Equal([]string{testEnvLine}))
teardownTestApp()
}
func TestCommonFileExists(t *testing.T) {
RegisterTestingT(t)
Expect(setupTestApp()).To(Succeed())
Expect(FileExists(testEnvFile)).To(BeTrue())
teardownTestApp()
}
func TestCommonReadFirstLine(t *testing.T) {
RegisterTestingT(t)
line := ReadFirstLine(testEnvFile)
Expect(line).To(Equal(""))
Expect(setupTestApp()).To(Succeed())
line = ReadFirstLine(testEnvFile)
Expect(line).To(Equal(testEnvLine))
teardownTestApp()
}

135
plugins/common/parallel.go Normal file
View File

@@ -0,0 +1,135 @@
package common
import (
"fmt"
"runtime"
"sync"
)
// ParallelCommand is a type that declares functions
// the ps plugins can execute in parallel
type ParallelCommand func(string) error
// ParallelCommandRun contains all the arguments
// necessary for a parallel command run
type ParallelCommandRun struct {
Name string
}
// ParallelCommandResult is the result of a parallel
// command run
type ParallelCommandResult struct {
Name string
Error error
}
// RunCommandAgainstAllApps runs a given ParallelCommand against all apps
func RunCommandAgainstAllApps(command ParallelCommand, commandName string, parallelCount int) error {
runInSerial := false
if parallelCount < -1 {
return fmt.Errorf("Invalid value %d for --parallel flag", parallelCount)
}
if parallelCount == -1 {
cpuCount := runtime.NumCPU()
LogWarn(fmt.Sprintf("Setting --parallel=%d value to CPU count of %d", parallelCount, cpuCount))
parallelCount = cpuCount
}
if parallelCount == 0 || parallelCount == 1 {
LogWarn(fmt.Sprintf("Running %s in serial mode", commandName))
runInSerial = true
}
if runInSerial {
return runCommandAgainstAllAppsSerially(command, commandName)
}
return runCommandAgainstAllAppsInParallel(command, commandName, parallelCount)
}
func runCommandAgainstAllAppsInParallel(command ParallelCommand, commandName string, parallelCount int) error {
apps, err := DokkuApps()
if err != nil {
LogWarn(err.Error())
return nil
}
jobs := make(chan string, parallelCount)
results := make(chan ParallelCommandResult, len(apps))
go allocateJobs(apps, jobs)
done := make(chan error)
go aggregateResults(results, done)
createParallelWorkerPool(jobs, results, command, parallelCount)
err = <-done
return err
}
func runCommandAgainstAllAppsSerially(command ParallelCommand, commandName string) error {
apps, err := DokkuApps()
if err != nil {
LogWarn(err.Error())
return nil
}
errorCount := 0
for _, appName := range apps {
LogInfo1(fmt.Sprintf("Running %s against app %s", commandName, appName))
if err = command(appName); err != nil {
errorCount++
}
}
if errorCount > 0 {
return fmt.Errorf("%s command returned %d errors", commandName, errorCount)
}
return nil
}
func allocateJobs(input []string, jobs chan string) {
for _, job := range input {
jobs <- job
}
close(jobs)
}
func aggregateResults(results chan ParallelCommandResult, done chan error) {
var parallelError error
errorCount := 0
for result := range results {
if result.Error != nil {
LogWarn(fmt.Sprintf("Error running command against %s", result.Name))
errorCount++
}
}
if errorCount > 0 {
parallelError = fmt.Errorf("Encountered %d errors during parallel run", errorCount)
}
done <- parallelError
}
func createParallelWorker(jobs chan string, results chan ParallelCommandResult, command ParallelCommand, wg *sync.WaitGroup, workerID int) {
for job := range jobs {
LogInfo1(fmt.Sprintf("Running command against %s", job))
output := ParallelCommandResult{
Name: job,
Error: command(job),
}
results <- output
}
wg.Done()
}
func createParallelWorkerPool(jobs chan string, results chan ParallelCommandResult, command ParallelCommand, numberOfWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numberOfWorkers; i++ {
wg.Add(1)
go createParallelWorker(jobs, results, command, &wg, i)
}
wg.Wait()
close(results)
}

View File

@@ -6,10 +6,8 @@ import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
)
@@ -174,7 +172,7 @@ func PropertyListAdd(pluginName string, appName string, property string, value s
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
SetPermissions(propertyPath, 0600)
return nil
}
@@ -301,7 +299,7 @@ func PropertyListRemove(pluginName string, appName string, property string, valu
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
SetPermissions(propertyPath, 0600)
if !found {
return errors.New("Property not found, nothing was removed")
@@ -337,7 +335,7 @@ func PropertyListRemoveByPrefix(pluginName string, appName string, property stri
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
SetPermissions(propertyPath, 0600)
if !found {
return errors.New("Property not found, nothing was removed")
@@ -390,7 +388,7 @@ func PropertyListSet(pluginName string, appName string, property string, value s
}
file.Chmod(0600)
setPermissions(propertyPath, 0600)
SetPermissions(propertyPath, 0600)
return nil
}
@@ -429,7 +427,7 @@ func PropertyWrite(pluginName string, appName string, property string, value str
fmt.Fprintf(file, value)
file.Chmod(0600)
setPermissions(propertyPath, 0600)
SetPermissions(propertyPath, 0600)
return nil
}
@@ -439,25 +437,25 @@ func PropertySetup(pluginName string) error {
if err := os.MkdirAll(pluginConfigRoot, 0755); err != nil {
return err
}
if err := setPermissions(path.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config"), 0755); err != nil {
if err := SetPermissions(filepath.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config"), 0755); err != nil {
return err
}
return setPermissions(pluginConfigRoot, 0755)
return SetPermissions(pluginConfigRoot, 0755)
}
func getPropertyPath(pluginName string, appName string, property string) string {
pluginAppConfigRoot := getPluginAppPropertyPath(pluginName, appName)
return path.Join(pluginAppConfigRoot, property)
return filepath.Join(pluginAppConfigRoot, property)
}
// getPluginAppPropertyPath returns the plugin property path for a given plugin/app combination
func getPluginAppPropertyPath(pluginName string, appName string) string {
return path.Join(getPluginConfigPath(pluginName), appName)
return filepath.Join(getPluginConfigPath(pluginName), appName)
}
// getPluginConfigPath returns the plugin property path for a given plugin
func getPluginConfigPath(pluginName string) string {
return path.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName)
return filepath.Join(MustGetEnv("DOKKU_LIB_ROOT"), "config", pluginName)
}
// makePluginAppPropertyPath ensures that a property path exists
@@ -466,35 +464,5 @@ func makePluginAppPropertyPath(pluginName string, appName string) error {
if err := os.MkdirAll(pluginAppConfigRoot, 0755); err != nil {
return err
}
return setPermissions(pluginAppConfigRoot, 0755)
}
// setPermissions sets the proper owner and filemode for a given file
func setPermissions(path string, fileMode os.FileMode) error {
if err := os.Chmod(path, fileMode); err != nil {
return err
}
systemGroup := GetenvWithDefault("DOKKU_SYSTEM_GROUP", "dokku")
systemUser := GetenvWithDefault("DOKKU_SYSTEM_USER", "dokku")
group, err := user.LookupGroup(systemGroup)
if err != nil {
return err
}
user, err := user.Lookup(systemUser)
if err != nil {
return err
}
uid, err := strconv.Atoi(user.Uid)
if err != nil {
return err
}
gid, err := strconv.Atoi(group.Gid)
if err != nil {
return err
}
return os.Chown(path, uid, gid)
return SetPermissions(pluginAppConfigRoot, 0755)
}

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
release_and_deploy "$@"

View File

@@ -0,0 +1,94 @@
package common
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/codeskyblue/go-sh"
)
// ShellCmd represents a shell command to be run for dokku
type ShellCmd struct {
Env map[string]string
Command *exec.Cmd
CommandString string
Args []string
ShowOutput bool
}
// NewShellCmd returns a new ShellCmd struct
func NewShellCmd(command string) *ShellCmd {
items := strings.Split(command, " ")
cmd := items[0]
args := items[1:]
return NewShellCmdWithArgs(cmd, args...)
}
// NewShellCmdWithArgs returns a new ShellCmd struct
func NewShellCmdWithArgs(cmd string, args ...string) *ShellCmd {
commandString := strings.Join(append([]string{cmd}, args...), " ")
return &ShellCmd{
Command: exec.Command(cmd, args...),
CommandString: commandString,
Args: args,
ShowOutput: true,
}
}
func (sc *ShellCmd) setup() {
env := os.Environ()
for k, v := range sc.Env {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
sc.Command.Env = env
if sc.ShowOutput {
sc.Command.Stdout = os.Stdout
sc.Command.Stderr = os.Stderr
}
}
// Execute is a lightweight wrapper around exec.Command
func (sc *ShellCmd) Execute() bool {
sc.setup()
if err := sc.Command.Run(); err != nil {
return false
}
return true
}
// Output is a lightweight wrapper around exec.Command.Output()
func (sc *ShellCmd) Output() ([]byte, error) {
sc.setup()
return sc.Command.Output()
}
// CombinedOutput is a lightweight wrapper around exec.Command.CombinedOutput()
func (sc *ShellCmd) CombinedOutput() ([]byte, error) {
sc.setup()
return sc.Command.CombinedOutput()
}
// PlugnTrigger fire the given plugn trigger with the given args
func PlugnTrigger(triggerName string, args ...string) error {
return PlugnTriggerSetup(triggerName, args...).Run()
}
// PlugnTriggerOutput fire the given plugn trigger with the given args
func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) {
return PlugnTriggerSetup(triggerName, args...).Output()
}
// PlugnTriggerSetup sets up a plugn trigger call
func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session {
shellArgs := make([]interface{}, len(args)+2)
shellArgs[0] = "trigger"
shellArgs[1] = triggerName
for i, arg := range args {
shellArgs[i+2] = arg
}
return sh.Command("plugn", shellArgs...)
}

View File

@@ -0,0 +1,87 @@
package dockeroptions
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
)
// SetDockerOptionForPhases sets an option to specified phases
func SetDockerOptionForPhases(appName string, phases []string, name string, value string) error {
for _, phase := range phases {
if err := touchPhaseFile(appName, phase); err != nil {
return err
}
options, err := GetDockerOptionsForPhase(appName, phase)
if err != nil {
return err
}
newOptions := []string{}
for _, option := range options {
if strings.HasPrefix(option, fmt.Sprintf("--%s=", name)) {
continue
}
newOptions = append(newOptions, option)
}
newOptions = append(newOptions, fmt.Sprintf("--%s=%s", name, value))
sort.Strings(newOptions)
if err = writeDockerOptionsForPhase(appName, phase, newOptions); err != nil {
return err
}
}
return nil
}
// AddDockerOptionToPhases adds docker option to specified phases
func AddDockerOptionToPhases(appName string, phases []string, option string) error {
for _, phase := range phases {
if err := touchPhaseFile(appName, phase); err != nil {
return err
}
options, err := GetDockerOptionsForPhase(appName, phase)
if err != nil {
return err
}
options = append(options, option)
sort.Strings(options)
if err = writeDockerOptionsForPhase(appName, phase, options); err != nil {
return err
}
}
return nil
}
// GetDockerOptionsForPhase returns the docker options for the specified phase
func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) {
options := []string{}
if err := touchPhaseFile(appName, phase); err != nil {
return options, err
}
phaseFilePath := getPhaseFilePath(appName, phase)
file, err := os.Open(phaseFilePath)
if err != nil {
return options, fmt.Errorf("Unable to open docker options phase file %s.%s: %s", appName, appName, err.Error())
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
options = append(options, strings.TrimSpace(scanner.Text()))
}
if err = scanner.Err(); err != nil {
return options, fmt.Errorf("Unable to read docker options phase file %s.%s: %s", appName, appName, err.Error())
}
return options, nil
}

View File

@@ -0,0 +1,36 @@
package dockeroptions
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/dokku/dokku/plugins/common"
)
func getPhaseFilePath(appName string, phase string) string {
return filepath.Join(common.AppRoot(appName), "DOCKER_OPTIONS_"+strings.ToUpper(phase))
}
func touchPhaseFile(appName string, phase string) error {
phaseFilePath := getPhaseFilePath(appName, phase)
_, err := os.Stat(phaseFilePath)
if !os.IsNotExist(err) {
return nil
}
file, err := os.Create(phaseFilePath)
if err != nil {
return fmt.Errorf("Unable to create docker options phase file %s.%s: %s", appName, phase, err.Error())
}
defer file.Close()
return nil
}
func writeDockerOptionsForPhase(appName string, phase string, options []string) error {
phaseFilePath := getPhaseFilePath(appName, phase)
return common.WriteSliceToFile(phaseFilePath, options)
}

View File

@@ -0,0 +1,12 @@
module github.com/dokku/dokku/plugins/docker-options
go 1.14
require (
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 // indirect
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 // indirect
)
replace github.com/dokku/dokku/plugins/common => ../common

View File

@@ -0,0 +1,8 @@
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=

View File

@@ -4,7 +4,6 @@ set -eo pipefail
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/certs/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/internal-functions"
fn-nginx-log-root() {

View File

@@ -10,7 +10,7 @@ import (
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
columnize "github.com/ryanuber/columnize"
"github.com/ryanuber/columnize"
)
// PortMap is a struct that contains a scheme:host-port:container-port mapping

11
plugins/ps/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
/commands
/subcommands/*
/triggers/*
/triggers
/proxy-*
/install
/post-*
/report
/pre-*
/procfile-*
/app-restart

6
plugins/ps/Makefile Normal file
View File

@@ -0,0 +1,6 @@
SUBCOMMANDS = subcommands/inspect subcommands/rebuild subcommands/report subcommands/restart subcommands/restart-policy subcommands/restore subcommands/retire subcommands/scale subcommands/set subcommands/start subcommands/stop
TRIGGERS = triggers/app-restart triggers/install triggers/post-app-clone triggers/post-app-rename triggers/post-create triggers/post-delete triggers/post-stop triggers/pre-deploy triggers/procfile-extract triggers/procfile-get-command triggers/procfile-remove triggers/report
BUILD = commands subcommands triggers
PLUGIN_NAME = ps
include ../../common.mk

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-app-restart() {
declare desc="ps app-restart plugin trigger"
declare trigger="app-restart"
declare APP="$1"
ps_restart "$APP"
}
trigger-ps-app-restart "$@"

View File

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

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-core-post-deploy() {
declare desc="ps core-post-deploy plugin trigger"
declare trigger="core-post-deploy"
declare APP="$1"
remove_procfile "$APP"
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_RESTORE=1
}
trigger-ps-core-post-deploy "$@"

View File

@@ -2,138 +2,6 @@
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
get_cmd_from_procfile() {
declare desc="parse cmd from app Procfile"
declare APP="$1" PROC_TYPE="$2" PORT="${3:-5000}"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
local COMMAND
verify_app_name "$APP"
fn-procfile-get-command "$DOKKU_PROCFILE" "$PROC_TYPE" "$PORT"
}
print_dokku_scale_file() {
declare desc="prints contents of DOKKU_SCALE file"
local APP="$1"
local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
dokku_log_verbose_quiet "DOKKU_SCALE declares scale -> $(xargs <"$DOKKU_SCALE_FILE")"
}
extract_procfile() {
declare desc="extracts procfile from app image"
local APP="$1"
local IMAGE_TAG="$2"
local IMAGE="$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
verify_app_name "$APP"
remove_procfile "$APP"
fn-procfile-extract "$IMAGE" "$DOKKU_PROCFILE"
}
fn-procfile-extract() {
declare desc="extract procfile from a specified docker image"
declare IMAGE="$1" PROCFILE_PATH="$2"
copy_from_image "$IMAGE" "Procfile" "$PROCFILE_PATH" 2>/dev/null || true
if [[ -f "$PROCFILE_PATH" ]]; then
dokku_log_info1_quiet "App Procfile file found"
# shellcheck disable=SC2069
PROCFILE_ERRORS="$(procfile-util check --procfile "$PROCFILE_PATH" 2>&1 >/dev/null || true)"
if [[ -n "$PROCFILE_ERRORS" ]]; then
dokku_log_fail "${PROCFILE_ERRORS}"
fi
else
dokku_log_info1_quiet "No Procfile found in app image"
fi
}
fn-procfile-get-command() {
declare desc="Fetch a command from a Procfile"
declare PROCFILE_PATH="$1" PROCESS_TYPE="$2" PORT="$3"
if [[ ! -f "$PROCFILE_PATH" ]]; then
return
fi
procfile-util show --procfile "$PROCFILE_PATH" --process-type "$PROCESS_TYPE" --default-port "$PORT" 2>/dev/null
}
remove_procfile() {
declare desc="removes DOKKU_PROCFILE"
local APP="$1"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
if [[ -f "$DOKKU_PROCFILE" ]]; then
rm -f "$DOKKU_PROCFILE"
fi
}
generate_scale_file() {
declare desc="generates DOKKU_SCALE file"
local APP="$1"
local IMAGE_TAG="$2"
local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG")
local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
verify_app_name "$APP"
if copy_from_image "$IMAGE" "DOKKU_SCALE" "$DOKKU_SCALE_FILE" 2>/dev/null; then
cp "$DOKKU_SCALE_FILE" "${DOKKU_SCALE_FILE}.extracted"
else
rm -f "${DOKKU_SCALE_FILE}.extracted"
fi
if [[ ! -f $DOKKU_SCALE_FILE ]]; then
dokku_log_info1_quiet "DOKKU_SCALE file not found in app image. Generating one based on Procfile..."
local GENERATE_SCALE_FILE_TMP_WORK_DIR=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
trap "rm -rf '$GENERATE_SCALE_FILE_TMP_WORK_DIR' >/dev/null" RETURN
if [[ -f $DOKKU_PROCFILE ]]; then
while read -r line || [[ -n "$line" ]]; do
if [[ -z "$line" ]] || [[ "$line" == "#"* ]]; then
continue
fi
local NAME=${line%%:*}
local NUM_PROCS=0
[[ "$NAME" == "web" ]] && NUM_PROCS=1
[[ -n "$NAME" ]] && echo "$NAME=$NUM_PROCS" >>"$DOKKU_SCALE_FILE"
done < <(grep -v -E "^#" "$DOKKU_PROCFILE" | grep ':' | awk -F ':' '{ print $1 }' | sort | uniq)
else
echo "web=1" >>"$DOKKU_SCALE_FILE"
fi
fi
print_dokku_scale_file "$APP"
}
set_scale() {
declare desc="sets app proc type scaling"
declare APP="$1"
local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
shift 1
extract_procfile "$APP" "$IMAGE_TAG" >/dev/null
trap "remove_procfile $APP" RETURN INT TERM EXIT
local SCALE_SETTINGS=("$@")
for procscale in "${SCALE_SETTINGS[@]}"; do
local PROC_NAME=${procscale%%=*}
local PROC_COUNT=${procscale#*=}
if [[ -f "$DOKKU_PROCFILE" ]] && [[ "$PROC_COUNT" != "0" ]] && ! procfile-util exists --procfile "$DOKKU_PROCFILE" --process-type "$PROC_NAME" >/dev/null 2>&1; then
dokku_log_fail "${PROC_NAME} is not a valid process name"
fi
is_number "$PROC_COUNT" || dokku_log_fail "ps:scale $PROC_COUNT is not a number"
dokku_log_info1_quiet "Scaling $APP:$PROC_NAME to $PROC_COUNT"
if (grep -q -E "^${PROC_NAME}=" "$DOKKU_SCALE_FILE" >/dev/null 2>&1); then
sed --in-place "s:^${PROC_NAME}=.*:$PROC_NAME=$PROC_COUNT:g" "$DOKKU_SCALE_FILE"
else
echo "$PROC_NAME=$PROC_COUNT" >>"$DOKKU_SCALE_FILE"
fi
done
}
ps_restore() {
declare desc="restores app"
@@ -142,118 +10,6 @@ ps_restore() {
ps_start "$APP"
}
ps_start() {
declare desc="starts app"
local APP="$1"
verify_app_name "$APP"
local IMAGE_TAG=$(get_running_image_tag "$APP")
local RUNNING
if (is_deployed "$APP"); then
plugn trigger pre-start "$APP"
RUNNING="$(fn-ps-is-app-running "$APP")"
if [[ "$RUNNING" == "mixed" ]]; then
dokku_log_warn "App is running in mixed mode, releasing"
fi
if [[ "$RUNNING" != "true" ]]; then
release_and_deploy "$APP" "$IMAGE_TAG"
else
dokku_log_warn "App $APP already running"
fi
plugn trigger proxy-build-config "$APP"
else
dokku_log_warn "App $APP has not been deployed"
fi
}
ps_stop() {
declare desc="stops app"
local APP="$1"
verify_app_name "$APP"
! (is_deployed "$APP") && dokku_log_warn "App $APP has not been deployed" && exit 0
dokku_log_quiet "Stopping $APP ..."
local DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
plugn trigger scheduler-stop "$DOKKU_SCHEDULER" "$APP"
plugn trigger post-stop "$APP"
}
ps_rebuild() {
declare desc="rebuilds app from base image"
local APP="$1"
verify_app_name "$APP"
plugn trigger receive-app "$APP"
}
ps_restart() {
declare desc="restarts app"
local APP="$1"
verify_app_name "$APP"
local IMAGE_TAG=$(get_running_image_tag "$APP")
if (is_deployed "$APP"); then
release_and_deploy "$APP" "$IMAGE_TAG"
else
dokku_log_warn "App $APP has not been deployed"
fi
}
ps_scale() {
declare desc="sets app scaling config according arguments; otherwise according to DOKKU_SCALE file"
local APP="$1"
verify_app_name "$APP"
local IMAGE_TAG=$(get_running_image_tag "$APP")
local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
local DOKKU_PROCFILE="$DOKKU_ROOT/$APP/DOKKU_PROCFILE"
shift 1
# shellcheck disable=SC2015
[[ ! -e $DOKKU_PROCFILE ]] && extract_procfile "$APP" "$IMAGE_TAG" >/dev/null 2>&1 || true
if [[ ! -e $DOKKU_SCALE_FILE ]]; then
generate_scale_file "$APP" "$IMAGE_TAG" >/dev/null 2>&1
elif [[ -e $DOKKU_PROCFILE ]]; then
for PROC_NAME in $(procfile-util list --procfile "$DOKKU_PROCFILE"); do
if ! grep -qE "^${PROC_NAME}=" "$DOKKU_SCALE_FILE"; then
echo "$PROC_NAME=0" >>"$DOKKU_SCALE_FILE"
fi
done
fi
if [[ -z "$@" ]]; then
dokku_log_info1_quiet "Scaling for $APP"
dokku_col_log_msg_quiet "proctype" "qty"
dokku_col_log_msg_quiet "--------" "---"
while read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
local PROC_NAME=${line%%=*}
local PROC_COUNT=${line#*=}
dokku_col_log_msg "${PROC_NAME}:" "$PROC_COUNT"
done <"$DOKKU_SCALE_FILE"
else
if [[ "$(fn-ps-can-scale "$APP")" == "true" ]]; then
set_scale "$APP" "$@"
release_and_deploy "$APP" "$IMAGE_TAG"
else
dokku_log_fail "App contains DOKKU_SCALE file and cannot be manually scaled"
fi
fi
}
get_raw_restart_policies() {
declare desc="strips docker options and prints raw restart policies"
local -r phase_file_path=$1
[[ -r "$phase_file_path" ]] && sed -e '/^--restart=/!d' <"$phase_file_path"
}
get_restart_policies() {
declare desc="strips docker options and prints restart policies"
local -r phase_file_path=$1
get_raw_restart_policies "$phase_file_path" | sed -e 's/^--restart=//g'
}
fn-ps-app-status() {
declare APP="$1"
@@ -266,17 +22,3 @@ fn-ps-is-app-running() {
APP_STATUS=$(fn-ps-app-status "$APP")
echo "$APP_STATUS" | cut -d ' ' -f 2
}
fn-ps-can-scale() {
declare APP="$1"
local IMAGE_TAG=$(get_running_image_tag "$APP")
local DOKKU_SCALE_FILE="$DOKKU_ROOT/$APP/DOKKU_SCALE"
local CAN_SCALE=false
[[ ! -e $DOKKU_SCALE_FILE ]] && generate_scale_file "$APP" "$IMAGE_TAG" >/dev/null 2>&1
if [[ ! -f "${DOKKU_SCALE_FILE}.extracted" ]]; then
CAN_SCALE=true
fi
echo "$CAN_SCALE"
}

445
plugins/ps/functions.go Normal file
View File

@@ -0,0 +1,445 @@
package ps
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
"github.com/ryanuber/columnize"
)
func canScaleApp(appName string) bool {
return !common.FileExists(getScalefileExtractedPath(appName))
}
func extractProcfile(appName, image string, procfilePath string) error {
if err := removeProcfile(appName); err != nil {
return err
}
destination := getProcfilePath(appName)
common.CopyFromImage(appName, image, "Procfile", destination)
if !common.FileExists(destination) {
common.LogInfo1Quiet("No Procfile found in app image")
return nil
}
common.LogInfo1Quiet("App Procfile file found")
checkCmd := common.NewShellCmd(strings.Join([]string{
"procfile-util",
"check",
"--procfile",
destination,
}, " "))
var stderr bytes.Buffer
checkCmd.ShowOutput = false
checkCmd.Command.Stderr = &stderr
_, err := checkCmd.Output()
if err != nil {
return fmt.Errorf(strings.TrimSpace(stderr.String()))
}
return nil
}
func extractOrGenerateScalefile(appName string, image string) error {
destination := getScalefilePath(appName)
extracted := getScalefileExtractedPath(appName)
previouslyExtracted := common.FileExists(extracted)
if err := common.CopyFromImage(appName, image, "DOKKU_SCALE", extracted); err != nil {
if previouslyExtracted {
os.Remove(destination)
}
os.Remove(extracted)
} else if err := common.CopyFile(extracted, destination); err != nil {
return err
}
if common.FileExists(destination) {
common.LogInfo1Quiet("DOKKU_SCALE file exists")
return updateScalefile(appName, make(map[string]int))
}
common.LogInfo1Quiet("DOKKU_SCALE file not found in app image. Generating one based on Procfile...")
if err := updateScalefile(appName, make(map[string]int)); err != nil {
common.LogDebug(fmt.Sprintf("Error generating scale file: %s", err.Error()))
return err
}
return nil
}
func generateScalefile(appName string) error {
destination := getScalefilePath(appName)
procfilePath := getProcfilePath(appName)
content := []string{"web=1"}
if !common.FileExists(procfilePath) {
return common.WriteSliceToFile(destination, content)
}
lines, err := common.FileToSlice(procfilePath)
if err != nil {
return common.WriteSliceToFile(destination, content)
}
content = []string{}
for _, line := range lines {
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Split(line, ":")
count := 0
if parts[0] == "web" {
count = 1
}
content = append(content, fmt.Sprintf("%s=%d", parts[0], count))
}
return common.WriteSliceToFile(destination, content)
}
func getProcessStatus(appName string) map[string]string {
statuses := make(map[string]string)
containerFiles := common.ListFilesWithPrefix(common.AppRoot(appName), "CONTAINER.")
for _, filename := range containerFiles {
containerID := common.ReadFirstLine(filename)
containerStatus, _ := common.DockerInspect(containerID, "{{ .State.Status }}")
process := strings.TrimPrefix(filename, fmt.Sprintf("%s/CONTAINER.", common.AppRoot(appName)))
if containerStatus == "" {
containerStatus = "missing"
}
statuses[process] = fmt.Sprintf("%s (CID: %s)", containerStatus, containerID[0:11])
}
return statuses
}
func getProcfileCommand(procfilePath string, processType string, port int) (string, error) {
if !common.FileExists(procfilePath) {
return "", errors.New("No procfile found")
}
shellCmd := common.NewShellCmd(strings.Join([]string{
"procfile-util",
"show",
"--procfile",
procfilePath,
"--process-type",
processType,
"--default-port",
strconv.Itoa(port),
}, " "))
var stderr bytes.Buffer
shellCmd.ShowOutput = false
shellCmd.Command.Stderr = &stderr
b, err := shellCmd.Output()
if err != nil {
return "", fmt.Errorf(strings.TrimSpace(stderr.String()))
}
return strings.TrimSpace(string(b[:])), nil
}
func getProcfilePath(appName string) string {
directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName)
return filepath.Join(directory, "Procfile")
}
func getRestartPolicy(appName string) (string, error) {
options, err := dockeroptions.GetDockerOptionsForPhase(appName, "deploy")
if err != nil {
return "", err
}
for _, option := range options {
if strings.HasPrefix(option, "--restart=") {
return strings.TrimPrefix(option, "--restart="), nil
}
}
return "", nil
}
func getProcessCount(appName string) (int, error) {
scheduler := common.GetAppScheduler(appName)
b, _ := common.PlugnTriggerOutput("scheduler-app-status", []string{scheduler, appName}...)
count := strings.Split(strings.TrimSpace(string(b[:])), " ")[0]
return strconv.Atoi(count)
}
func getRunningState(appName string) string {
scheduler := common.GetAppScheduler(appName)
b, _ := common.PlugnTriggerOutput("scheduler-app-status", []string{scheduler, appName}...)
return strings.Split(strings.TrimSpace(string(b[:])), " ")[1]
}
func getScalefilePath(appName string) string {
return filepath.Join(common.AppRoot(appName), "DOKKU_SCALE")
}
func getScalefileExtractedPath(appName string) string {
return filepath.Join(common.AppRoot(appName), "DOKKU_SCALE.extracted")
}
func hasScaleFile(appName string) bool {
scalefilePath := getScalefilePath(appName)
return common.FileExists(scalefilePath)
}
func hasProcfile(appName string) bool {
procfilePath := getProcfilePath(appName)
return common.FileExists(procfilePath)
}
func isValidRestartPolicy(policy string) bool {
if policy == "" {
return false
}
validRestartPolicies := map[string]bool{
"no": true,
"always": true,
"unless-stopped": true,
"on-failure": true,
}
if _, ok := validRestartPolicies[policy]; ok {
return true
}
return strings.HasPrefix(policy, "on-failure:")
}
func parseProcessTuples(processTuples []string) (map[string]int, error) {
scale := make(map[string]int)
for _, processTuple := range processTuples {
s := strings.Split(processTuple, "=")
if len(s) == 1 {
return scale, fmt.Errorf("Missing count for process type %s", processTuple)
}
processType := s[0]
count, err := strconv.Atoi(s[1])
if err != nil {
return scale, fmt.Errorf("Invalid count for process type %s", s[0])
}
scale[processType] = count
}
return scale, nil
}
func processesInProcfile(procfilePath string) (map[string]bool, error) {
processes := map[string]bool{}
shellCmd := common.NewShellCmd(strings.Join([]string{
"procfile-util",
"list",
"--procfile",
procfilePath,
}, " "))
var stderr bytes.Buffer
shellCmd.ShowOutput = false
shellCmd.Command.Stderr = &stderr
b, err := shellCmd.Output()
if err != nil {
return processes, fmt.Errorf(strings.TrimSpace(stderr.String()))
}
for _, s := range strings.Split(strings.TrimSpace(string(b[:])), "\n") {
processes[s] = true
}
return processes, nil
}
func readScaleFile(appName string) (map[string]int, error) {
scale := make(map[string]int)
if !hasScaleFile(appName) {
return scale, errors.New("No DOKKU_SCALE file found")
}
scalefilePath := getScalefilePath(appName)
lines, err := common.FileToSlice(scalefilePath)
if err != nil {
return scale, err
}
for i, line := range lines {
s := strings.Split(line, "=")
if len(s) != 2 {
common.LogWarn(fmt.Sprintf("Invalid scale entry on line %d, skipping", i))
continue
}
processType := s[0]
count, err := strconv.Atoi(s[1])
if err != nil {
common.LogWarn(fmt.Sprintf("Invalid count on line %d, skipping", i))
continue
}
scale[processType] = count
}
if len(scale) == 0 {
common.LogWarn("No valid entries found in scale file, defaulting to web=1")
scale["web"] = 1
}
return scale, nil
}
func removeProcfile(appName string) error {
procfile := getProcfilePath(appName)
if !common.FileExists(procfile) {
return nil
}
return os.Remove(procfile)
}
func restorePrep() error {
apps, err := common.DokkuApps()
if err != nil {
common.LogWarn(err.Error())
return nil
}
for _, appName := range apps {
if err := common.PlugnTrigger("proxy-clear-config", []string{appName}...); err != nil {
return fmt.Errorf("Error clearing proxy config: %s", err)
}
}
return nil
}
func scaleReport(appName string) error {
scalefilePath := getScalefilePath(appName)
lines, err := common.FileToSlice(scalefilePath)
if err != nil {
return err
}
common.LogInfo1Quiet(fmt.Sprintf("Scaling for %s", appName))
config := columnize.DefaultConfig()
config.Delim = "="
config.Glue = ": "
config.Prefix = " "
config.Empty = ""
content := []string{}
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
content = append(content, "proctype=qty", "--------=---")
}
sort.Strings(lines)
for _, line := range lines {
content = append(content, line)
}
for _, line := range content {
s := strings.Split(line, "=")
common.Log(fmt.Sprintf("%s %s", common.RightPad(fmt.Sprintf("%s:", s[0]), 5, " "), s[1]))
}
return nil
}
func scaleSet(appName string, skipDeploy bool, processTuples []string) error {
if !canScaleApp(appName) {
return fmt.Errorf("App %s contains DOKKU_SCALE file and cannot be manually scaled", appName)
}
scale, err := parseProcessTuples(processTuples)
if err != nil {
return err
}
common.LogInfo1(fmt.Sprintf("Scaling %s processes: %s", appName, strings.Join(processTuples, " ")))
if err := updateScalefile(appName, scale); err != nil {
return err
}
if !common.IsDeployed(appName) {
return nil
}
imageTag, err := common.GetRunningImageTag(appName)
if err != nil {
return err
}
if skipDeploy {
return nil
}
return common.PlugnTrigger("release-and-deploy", []string{appName, imageTag}...)
}
func updateScalefile(appName string, scaleUpdates map[string]int) error {
if !hasScaleFile(appName) {
if err := generateScalefile(appName); err != nil {
return err
}
}
scale, err := readScaleFile(appName)
if err != nil {
return err
}
procfilePath := getProcfilePath(appName)
procfileExists := hasProcfile(appName)
validProcessTypes := make(map[string]bool)
if procfileExists {
validProcessTypes, err = processesInProcfile(procfilePath)
if err != nil {
return err
}
}
for processType, count := range scaleUpdates {
if procfileExists && !validProcessTypes[processType] && count != 0 {
return fmt.Errorf("%s is not a valid process name to scale up", processType)
}
scale[processType] = count
}
for processType := range validProcessTypes {
count, ok := scale[processType]
if !ok {
count = 0
}
scale[processType] = count
}
content := []string{}
for processType, count := range scale {
content = append(content, fmt.Sprintf("%s=%d", processType, count))
}
scalefilePath := getScalefilePath(appName)
if err := common.WriteSliceToFile(scalefilePath, content); err != nil {
return err
}
return nil
}

18
plugins/ps/go.mod Normal file
View File

@@ -0,0 +1,18 @@
module github.com/dokku/dokku/plugins/ps
go 1.14
require (
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27
github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/config v0.0.0-00010101000000-000000000000
github.com/dokku/dokku/plugins/docker-options v0.0.0-00010101000000-000000000000
github.com/gofrs/flock v0.8.0
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3
)
replace github.com/dokku/dokku/plugins/common => ../common
replace github.com/dokku/dokku/plugins/config => ../config
replace github.com/dokku/dokku/plugins/docker-options => ../docker-options

12
plugins/ps/go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27 h1:HHUr4P/aKh4quafGxDT9LDasjGdlGkzLbfmmrlng3kA=
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE=
github.com/dokku/dokku v0.21.4 h1:WG0hyPzNqKqoQXuI7H/N+cWX9B7mAxTW6Yop40BjCQk=
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/joho/godotenv v1.2.0 h1:vGTvz69FzUFp+X4/bAkb0j5BoLC+9bpqTWY8mjhA9pc=
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3 h1:utdYOikI1XjNtTFGCwSM6OmFJblU4ld4gACoJsbadJg=
github.com/ryanuber/columnize v1.1.2-0.20190319233515-9e6335e58db3/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-ps-help() {
declare desc="help command"
declare CMD="$1"
local plugin_name="ps"
local plugin_description="Manage container processes"
if [[ "$CMD" == "$plugin_name:help" ]]; then
echo -e "Usage: dokku $plugin_name[:COMMAND]"
echo ''
echo "$plugin_description"
echo ''
echo 'Additional commands:'
fn-help-content | sort | column -c2 -t -s,
elif [[ $(ps -o command= $PPID) == *"--all"* ]]; then
fn-help-content
else
cat <<help_desc
$plugin_name, $plugin_description
help_desc
fi
}
fn-help-content() {
declare desc="return help content"
cat <<help_content
ps:inspect <app>, Displays a sanitized version of docker inspect for an app
ps:scale <app> <proc>=<count> [<proc>=<count>...], Get/Set how many instances of a given process to run
ps:start <app>, Start app container(s)
ps:startall, Starts all apps via command line
ps:stop <app>, Stop app container(s)
ps:stopall, Stop all app container(s)
ps:rebuild <app>, Rebuild an app from source
ps:rebuildall, Rebuild all apps from source
ps:report [<app>] [<flag>], Displays a process report for one or more apps
ps:restart <app>, Restart app container(s)
ps:restartall, Restart all deployed app containers
ps:restore, Start previously running apps e.g. after reboot
ps:restart-policy <app>, Shows the restart-policy for an app
ps:set-restart-policy <app> <policy>, Sets app restart-policy
help_content
}

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-install() {
declare desc="set the default restart policy for all applications if there is not one already set"
declare trigger="install"
local APPS="$(dokku_apps)"
local APP
mkdir -p "${DOKKU_LIB_ROOT}/data/ps"
chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps"
for APP in $APPS; do
local RESTART_POLICIES=$(get_restart_policies "$(get_phase_file_path "deploy")")
if [[ -z "$RESTART_POLICIES" ]]; then
local passed_phases=(deploy)
add_passed_docker_option passed_phases[@] "--restart=on-failure:10"
fi
done
}
trigger-ps-install "$@"

View File

@@ -6,121 +6,6 @@ source "$PLUGIN_AVAILABLE_PATH/ps/functions"
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
cmd-ps-inspect() {
declare desc="displays a sanitized version of docker inspect for an app"
declare cmd="ps:inspect"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
verify_app_name "$APP"
local DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
plugn trigger scheduler-inspect "$DOKKU_SCHEDULER" "$APP"
}
cmd-ps-report() {
declare desc="displays a ps report for one or more apps"
declare cmd="ps:report"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" INFO_FLAG="$2"
local INSTALLED_APPS=$(dokku_apps)
if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then
INFO_FLAG="$APP"
APP=""
fi
if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then
INFO_FLAG="true"
fi
if [[ -z "$APP" ]]; then
for app in $INSTALLED_APPS; do
cmd-ps-report-single "$app" "$INFO_FLAG" | tee || true
done
else
cmd-ps-report-single "$APP" "$INFO_FLAG"
fi
}
cmd-ps-report-single() {
declare APP="$1" INFO_FLAG="$2"
local APP_DIR="$DOKKU_ROOT/$APP"
[[ "$INFO_FLAG" == "true" ]] && INFO_FLAG=""
local APP_RUNNING="$(fn-ps-app-status "$APP")"
local PROCS=$(echo "$APP_RUNNING" | cut -d ' ' -f 1)
local RUNNING=$(echo "$APP_RUNNING" | cut -d ' ' -f 2)
if (is_deployed "$APP"); then DEPLOYED="true"; else DEPLOYED="false"; fi
local passed_phases="deploy"
local RESTARTPOLICY=$(get_restart_policies "$(get_phase_file_path "$passed_phases")" || true)
local CAN_SCALE="$(fn-ps-can-scale "$APP")"
local CONTAINER_FILES="$(find "$DOKKU_ROOT/$APP" -maxdepth 1 -name "CONTAINER.*" -printf "%f\n" 2>/dev/null | sort -t . -k 2 -n | xargs)"
local CONTAINER_FILE
local DOKKU_APP_RESTORE=$(config_get "$APP" DOKKU_APP_RESTORE || true)
local STATUSES=()
if [[ "$DOKKU_APP_RESTORE" != 0 ]]; then RESTORE="true"; else RESTORE="false"; fi
local APP_CONTAINER_STATUS
for CONTAINER_FILE in $CONTAINER_FILES; do
CID=$(<"$DOKKU_ROOT/$APP/$CONTAINER_FILE")
APP_CONTAINER_STATUS=$("$DOCKER_BIN" container inspect --format '{{.State.Status}}' "$CID" 2>/dev/null || true)
[[ -z "$APP_CONTAINER_STATUS" ]] && APP_CONTAINER_STATUS="missing"
STATUSES+=("${CONTAINER_FILE#*.}:$APP_CONTAINER_STATUS#${CID:0:12}")
done
local flag_map=(
"--processes: $PROCS"
"--deployed: $DEPLOYED"
"--running: $RUNNING"
"--restore: $RESTORE"
"--restart-policy: $RESTARTPOLICY"
"--ps-can-scale: $CAN_SCALE"
)
if [[ -z "$INFO_FLAG" ]]; then
dokku_log_info2_quiet "$APP ps information"
for flag in "${flag_map[@]}"; do
key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')"
dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")"
done
for STATUS in "${STATUSES[@]}"; do
name=${STATUS%:*}
value=${STATUS#*:}
status=${value%#*}
cid=${value#*#}
dokku_log_verbose "$(printf "%-30s %-10s (CID: %s)" "Status $name:" "$status" "$cid")"
done
else
local match=false
local value_exists=false
for STATUS in "${STATUSES[@]}"; do
name=${STATUS%:*}
value=${STATUS#*:}
status=${value%#*}
cid=${value#*#}
status_flag="--status-$name"
flag_map+=("$status_flag: $status")
done
for flag in "${flag_map[@]}"; do
valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)"
if [[ "$flag" == "${INFO_FLAG}:"* ]]; then
value=${flag#*: }
size="${#value}"
if [[ "$size" -ne 0 ]]; then
echo "$value" && match=true && value_exists=true
else
match=true
fi
fi
done
if [[ "$match" == "true" ]]; then
[[ "$value_exists" == "true" ]] || dokku_log_fail "not deployed"
else
dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}"
fi
fi
}
fn-ps-parallel-cmd() {
declare desc="Run a ps command against all apps"
declare CMD="$1"

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-post-app-clone() {
declare desc="ps post-app-clone plugin trigger"
declare trigger="post-app-clone"
declare OLD_APP="$1" NEW_APP="$2"
if [[ "$SKIP_REBUILD" != "true" ]]; then
ps_rebuild "$NEW_APP"
fi
}
trigger-ps-post-app-clone "$@"

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-post-app-rename() {
declare desc="ps post-app-rename plugin trigger"
declare trigger="post-app-rename"
declare OLD_APP="$1" NEW_APP="$2"
if [[ "$SKIP_REBUILD" != "true" ]]; then
ps_rebuild "$NEW_APP"
fi
}
trigger-ps-post-app-rename "$@"

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-post-create() {
declare desc="ps post-create plugin trigger"
declare trigger="post-create"
local APP="$1"
local passed_phases=(deploy)
add_passed_docker_option passed_phases[@] "--restart=on-failure:10"
mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}"
chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}"
}
trigger-ps-post-create "$@"

View File

@@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
trigger-ps-post-extract() {
declare desc="ps post-extract plugin trigger"
declare trigger="post-extract"
declare APP="$1" TMP_WORK_DIR="$2" REV="$3"
if [[ ! -f "$TMP_WORK_DIR/Procfile" ]]; then
return
fi
pushd "$TMP_WORK_DIR" >/dev/null
for process_type in $(procfile-util list); do
if echo "$process_type" | grep -vqE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$'; then
dokku_log_fail "Invalid process type name ($process_type)"
fi
done
popd >/dev/null
}
trigger-ps-post-extract "$@"

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-post-stop() {
declare desc="ps post-stop plugin trigger"
declare trigger="post-stop"
declare APP="$1"
DOKKU_QUIET_OUTPUT=1 config_set --no-restart "$APP" DOKKU_APP_RESTORE=0
}
trigger-ps-post-stop "$@"

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-pre-deploy() {
declare desc="ps pre-deploy plugin trigger"
declare trigger="pre-deploy"
declare APP="$1" IMAGE_TAG="$2"
extract_procfile "$APP" "$IMAGE_TAG"
generate_scale_file "$APP" "$IMAGE_TAG"
}
trigger-ps-pre-deploy "$@"

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-procfile-extract() {
declare desc="ps procfile-extract plugin trigger"
declare trigger="procfile-extract"
declare desc="extract procfile from a specified docker image"
declare APP="$1" IMAGE="$2" FORCE_EXTRACT="$3" PROCFILE_PATH="$4"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
mkdir -p "${DOKKU_LIB_ROOT}/data/ps/${APP}"
chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/ps/${APP}"
fi
if [[ "$FORCE_EXTRACT" == "true" ]]; then
plugn trigger procfile-remove "$APP" "$PROCFILE_PATH"
fi
fn-procfile-extract "$IMAGE" "$PROCFILE_PATH"
}
trigger-ps-procfile-extract "$@"

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-ps-procfile-get-command() {
declare desc="ps procfile-get-command plugin trigger"
declare trigger="procfile-get-command"
declare desc="extract command from an app procfile"
declare APP="$1" PROCESS_TYPE="$2" PORT="$3" PROCFILE_PATH="$4"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
fi
fn-procfile-get-command "$PROCFILE_PATH" "$PROCESS_TYPE" "$PORT"
}
trigger-ps-procfile-get-command "$@"

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
trigger-ps-procfile-remove() {
declare desc="ps procfile-remove plugin trigger"
declare trigger="procfile-remove"
declare desc="remove procfile for a given app"
declare APP="$1" PROCFILE_PATH="$2"
if [[ -z "$PROCFILE_PATH" ]]; then
PROCFILE_PATH="$DOKKU_LIB_ROOT/data/ps/$APP/Procfile.extracted"
fi
if [[ ! -f "$PROCFILE_PATH" ]]; then
return
fi
rm -f "$PROCFILE_PATH" 2>/dev/null
}
trigger-ps-procfile-remove "$@"

173
plugins/ps/ps.go Normal file
View File

@@ -0,0 +1,173 @@
package ps
import (
"fmt"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// RunInSerial is the default value for whether to run a command in parallel or not
// and defaults to -1 (false)
const RunInSerial = 0
var (
// DefaultProperties is a map of all valid ps properties with corresponding default property values
DefaultProperties = map[string]string{
"restart-policy": "on-failure:10",
}
)
// Rebuild rebuilds app from base image
func Rebuild(appName string) error {
return common.PlugnTrigger("receive-app", []string{appName}...)
}
// ReportSingleApp is an internal function that displays the ps report for one or more apps
func ReportSingleApp(appName, infoFlag string) error {
if err := common.VerifyAppName(appName); err != nil {
return err
}
policy, _ := getRestartPolicy(appName)
if policy == "" {
policy = DefaultProperties["restart-policy"]
}
canScale := "false"
if canScaleApp(appName) {
canScale = "true"
}
deployed := "false"
if common.IsDeployed(appName) {
deployed = "true"
}
runningState := getRunningState(appName)
count, err := getProcessCount(appName)
if err != nil {
count = -1
}
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_APP_RESTORE"}...)
restore := strings.TrimSpace(string(b[:]))
if restore == "0" {
restore = "false"
} else {
restore = "true"
}
infoFlags := map[string]string{
"--deployed": deployed,
"--processes": strconv.Itoa(count),
"--ps-can-scale": canScale,
"--ps-restart-policy": policy,
"--restore": restore,
"--running": runningState,
}
scheduler := common.GetAppScheduler(appName)
if scheduler == "docker-local" {
processStatus := getProcessStatus(appName)
for process, value := range processStatus {
infoFlags[fmt.Sprintf("--status-%s", process)] = value
}
}
trimPrefix := false
uppercaseFirstCharacter := true
return common.ReportSingleApp("ps", appName, infoFlag, infoFlags, trimPrefix, uppercaseFirstCharacter)
}
// Restart restarts the app
func Restart(appName string) error {
if !common.IsDeployed(appName) {
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
return nil
}
return common.PlugnTrigger("release-and-deploy", []string{appName}...)
}
// Restore ensures an app that should be running is running on boot
func Restore(appName string) error {
scheduler := common.GetAppScheduler(appName)
if err := common.PlugnTrigger("pre-restore", []string{scheduler, appName}...); err != nil {
return fmt.Errorf("Error running pre-restore: %s", err)
}
if err := common.PlugnTrigger("proxy-clear-config", []string{appName}...); err != nil {
return fmt.Errorf("Error clearing proxy config: %s", err)
}
if !common.IsDeployed(appName) {
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
return nil
}
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_APP_RESTORE"}...)
restore := strings.TrimSpace(string(b[:]))
if restore == "0" {
return nil
}
return Start(appName)
}
// Start starts the app
func Start(appName string) error {
imageTag, _ := common.GetRunningImageTag(appName)
if !common.IsDeployed(appName) {
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
return nil
}
if err := common.PlugnTrigger("pre-start", []string{appName}...); err != nil {
return fmt.Errorf("Failure in pre-start hook: %s", err)
}
runningState := getRunningState(appName)
if runningState == "mixed" {
common.LogWarn("App is running in mixed mode, releasing")
}
if runningState != "true" {
if err := common.PlugnTrigger("release-and-deploy", []string{appName, imageTag}...); err != nil {
return err
}
} else {
common.LogWarn(fmt.Sprintf("App %s already running", appName))
}
if err := common.PlugnTrigger("proxy-build-config", []string{appName}...); err != nil {
return err
}
return nil
}
// Stop stops the app
func Stop(appName string) error {
if !common.IsDeployed(appName) {
common.LogWarn(fmt.Sprintf("App %s has not been deployed", appName))
return nil
}
common.LogQuiet(fmt.Sprintf("Stopping %s", appName))
scheduler := common.GetAppScheduler(appName)
if err := common.PlugnTrigger("scheduler-stop", []string{scheduler, appName}...); err != nil {
return err
}
if err := common.PlugnTrigger("post-stop", []string{appName}...); err != nil {
return err
}
return nil
}

View File

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

View File

@@ -0,0 +1,63 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/dokku/dokku/plugins/common"
)
const (
helpHeader = `Usage: dokku ps[:COMMAND]
Manage app processes
Additional commands:`
helpContent = `
ps:inspect <app>, Displays a sanitized version of docker inspect for an app
ps:rebuild [--parallel count] [--all|<app>], Rebuilds an app from source
ps:report [<app>] [<flag>], Displays a process report for one or more apps
ps:restart [--parallel count] [--all|<app>], Restart an app
ps:restore [<app>], Start previously running apps e.g. after reboot
ps:scale [--skip-deploy] <app> <proc>=<count> [<proc>=<count>...], Get/Set how many instances of a given process to run
ps:set <app> <key> <value>, Set or clear a ps property for an app
ps:start [--parallel count] [--all|<app>], Start an app
ps:stop [--parallel count] [--all|<app>], Stop an app
`
)
func main() {
flag.Usage = usage
flag.Parse()
cmd := flag.Arg(0)
switch cmd {
case "ps", "ps: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 ps, Manage app processes\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() {
common.CommandUsage(helpHeader, helpContent)
}

View File

@@ -0,0 +1,91 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/ps"
)
// main entrypoint to all subcommands
func main() {
parts := strings.Split(os.Args[0], "/")
subcommand := parts[len(parts)-1]
var err error
switch subcommand {
case "inspect":
args := flag.NewFlagSet("ps:inspect", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandInspect(appName)
case "rebuild":
args := flag.NewFlagSet("ps:rebuild", flag.ExitOnError)
allApps := args.Bool("all", false, "--all: rebuild all apps")
parallelCount := args.Int("parallel", ps.RunInSerial, "--parallel: number of apps to rebuild in parallel, -1 to match cpu count")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandRebuild(appName, *allApps, *parallelCount)
case "report":
args := flag.NewFlagSet("ps:report", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
infoFlag := args.Arg(1)
err = ps.CommandReport(appName, infoFlag)
case "restart":
args := flag.NewFlagSet("ps:restart", flag.ExitOnError)
allApps := args.Bool("all", false, "--all: restart all apps")
parallelCount := args.Int("parallel", ps.RunInSerial, "--parallel: number of apps to restart in parallel, -1 to match cpu count")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandRestart(appName, *allApps, *parallelCount)
case "restore":
args := flag.NewFlagSet("ps:restore", flag.ExitOnError)
allApps := args.Bool("all", false, "--all: restore all apps")
parallelCount := args.Int("parallel", ps.RunInSerial, "--parallel: number of apps to restore in parallel, -1 to match cpu count")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandRestore(appName, *allApps, *parallelCount)
case "retire":
args := flag.NewFlagSet("ps:retire", flag.ExitOnError)
args.Parse(os.Args[2:])
err = ps.CommandRetire()
case "scale":
args := flag.NewFlagSet("ps:scale", flag.ExitOnError)
skipDeploy := args.Bool("skip-deploy", false, "--skip-deploy: skip deploy of the app")
args.Parse(os.Args[2:])
appName := args.Arg(0)
_, processTuples := common.ShiftString(args.Args())
err = ps.CommandScale(appName, *skipDeploy, processTuples)
case "set":
args := flag.NewFlagSet("ps:set", flag.ExitOnError)
args.Parse(os.Args[2:])
appName := args.Arg(0)
property := args.Arg(1)
value := args.Arg(2)
err = ps.CommandSet(appName, property, value)
case "start":
args := flag.NewFlagSet("ps:start", flag.ExitOnError)
allApps := args.Bool("all", false, "--all: start all apps")
parallelCount := args.Int("parallel", ps.RunInSerial, "--parallel: number of apps to start in parallel, -1 to match cpu count")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandStart(appName, *allApps, *parallelCount)
case "stop":
args := flag.NewFlagSet("ps:stop", flag.ExitOnError)
allApps := args.Bool("all", false, "--all: stop all apps")
parallelCount := args.Int("parallel", ps.RunInSerial, "--parallel: number of apps to stop in parallel, -1 to match cpu count")
args.Parse(os.Args[2:])
appName := args.Arg(0)
err = ps.CommandStop(appName, *allApps, *parallelCount)
default:
common.LogFail(fmt.Sprintf("Invalid plugin subcommand call: %s", subcommand))
}
if err != nil {
common.LogFail(err.Error())
}
}

View File

@@ -0,0 +1,77 @@
package main
import (
"flag"
"fmt"
"os"
"strings"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/ps"
)
// main entrypoint to all triggers
func main() {
parts := strings.Split(os.Args[0], "/")
trigger := parts[len(parts)-1]
flag.Parse()
var err error
switch trigger {
case "app-restart":
appName := flag.Arg(0)
err = ps.TriggerAppRestart(appName)
case "core-post-deploy":
appName := flag.Arg(0)
err = ps.TriggerCorePostDeploy(appName)
case "install":
err = ps.TriggerInstall()
case "post-app-clone":
oldAppName := flag.Arg(0)
newAppName := flag.Arg(1)
err = ps.TriggerPostAppClone(oldAppName, newAppName)
case "post-app-rename":
oldAppName := flag.Arg(0)
newAppName := flag.Arg(1)
err = ps.TriggerPostAppRename(oldAppName, newAppName)
case "post-create":
appName := flag.Arg(0)
err = ps.TriggerPostCreate(appName)
case "post-delete":
appName := flag.Arg(0)
err = ps.TriggerPostDelete(appName)
case "post-extract":
appName := flag.Arg(0)
tmpWorkDir := flag.Arg(1)
err = ps.TriggerPostExtract(appName, tmpWorkDir)
case "post-stop":
appName := flag.Arg(0)
err = ps.TriggerPostStop(appName)
case "pre-deploy":
appName := flag.Arg(0)
imageTag := flag.Arg(1)
err = ps.TriggerPreDeploy(appName, imageTag)
case "procfile-extract":
appName := flag.Arg(0)
image := flag.Arg(1)
err = ps.TriggerProcfileExtract(appName, image)
case "procfile-get-command":
appName := flag.Arg(0)
processType := flag.Arg(1)
port := common.ToInt(flag.Arg(2), 5000)
err = ps.TriggerProcfileGetCommand(appName, processType, port)
case "procfile-remove":
appName := flag.Arg(0)
procfilePath := flag.Arg(1)
err = ps.TriggerProcfileRemove(appName, procfilePath)
case "report":
appName := flag.Arg(0)
err = ps.ReportSingleApp(appName, "")
default:
common.LogFail(fmt.Sprintf("Invalid plugin trigger call: %s", trigger))
}
if err != nil {
common.LogFail(err.Error())
}
}

221
plugins/ps/subcommands.go Normal file
View File

@@ -0,0 +1,221 @@
package ps
import (
"errors"
"fmt"
"path/filepath"
"strings"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
"github.com/dokku/dokku/plugins/common"
"github.com/gofrs/flock"
)
// CommandInspect displays a sanitized version of docker inspect for an app
func CommandInspect(appName string) error {
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
scheduler := common.GetAppScheduler(appName)
return common.PlugnTrigger("scheduler-inspect", []string{scheduler, appName}...)
}
// CommandRebuild rebuilds an app from source
func CommandRebuild(appName string, allApps bool, parallelCount int) error {
if allApps {
return common.RunCommandAgainstAllApps(Rebuild, "rebuild", parallelCount)
}
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
return Rebuild(appName)
}
// CommandReport displays a ps report for one or more apps
func CommandReport(appName string, infoFlag string) error {
if strings.HasPrefix(appName, "--") {
infoFlag = appName
appName = ""
}
if len(appName) == 0 {
apps, err := common.DokkuApps()
if err != nil {
return err
}
for _, appName := range apps {
if err := ReportSingleApp(appName, infoFlag); err != nil {
return err
}
}
return nil
}
return ReportSingleApp(appName, infoFlag)
}
// CommandRestart restarts an app
func CommandRestart(appName string, allApps bool, parallelCount int) error {
if allApps {
return common.RunCommandAgainstAllApps(Restart, "restart", parallelCount)
}
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
return Restart(appName)
}
// CommandRestore starts previously running apps e.g. after reboot
func CommandRestore(appName string, allApps bool, parallelCount int) error {
if allApps {
if err := restorePrep(); err != nil {
return err
}
return common.RunCommandAgainstAllApps(Restore, "restore", parallelCount)
}
if appName == "" {
common.LogWarn("Restore specified without app, assuming --all")
if err := restorePrep(); err != nil {
return err
}
return common.RunCommandAgainstAllApps(Restore, "restore", parallelCount)
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
return Restore(appName)
}
// CommandRetire ensures old containers are retired
func CommandRetire() error {
lockFile := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", "retire")
scheduler := common.GetGlobalScheduler()
fileLock := flock.New(lockFile)
locked, err := fileLock.TryLock()
if err != nil {
return fmt.Errorf("Failed to acquire ps:retire lock: %s", err)
}
defer fileLock.Unlock()
if !locked {
return fmt.Errorf("Failed to acquire ps:retire lock")
}
if err := common.PlugnTrigger("scheduler-retire", []string{scheduler}...); err != nil {
return err
}
return nil
}
// CommandScale gets or sets how many instances of a given process to run
func CommandScale(appName string, skipDeploy bool, processTuples []string) error {
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
procfilePath := getProcfilePath(appName)
if !common.FileExists(procfilePath) {
image := common.GetAppImageRepo(appName)
common.SuppressOutput(func() error {
extractProcfile(appName, image, procfilePath)
return nil
})
}
if !hasScaleFile(appName) {
err := common.SuppressOutput(func() error {
return updateScalefile(appName, make(map[string]int))
})
if err != nil {
return err
}
} else if common.FileExists(procfilePath) {
if err := updateScalefile(appName, make(map[string]int)); err != nil {
return err
}
}
if len(processTuples) == 0 {
return scaleReport(appName)
}
return scaleSet(appName, skipDeploy, processTuples)
}
// CommandSet sets or clears a ps property for an app
func CommandSet(appName string, property string, value string) error {
if property == "restart-policy" {
if !isValidRestartPolicy(value) {
return errors.New("Invalid restart-policy specified")
}
common.LogInfo2Quiet(fmt.Sprintf("Setting %s to %s", property, value))
return dockeroptions.SetDockerOptionForPhases(appName, []string{"deploy"}, "restart", value)
}
common.CommandPropertySet("ps", appName, property, value, DefaultProperties)
return nil
}
// CommandStart starts an app
func CommandStart(appName string, allApps bool, parallelCount int) error {
if allApps {
return common.RunCommandAgainstAllApps(Start, "start", parallelCount)
}
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
return Start(appName)
}
// CommandStop stops an app
func CommandStop(appName string, allApps bool, parallelCount int) error {
if allApps {
return common.RunCommandAgainstAllApps(Stop, "stop", parallelCount)
}
if appName == "" {
return errors.New("Please specify an app to run the command on")
}
if err := common.VerifyAppName(appName); err != nil {
return err
}
return Stop(appName)
}

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-rebuild() {
declare desc="rebuilds an app from source"
declare cmd="ps:rebuild"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
if (is_deployed "$APP"); then
ps_rebuild "$APP"
else
dokku_log_warn "App $APP has not been deployed"
fi
}
cmd-ps-rebuild "$@"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions"
cmd-ps-rebuildall() {
declare desc="rebuilds all apps from source"
declare cmd="ps:rebuildall"
[[ "$1" == "$cmd" ]] && shift 1
fn-ps-parallel-cmd "rebuild"
}
cmd-ps-rebuildall "$@"

View File

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

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
cmd-ps-restart() {
declare desc="restarts app via command line"
declare cmd="ps:restart"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
plugn trigger app-restart "$APP"
}
cmd-ps-restart "$@"

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-restart-policy() {
declare desc="shows the restart-policy for an app"
declare cmd="ps:restart-policy"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
local passed_phases="deploy"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
verify_app_name "$APP"
dokku_log_info2_quiet "$APP restart-policy:"
get_restart_policies "$(get_phase_file_path "$passed_phases")"
}
cmd-ps-restart-policy "$@"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions"
cmd-ps-restartall() {
declare desc="restarts all apps via command line"
declare cmd="ps:restartall"
[[ "$1" == "$cmd" ]] && shift 1
fn-ps-parallel-cmd "restart"
}
cmd-ps-restartall "$@"

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions"
cmd-ps-restore() {
declare desc="starts all apps with DOKKU_APP_RESTORE not set to 0 via command line"
declare cmd="ps:restore"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
local DOKKU_SCHEDULER
DOKKU_SCHEDULER=$(get_app_scheduler "$APP")
plugn trigger pre-restore "$DOKKU_SCHEDULER" "$APP"
if [[ -n "$APP" ]]; then
plugn trigger proxy-clear-config "$APP"
if ! (is_deployed "$APP"); then
dokku_log_warn "App $APP has not been deployed"
return
fi
DOKKU_APP_RESTORE="$(config_get "$APP" DOKKU_APP_RESTORE || true)"
if [[ $DOKKU_APP_RESTORE != 0 ]]; then
ps_restore "$APP" || dokku_log_warn "dokku ps:restore ${APP} failed"
fi
else
for APP in $(dokku_apps); do
plugn trigger proxy-clear-config "$APP"
done
fn-ps-parallel-cmd "restore"
fi
}
cmd-ps-restore "$@"

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
cmd-ps-retire() {
declare desc="ensures old containers are retired"
declare cmd="ps:retire"
[[ "$1" == "$cmd" ]] && shift 1
local LOCK_FILE="${DOKKU_LIB_ROOT}/data/ps/retire"
local DOKKU_SCHEDULER=$(config_get "--global" DOKKU_SCHEDULER || echo "docker-local")
acquire_advisory_lock "$LOCK_FILE" "exclusive" "" "Failed to acquire ps:retire lock"
plugn trigger scheduler-retire "$DOKKU_SCHEDULER"
release_advisory_lock "$LOCK_FILE"
}
cmd-ps-retire "$@"

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-scale() {
declare desc="sets app scaling config via command line"
declare cmd="ps:scale"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
ps_scale "$@"
}
cmd-ps-scale "$@"

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/docker-options/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-set-restart-policy() {
declare desc="sets app restart-policy"
declare cmd="ps:set-restart-policy"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1" RESTART_POLICY="$2"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
verify_app_name "$APP"
local RE_RESTART_POLICY="^(no|unless-stopped|always|on-failure(:[1-9][0-9]*)?)$"
if [[ ! "$RESTART_POLICY" =~ $RE_RESTART_POLICY ]]; then
dokku_log_fail "Please specify an valid restart policy matching the following regex: ${RE_RESTART_POLICY}"
fi
local RESTART_POLICIES=$(get_raw_restart_policies "$(get_phase_file_path "deploy")")
local passed_phases=(deploy)
for policy in $RESTART_POLICIES; do
remove_passed_docker_option passed_phases[@] "$policy"
done
dokku_log_info1_quiet "Setting restart policy: ${RESTART_POLICY}"
local passed_phases=(deploy)
add_passed_docker_option passed_phases[@] "--restart=${RESTART_POLICY}"
}
cmd-ps-set-restart-policy "$@"

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-start() {
declare desc="starts app via command line"
declare cmd="ps:start"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
ps_start "$APP"
}
cmd-ps-start "$@"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions"
cmd-ps-startall() {
declare desc="starts all apps via command line"
declare cmd="ps:startall"
[[ "$1" == "$cmd" ]] && shift 1
fn-ps-parallel-cmd "start"
}
cmd-ps-startall "$@"

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
cmd-ps-stop() {
declare desc="stops app via command line"
declare cmd="ps:stop"
[[ "$1" == "$cmd" ]] && shift 1
declare APP="$1"
[[ -z "$APP" ]] && dokku_log_fail "Please specify an app to run the command on"
ps_stop "$APP"
}
cmd-ps-stop "$@"

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/internal-functions"
cmd-ps-stopall() {
declare desc="stops all apps via command line"
declare cmd="ps:stopall"
[[ "$1" == "$cmd" ]] && shift 1
fn-ps-parallel-cmd "stop"
}
cmd-ps-stopall "$@"

225
plugins/ps/triggers.go Normal file
View File

@@ -0,0 +1,225 @@
package ps
import (
"fmt"
"os"
"path/filepath"
"strings"
sh "github.com/codeskyblue/go-sh"
"github.com/dokku/dokku/plugins/common"
"github.com/dokku/dokku/plugins/config"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
)
// TriggerAppRestart restarts an app
func TriggerAppRestart(appName string) error {
return Restart(appName)
}
// TriggerCorePostDeploy removes extracted procfiles
// and sets a property to allow the app to be restored on boot
func TriggerCorePostDeploy(appName string) error {
if err := removeProcfile(appName); err != nil {
return err
}
entries := map[string]string{
"DOKKU_APP_RESTORE": "1",
}
return common.SuppressOutput(func() error {
return config.SetMany(appName, entries, false)
})
}
// TriggerInstall initializes app restart policies
func TriggerInstall() error {
if err := common.PropertySetup("buildpacks"); err != nil {
return fmt.Errorf("Unable to install the buildpacks plugin: %s", err.Error())
}
directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps")
if err := os.MkdirAll(directory, 0755); err != nil {
return err
}
if err := common.SetPermissions(directory, 0755); err != nil {
return err
}
apps, err := common.DokkuApps()
if err != nil {
return nil
}
for _, appName := range apps {
policies, err := getRestartPolicy(appName)
if err != nil {
return err
}
if len(policies) != 0 {
continue
}
if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil {
common.LogWarn(err.Error())
}
}
return nil
}
// TriggerPostAppClone rebuilds the new app
func TriggerPostAppClone(oldAppName string, newAppName string) error {
if os.Getenv("SKIP_REBUILD") == "true" {
return nil
}
return Rebuild(newAppName)
}
// TriggerPostAppRename rebuilds the renamed app
func TriggerPostAppRename(oldAppName string, newAppName string) error {
if os.Getenv("SKIP_REBUILD") == "true" {
return nil
}
return Rebuild(newAppName)
}
// TriggerPostCreate ensures apps have a default restart policy
// and scale value for web
func TriggerPostCreate(appName string) error {
if err := dockeroptions.AddDockerOptionToPhases(appName, []string{"deploy"}, "--restart=on-failure:10"); err != nil {
return err
}
directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName)
if err := os.MkdirAll(directory, 0755); err != nil {
return err
}
if err := common.SetPermissions(directory, 0755); err != nil {
return err
}
return updateScalefile(appName, make(map[string]int))
}
// TriggerPostDelete destroys the ps properties for a given app container
func TriggerPostDelete(appName string) error {
directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName)
dataErr := os.RemoveAll(directory)
propertyErr := common.PropertyDestroy("ps", appName)
if dataErr != nil {
return dataErr
}
return propertyErr
}
// TriggerPostExtract validates a procfile
func TriggerPostExtract(appName string, tempWorkDir string) error {
procfile := filepath.Join(tempWorkDir, "Procfile")
if !common.FileExists(procfile) {
return nil
}
b, err := sh.Command("procfile-util", "check", "-P", procfile).CombinedOutput()
if err != nil {
return fmt.Errorf(strings.TrimSpace(string(b[:])))
}
return nil
}
// TriggerPostStop sets the restore property to false
func TriggerPostStop(appName string) error {
entries := map[string]string{
"DOKKU_APP_RESTORE": "0",
}
return common.SuppressOutput(func() error {
return config.SetMany(appName, entries, false)
})
}
// TriggerPreDeploy ensures an app has an up to date scale file
func TriggerPreDeploy(appName string, imageTag string) error {
image := common.GetAppImageRepo(appName)
removeProcfile(appName)
procfilePath := getProcfilePath(appName)
if err := extractProcfile(appName, image, procfilePath); err != nil {
return err
}
if err := extractOrGenerateScalefile(appName, image); err != nil {
return err
}
return nil
}
// TriggerProcfileExtract extracted the procfile
func TriggerProcfileExtract(appName string, image string) error {
directory := filepath.Join(common.MustGetEnv("DOKKU_LIB_ROOT"), "data", "ps", appName)
if err := os.MkdirAll(directory, 0755); err != nil {
return err
}
if err := common.SetPermissions(directory, 0755); err != nil {
return err
}
procfilePath := getProcfilePath(appName)
if common.FileExists(procfilePath) {
if err := common.PlugnTrigger("procfile-remove", []string{appName, procfilePath}...); err != nil {
return err
}
}
return extractProcfile(appName, image, procfilePath)
}
// TriggerProcfileGetCommand fetches a command from the procfile
func TriggerProcfileGetCommand(appName string, processType string, port int) error {
procfilePath := getProcfilePath(appName)
if !common.FileExists(procfilePath) {
image := common.GetDeployingAppImageName(appName, "", "")
err := common.SuppressOutput(func() error {
return common.PlugnTrigger("procfile-extract", []string{appName, image}...)
})
if err != nil {
return err
}
}
command, err := getProcfileCommand(procfilePath, processType, port)
if err != nil {
return err
}
if command != "" {
fmt.Printf("%s\n", command)
}
return nil
}
// TriggerProcfileRemove removes the procfile if it exists
func TriggerProcfileRemove(appName string, procfilePath string) error {
if procfilePath == "" {
procfilePath = getProcfilePath(appName)
}
if !common.FileExists(procfilePath) {
return nil
}
os.Remove(procfilePath)
return nil
}

View File

@@ -16,8 +16,8 @@ func PurgeCache(appName string) error {
return err
}
cacheDir := strings.Join([]string{common.MustGetEnv("DOKKU_ROOT"), appName, "cache"}, "/")
cacheHostDir := strings.Join([]string{common.MustGetEnv("DOKKU_HOST_ROOT"), appName, "cache"}, "/")
cacheDir := strings.Join([]string{common.AppRoot(appName), "cache"}, "/")
cacheHostDir := strings.Join([]string{common.AppHostRoot(appName), "cache"}, "/")
dokkuGlobalRunArgs := common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS")
image := config.GetWithDefault(appName, "DOKKU_IMAGE", os.Getenv("DOKKU_IMAGE"))
if info, _ := os.Stat(cacheDir); info != nil && info.IsDir() {

View File

@@ -70,6 +70,7 @@ fn-scheduler-docker-local-retire-container() {
declare APP="$1" CID="$2" DEAD_TIME="$3"
local STATE
dokku_log_info1_quiet "Attempting to retire $APP container $CID"
STATE="$("$DOCKER_BIN" container inspect --format "{{ .State.Status }}" "$CID" 2>/dev/null || true)"
if [[ -z "$STATE" ]]; then
return

View File

@@ -4,7 +4,6 @@ set -eo pipefail
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/checks/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
source "$PLUGIN_AVAILABLE_PATH/scheduler-docker-local/internal-functions"
trigger-scheduler-docker-local-scheduler-deploy() {
@@ -216,7 +215,7 @@ fn-scheduler-docker-local-extract-start-cmd() {
fi
DOKKU_DOCKERFILE_START_CMD=$(config_get "$APP" DOKKU_DOCKERFILE_START_CMD || true)
DOKKU_PROCFILE_START_CMD=$(get_cmd_from_procfile "$APP" "$PROC_TYPE" "$PORT")
DOKKU_PROCFILE_START_CMD=$(plugn trigger procfile-get-command "$APP" "$PROC_TYPE" "$PORT")
START_CMD=${DOKKU_DOCKERFILE_START_CMD:-$DOKKU_PROCFILE_START_CMD}
echo "$START_CMD"
}

View File

@@ -3,7 +3,6 @@ set -eo pipefail
[[ $DOKKU_TRACE ]] && set -x
source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions"
source "$PLUGIN_AVAILABLE_PATH/config/functions"
source "$PLUGIN_AVAILABLE_PATH/ps/functions"
trigger-scheduler-docker-local-scheduler-run() {
declare desc="runs command in container based on app image"
@@ -63,11 +62,8 @@ trigger-scheduler-docker-local-scheduler-run() {
[[ "$IMAGE_SOURCE_TYPE" == "herokuish" ]] && local EXEC_CMD="/exec"
DOKKU_QUIET_OUTPUT=1 extract_procfile "$APP"
POTENTIAL_PROCFILE_KEY="$1"
PROC_CMD=$(get_cmd_from_procfile "$APP" "$POTENTIAL_PROCFILE_KEY" 2>/dev/null || echo '')
remove_procfile "$APP"
PROC_CMD=$(plugn trigger procfile-get-command "$APP" "$POTENTIAL_PROCFILE_KEY" "5000" 2>/dev/null || echo '')
if [[ -n "$PROC_CMD" ]]; then
dokku_log_info1_quiet "Found '$POTENTIAL_PROCFILE_KEY' in Procfile, running that command"

View File

@@ -14,7 +14,7 @@ cmd-tags-deploy() {
local IMAGE="$(get_app_image_name "$APP" "$IMAGE_TAG")"
verify_image "$IMAGE" || return 1
release_and_deploy "$APP" "$IMAGE_TAG"
plugn trigger release-and-deploy "$APP" "$IMAGE_TAG"
if [[ "$IMAGE_TAG" != "latest" ]]; then
local DOKKU_SCHEDULER="$(get_app_scheduler "$APP")"

2
rpm.mk
View File

@@ -42,7 +42,7 @@ endif
--depends 'nc' \
--depends 'nginx >= 1.8.0' \
--depends 'plugn' \
--depends 'procfile-util' \
--depends 'procfile-util >= 0.11.0' \
--depends '/usr/bin/python3' \
--depends 'sshcommand >= 0.10.0' \
--depends 'sudo' \

View File

@@ -64,7 +64,7 @@ install_dokku() {
echo "dokku dokku/skip_key_file boolean true" | sudo debconf-set-selections
echo "dokku dokku/vhost_enable boolean true" | sudo debconf-set-selections
echo "dokku dokku/web_config boolean false" | sudo debconf-set-selections
sudo dpkg -i "$(cat "${ROOT_DIR}/build/deb-filename")"
sudo TRACE=1 dpkg -i "$(cat "${ROOT_DIR}/build/deb-filename")"
}
build_dokku_docker_image() {

View File

@@ -89,7 +89,7 @@ teardown() {
}
@test "(app-json) app.json dockerfile release" {
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk ENVIRONMENT=dev"
run /bin/bash -c "dokku config:set --no-restart $TEST_APP SECRET_KEY=fjdkslafjdk ENVIRONMENT=dev DATABASE_URL=sqlite:///db.sqlite3"
echo "output: $output"
echo "status: $status"
assert_success

View File

@@ -95,6 +95,8 @@ teardown() {
assert_failure
run create_app
echo "output: $output"
echo "status: $status"
assert_success
}

View File

@@ -16,13 +16,13 @@ teardown() {
run /bin/bash -c "dokku ps"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage container processes"
assert_output_contains "Manage app processes"
help_output="$output"
run /bin/bash -c "dokku ps:help"
echo "output: $output"
echo "status: $status"
assert_output_contains "Manage container processes"
assert_output_contains "Manage app processes"
assert_output "$help_output"
}
@@ -40,16 +40,16 @@ teardown() {
@test "(ps:scale) procfile commands extraction" {
source "$PLUGIN_CORE_AVAILABLE_PATH/ps/functions"
cat <<EOF > "$DOKKU_ROOT/$TEST_APP/DOKKU_PROCFILE"
cat <<EOF > "$DOKKU_LIB_ROOT/data/ps/$TEST_APP/Procfile"
web: node web.js --port \$PORT
worker: node worker.js
EOF
run get_cmd_from_procfile "$TEST_APP" web 5001
PLUGIN_PATH=/var/lib/dokku/plugins PLUGIN_CORE_AVAILABLE_PATH=/var/lib/dokku/core-plugins/available DOKKU_LIB_ROOT=/var/lib/dokku run plugn trigger procfile-get-command "$TEST_APP" web 5001
echo "output: $output"
echo "status: $status"
assert_output "node web.js --port 5001"
run get_cmd_from_procfile "$TEST_APP" worker
PLUGIN_PATH=/var/lib/dokku/plugins PLUGIN_CORE_AVAILABLE_PATH=/var/lib/dokku/core-plugins/available DOKKU_LIB_ROOT=/var/lib/dokku run plugn trigger procfile-get-command "$TEST_APP" worker
echo "output: $output"
echo "status: $status"
assert_output "node worker.js"
@@ -64,7 +64,7 @@ EOF
run /bin/bash -c "dokku --quiet ps:scale $TEST_APP"
output=$(echo "$output" | tr -s " ")
echo "output: ($output)"
assert_output $'cron: 0 \ncustom: 0 \nrelease: 0 \nweb: 1 \nworker: 0 '
assert_output $'cron: 0\ncustom: 0\nrelease: 0\nweb: 1\nworker: 0'
pushd $TMP
echo scaletest: sleep infinity >> Procfile
@@ -74,28 +74,27 @@ EOF
run /bin/bash -c "dokku --quiet ps:scale $TEST_APP"
output=$(echo "$output" | tr -s " ")
echo "output: ($output)"
assert_output $'cron: 0 \ncustom: 0 \nrelease: 0 \nweb: 1 \nworker: 0 \nscaletest: 0 '
assert_output $'cron: 0\ncustom: 0\nrelease: 0\nscaletest: 0\nweb: 1\nworker: 0'
popd
rm -rf "$TMP"
}
@test "(ps:restart-policy) default policy" {
run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP"
run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy"
echo "output: $output"
echo "status: $status"
assert_output "on-failure:10"
}
@test "(ps:restart-policy) ps:set-restart-policy, ps:restart-policy" {
@test "(ps:restart-policy) ps:set restart-policy" {
for policy in no unless-stopped always on-failure on-failure:20; do
run /bin/bash -c "dokku ps:set-restart-policy $TEST_APP $policy"
run /bin/bash -c "dokku ps:set $TEST_APP restart-policy $policy"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP"
run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy"
echo "output: $output"
echo "status: $status"
assert_output "$policy"
@@ -104,12 +103,12 @@ EOF
@test "(ps:restart-policy) deployed policy" {
test_restart_policy="on-failure:20"
run /bin/bash -c "dokku ps:set-restart-policy $TEST_APP $test_restart_policy"
run /bin/bash -c "dokku ps:set $TEST_APP restart-policy $test_restart_policy"
echo "output: $output"
echo "status: $status"
assert_success
run /bin/bash -c "dokku --quiet ps:restart-policy $TEST_APP"
run /bin/bash -c "dokku --quiet ps:report $TEST_APP --ps-restart-policy"
echo "output: $output"
echo "status: $status"
assert_output "$test_restart_policy"