diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index f326282ca42..8c4ad2461e3 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -297,6 +297,21 @@ set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x # TODO ``` +### `certs-force` + +- Description: Echos `true` if a cert should be simulated for the app, no output otherwise +- Invoked by: +- Arguments: `$APP` +- Example: + +```shell +#!/usr/bin/env bash + +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x + +# TODO +``` + ### `check-deploy` - Description: Allows you to run checks on a deploy before Dokku allows the container to handle requests. diff --git a/docs/networking/proxies/openresty.md b/docs/networking/proxies/openresty.md new file mode 100644 index 00000000000..65299f4621a --- /dev/null +++ b/docs/networking/proxies/openresty.md @@ -0,0 +1,164 @@ +# OpenResty Proxy + +> New as of 0.31.0 + +Dokku can provide integration with the [OpenResty](https://openresty.org/) proxy service by utilizing the Docker label-based integration implemented by [openresty-docker-proxy](https://github.com/dokku/openresty-docker-proxy). + +``` +openresty:report [] [] # Displays a openresty report for one or more apps +openresty:logs [--num num] [--tail] # Display openresty log output +openresty:set () # Set or clear an openresty property for an app +openresty:show-config # Display openresty compose config +openresty:start # Starts the openresty server +openresty:stop # Stops the openresty server +``` + +## Requirements + +Using the `openresty` plugin integration requires the `docker-compose-plugin` for Docker. See [this document](https://docs.docker.com/compose/install/) from the Docker documentation for more information on the installation process for the `docker-compose-plugin`. + +## Usage + +> Warning: As using multiple proxy plugins on a single Dokku installation can lead to issues routing requests to apps, doing so should be avoided. As the default proxy implementation is nginx, users are encouraged to stop the nginx service before switching to OpenResty. + +The OpenResty plugin has specific rules for routing requests: + +- OpenResty integration is exposed via docker labels attached to containers. Changes in labels require either app deploys or rebuilds. +- While OpenResty will respect labels associated with other containers, only `web` containers have OpenResty labels injected by the plugin. +- Only `http:80` and `https:443` port mappings are supported at this time. +- Requests are routed as soon as the container is running and passing healthchecks. + +### Switching to OpenResty + +To use the OpenResty plugin, use the `proxy:set` command for the app in question: + +```shell +dokku proxy:set node-js-app openresty +``` + +This will enable the docker label-based OpenResty integration. All future deploys will inject the correct labels for OpenResty to read and route requests to containers. Due to the docker label-based integration used by OpenResty, a single deploy or rebuild will be required before requests will route successfully. + +```shell +dokku ps:rebuild node-js-app +``` + +Any changes to domains or port mappings will also require either a deploy or rebuild. + +### Starting OpenResty container + +OpenResty can be started via the `openresty:start` command. This will start a OpenResty container via the `docker compose up` command. + +```shell +dokku openresty:start +``` + +### Stopping the OpenResty container + +OpenResty may be stopped via the `openresty:stop` command. + +```shell +dokku openresty:stop +``` + +The OpenResty container will be stopped and removed from the system. If the container is not running, this command will do nothing. + +### Showing the OpenResty compose config + +For debugging purposes, it may be useful to show the OpenResty compose config. This can be achieved via the `openresty:show-config` command. + +```shell +dokku openresty:show-config +``` + +### Customizing the OpenResty container image + +While the default OpenResty image is hardcoded, users may specify an alternative by setting the `image` property with the `--global` flag: + +```shell +dokku openresty:set --global image dokku/openresty-docker-proxy:0.5.6 +``` + +#### Checking the OpenResty container's logs + +It may be necessary to check the OpenResty container's logs to ensure that OpenResty is operating as expected. This can be performed with the `openresty:logs` command. + +```shell +dokku openresty:logs +``` + +This command also supports the following modifiers: + +```shell +--num NUM # the number of lines to display +--tail # continually stream logs +``` + +You can use these modifiers as follows: + +```shell +dokku openresty:logs --tail --num 10 +``` + +The above command will show logs continually from the openresty container, with an initial history of 10 log lines + +### SSL Configuration + +The OpenResty plugin only supports automatic ssl certificates from it's letsencrypt integration. Managed certificates provided by the `certs` plugin are ignored. + +#### Enabling letsencrypt integration + +By default, letsencrypt is disabled and https port mappings are ignored. To enable, set the `letsencrypt-email` property with the `--global` flag: + +```shell +dokku openresty:set --global letsencrypt-email automated@dokku.sh +``` + +After enabling, the OpenResty container will need to be restarted and apps will need to be rebuilt. All http requests will then be redirected to https. + +#### Customizing the letsencrypt server + +The letsencrypt integration is set to the production letsencrypt server by default. To change this, set the `letsencrypt-server` property with the `--global` flag: + +```shell +dokku openresty:set --global letsencrypt-server https://acme-staging-v02.api.letsencrypt.org/directory +``` + +After enabling, the OpenResty container will need to be restarted and apps will need to be rebuilt to retrieve certificates from the new server. + +## Displaying OpenResty reports for an app + +You can get a report about the app's OpenResty config using the `openresty:report` command: + +```shell +dokku openresty:report +``` + +``` +=====> node-js-app openresty information + Openresty image: dokku/openresty-docker-proxy:0.5.6 + Openresty letsencrypt email: automated@dokku.sh +=====> python-app openresty information + Openresty image: dokku/openresty-docker-proxy:0.5.6 + Openresty letsencrypt email: automated@dokku.sh +=====> ruby-app openresty information + Openresty image: dokku/openresty-docker-proxy:0.5.6 + Openresty letsencrypt email: automated@dokku.sh +``` + +You can run the command for a specific app also. + +```shell +dokku openresty:report node-js-app +``` + +``` +=====> node-js-app openresty information + Openresty image: dokku/openresty-docker-proxy:0.5.6 + Openresty letsencrypt email: automated@dokku.sh +``` + +You can pass flags which will output only the value of the specific information you want. For example: + +```shell +dokku openresty:report node-js-app --openresty-letsencrypt-email +``` diff --git a/docs/template.html b/docs/template.html index ce49b3a88dc..1832e64aeb0 100644 --- a/docs/template.html +++ b/docs/template.html @@ -178,6 +178,7 @@ Caddy Proxy Haproxy Proxy Nginx Proxy + Openresty Proxy Traefik Proxy Advanced Usage diff --git a/dokku b/dokku index 0a208647175..ce657b8a1c7 100755 --- a/dokku +++ b/dokku @@ -143,6 +143,9 @@ execute_dokku_cmd() { nginx | nginx:*) local PLUGIN_NAME=${PLUGIN_NAME/nginx/nginx-vhosts} ;; + openresty | openresty:*) + local PLUGIN_NAME=${PLUGIN_NAME/openresty/openresty-vhosts} + ;; traefik | traefik:*) local PLUGIN_NAME=${PLUGIN_NAME/traefik/traefik-vhosts} ;; diff --git a/plugins/20_events/certs-force b/plugins/20_events/certs-force new file mode 120000 index 00000000000..5178a749ff6 --- /dev/null +++ b/plugins/20_events/certs-force @@ -0,0 +1 @@ +hook \ No newline at end of file diff --git a/plugins/caddy-vhosts/certs-force b/plugins/caddy-vhosts/certs-force new file mode 100755 index 00000000000..a01fd5d5aaa --- /dev/null +++ b/plugins/caddy-vhosts/certs-force @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/caddy-vhosts/internal-functions" + +trigger-caddy-vhosts-certs-force() { + declare desc="caddy-vhosts certs-force plugin trigger" + declare trigger="certs-force" + declare APP="$1" + + if [[ "$(plugn trigger proxy-type "$APP")" != "caddy" ]]; then + return + fi + + if [[ -n "$(fn-caddy-letsencrypt-email)" ]]; then + echo true + fi +} + +trigger-caddy-vhosts-certs-force "$@" diff --git a/plugins/common/functions b/plugins/common/functions index adf5066ae68..f9a07f50297 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -506,6 +506,63 @@ copy_from_image() { fi } +copy_dir_from_image() { + declare desc="copy a directory from named image to destination" + declare IMAGE="$1" SRC_DIR="$2" DST_DIR="$3" + local WORK_DIR="" + + local DOCKER_CREATE_LABEL_ARGS="--label=com.dokku.app-name=$APP" + + if verify_image "$IMAGE"; then + if ! is_abs_path "$SRC_DIR"; then + if is_image_cnb_based "$IMAGE"; then + WORKDIR="/workspace" + elif is_image_herokuish_based "$IMAGE" "$APP"; then + WORKDIR="/app" + else + WORKDIR="$("$DOCKER_BIN" image inspect --format '{{.Config.WorkingDir}}' "$IMAGE")" + fi + + if [[ -n "$WORKDIR" ]]; then + SRC_DIR="${WORKDIR}/${SRC_DIR}" + fi + fi + + TMP_DIR_COMMAND_OUTPUT=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") + trap "rm -rf '$TMP_DIR_COMMAND_OUTPUT' &>/dev/null || true" RETURN + + local CID=$("$DOCKER_BIN" container create "${DOCKER_CREATE_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS "$IMAGE") + "$DOCKER_BIN" container cp "$CID:$SRC_DIR" "$TMP_DIR_COMMAND_OUTPUT" 2>/dev/null || true + "$DOCKER_BIN" container rm --force "$CID" &>/dev/null + plugn trigger scheduler-register-retired "$APP" "$CID" + + # 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 + if [[ ! -d "$TMP_DIR_COMMAND_OUTPUT" ]]; then + return 1 + fi + + pushd "$TMP_DIR_COMMAND_OUTPUT" >/dev/null + for filename in *; do + if [[ ! -f "$TMP_DIR_COMMAND_OUTPUT/$filename" ]]; then + continue + fi + + # workaround for CHECKS file when owner is root. seems to only happen when running inside docker + dos2unix -l <"$TMP_DIR_COMMAND_OUTPUT/$filename" >"$DST_DIR/$filename" + + # add trailing newline for certain places where file parsing depends on it + if [[ "$(tail -c1 "$DST_DIR/$filename")" != "" ]]; then + echo "" >>"$DST_DIR/$filename" + fi + done + popd &>/dev/null || pushd "/tmp" >/dev/null + else + return 1 + fi +} + get_app_container_ids() { declare desc="returns list of docker container ids for given app and optional container_type" local APP="$1" diff --git a/plugins/common/subprocess.go b/plugins/common/subprocess.go index 19b02e81e4d..b428b9da866 100644 --- a/plugins/common/subprocess.go +++ b/plugins/common/subprocess.go @@ -124,6 +124,12 @@ func PlugnTriggerOutput(triggerName string, args ...string) ([]byte, error) { return readStdout, err } +// PlugnTriggerOutputAsString fires the given plugn trigger with the given args and returns the string contents instead of bytes +func PlugnTriggerOutputAsString(triggerName string, args ...string) (string, error) { + b, err := PlugnTriggerOutput(triggerName, args...) + return strings.TrimSpace(string(b[:])), err +} + // PlugnTriggerSetup sets up a plugn trigger call func PlugnTriggerSetup(triggerName string, args ...string) *sh.Session { shellArgs := make([]interface{}, len(args)+2) diff --git a/plugins/haproxy-vhosts/certs-force b/plugins/haproxy-vhosts/certs-force new file mode 100755 index 00000000000..69bab2c83a2 --- /dev/null +++ b/plugins/haproxy-vhosts/certs-force @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/haproxy-vhosts/internal-functions" + +trigger-haproxy-vhosts-certs-force() { + declare desc="haproxy-vhosts certs-force plugin trigger" + declare trigger="certs-force" + declare APP="$1" + + if [[ "$(plugn trigger proxy-type "$APP")" != "haproxy" ]]; then + return + fi + + if [[ -n "$(fn-haproxy-letsencrypt-email)" ]]; then + echo true + fi +} + +trigger-haproxy-vhosts-certs-force "$@" diff --git a/plugins/nginx-vhosts/docker-args-process-deploy b/plugins/nginx-vhosts/docker-args-process-deploy deleted file mode 100755 index 78b06cb98d9..00000000000 --- a/plugins/nginx-vhosts/docker-args-process-deploy +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x -source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" -source "$PLUGIN_AVAILABLE_PATH/nginx-vhosts/functions" - -trigger-nginx-vhosts-docker-args-process-deploy() { - declare desc="nginx-vhosts docker-args-process-deploy plugin trigger" - declare trigger="docker-args-process-deploy" - declare APP="$1" IMAGE_SOURCE_TYPE="$2" IMAGE_TAG="$3" PROC_TYPE="$4" CONTAINER_INDEX="$5" - local output - local STDIN=$(cat) - - if [[ "$PROC_TYPE" != "web" ]]; then - return - fi - - if [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then - return - fi - - if [[ "$(plugn trigger proxy-is-enabled "$APP")" != "true" ]]; then - return - fi - - if ! plugn trigger domains-vhost-enabled "$APP" 2>/dev/null; then - return - fi - - # ensure we have a port mapping - plugn trigger ports-configure "$APP" - - value="$(fn-nginx-access-log-format "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.access-log-format=$value'" - value="$(fn-nginx-bind-address-ipv4 "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.bind-address-ipv4=$value'" - value="$(fn-nginx-bind-address-ipv6 "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.bind-address-ipv6=$value'" - value="$(fn-nginx-client-max-body-size "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.client-max-body-size=$value'" - value="$(fn-nginx-hsts-include-subdomains "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.hsts-include-subdomains=$value'" - value="$(fn-nginx-hsts-max-age "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.hsts-max-age=$value'" - value="$(fn-nginx-hsts-preload "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.hsts-preload=$value'" - value="$(fn-nginx-hsts-is-enabled "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.hsts=$value'" - value="443" - [[ -n "$value" ]] && output="$output '--label=nginx.https-port=$value'" - value="$(plugn trigger domains-list "$APP" | xargs)" - [[ -n "$value" ]] && output="$output '--label=nginx.domains=$value'" - value="$(plugn trigger network-get-property "$APP" initial-network)" - [[ -n "$value" ]] && output="$output '--label=nginx.initial-network=$value'" - value="$(plugn trigger ports-get "$APP" | xargs)" - [[ -n "$value" ]] && output="$output '--label=nginx.port-mapping=$value'" - value="$(fn-nginx-proxy-buffer-size "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.proxy-buffer-size=$value'" - value="$(fn-nginx-proxy-buffering "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.proxy-buffering=$value'" - value="$(fn-nginx-proxy-buffers "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.proxy-buffers=$value'" - value="$(fn-nginx-proxy-busy-buffers-size "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.proxy-busy-buffer-size=$value'" - value="$(fn-nginx-proxy-read-timeout "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.proxy-read-timeout=$value'" - value="$(fn-nginx-x-forwarded-for-value "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.x-forwarded-for-value=$value'" - value="$(fn-nginx-x-forwarded-port-value "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.x-forwarded-port-value=$value'" - value="$(fn-nginx-x-forwarded-proto-value "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.x-forwarded-proto-value=$value'" - value="$(fn-nginx-x-forwarded-ssl "$APP")" - [[ -n "$value" ]] && output="$output '--label=nginx.x-forwarded-ssl=$value'" - - echo -n "$STDIN$output" -} - -trigger-nginx-vhosts-docker-args-process-deploy "$@" diff --git a/plugins/openresty-vhosts/certs-force b/plugins/openresty-vhosts/certs-force new file mode 100755 index 00000000000..fdaaf728572 --- /dev/null +++ b/plugins/openresty-vhosts/certs-force @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" + +trigger-openresty-vhosts-certs-force() { + declare desc="openresty-vhosts certs-force plugin trigger" + declare trigger="certs-force" + declare APP="$1" + + if [[ "$(plugn trigger proxy-type "$APP")" != "openresty" ]]; then + return + fi + + if [[ -n "$(fn-openresty-letsencrypt-email)" ]]; then + echo true + fi +} + +trigger-openresty-vhosts-certs-force "$@" diff --git a/plugins/openresty-vhosts/command-functions b/plugins/openresty-vhosts/command-functions new file mode 100755 index 00000000000..d630d87d5cb --- /dev/null +++ b/plugins/openresty-vhosts/command-functions @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-report() { + declare desc="displays a openresty report for one or more apps" + declare cmd="openresty:report" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" INFO_FLAG="$2" + + 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 $(dokku_apps); do + cmd-openresty-report-single "$app" "$INFO_FLAG" | tee || true + done + else + cmd-openresty-report-single "$APP" "$INFO_FLAG" + fi +} + +cmd-openresty-report-single() { + declare APP="$1" INFO_FLAG="$2" + if [[ "$INFO_FLAG" == "true" ]]; then + INFO_FLAG="" + fi + verify_app_name "$APP" + local flag_map=( + "--openresty-access-log-format: $(fn-openresty-access-log-format "$APP")" + "--openresty-access-log-path: $(fn-openresty-access-log-path "$APP")" + "--openresty-bind-address-ipv4: $(fn-openresty-bind-address-ipv4 "$APP")" + "--openresty-bind-address-ipv6: $(fn-openresty-bind-address-ipv6 "$APP")" + "--openresty-client-max-body-size: $(fn-openresty-client-max-body-size "$APP")" + "--openresty-error-log-path: $(fn-openresty-error-log-path "$APP")" + "--openresty-global-hsts: $(fn-plugin-property-get-default "openresty" "--global" "hsts" "true")" + "--openresty-computed-hsts: $(fn-openresty-hsts-is-enabled "$APP")" + "--openresty-hsts: $(fn-plugin-property-get-default "openresty" "$APP" "hsts" "")" + "--openresty-hsts-include-subdomains: $(fn-openresty-hsts-include-subdomains "$APP")" + "--openresty-hsts-max-age: $(fn-openresty-hsts-max-age "$APP")" + "--openresty-hsts-preload: $(fn-openresty-hsts-preload "$APP")" + "--openresty-image: $(fn-openresty-image)" + "--openresty-letsencrypt-email: $(fn-openresty-letsencrypt-email)" + "--openresty-letsencrypt-server: $(fn-openresty-letsencrypt-server)" + "--openresty-proxy-buffer-size: $(fn-openresty-proxy-buffer-size "$APP")" + "--openresty-proxy-buffering: $(fn-openresty-proxy-buffering "$APP")" + "--openresty-proxy-buffers: $(fn-openresty-proxy-buffers "$APP")" + "--openresty-proxy-busy-buffers-size: $(fn-openresty-proxy-busy-buffers-size "$APP")" + "--openresty-proxy-read-timeout: $(fn-openresty-proxy-read-timeout "$APP")" + "--openresty-x-forwarded-for-value: $(fn-openresty-x-forwarded-for-value "$APP")" + "--openresty-x-forwarded-port-value: $(fn-openresty-x-forwarded-port-value "$APP")" + "--openresty-x-forwarded-proto-value: $(fn-openresty-x-forwarded-proto-value "$APP")" + "--openresty-x-forwarded-ssl: $(fn-openresty-x-forwarded-ssl "$APP")" + ) + + if [[ -z "$INFO_FLAG" ]]; then + dokku_log_info2_quiet "${APP} openresty information" + for flag in "${flag_map[@]}"; do + key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" + dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" + done + else + local match=false + local value_exists=false + for flag in "${flag_map[@]}"; do + valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" + if [[ "$flag" == "${INFO_FLAG}:"* ]]; then + value=${flag#*: } + size="${#value}" + if [[ "$size" -ne 0 ]]; then + echo "$value" && match=true && value_exists=true + else + match=true + fi + fi + done + [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" + fi +} + +cmd-openresty-logs() { + declare desc="display openresty logs from command line" + declare cmd="openresty:logs" + [[ "$1" == "$cmd" ]] && shift 1 + local NUM="100" TAIL=false + + local TEMP=$(getopt -o htn: --long help,tail,num: -n 'dokku openresty:logs' -- "$@") + local EXIT_CODE="$?" + if [[ "$EXIT_CODE" != 0 ]]; then + fn-openresty-logs-usage >&2 + exit 1 + fi + eval set -- "$TEMP" + + while true; do + case "$1" in + -t | --tail) + local TAIL=true + shift + ;; + -n | --num) + local NUM="$2" + shift 2 + ;; + --) + shift + break + ;; + *) dokku_log_fail "Internal error" ;; + esac + done + + fn-openresty-logs "$TAIL" "$NUM" +} + +cmd-openresty-show-config() { + declare desc="display openresty config" + declare cmd="openresty:show-config" + [[ "$1" == "$cmd" ]] && shift 1 + + if ! fn-is-compose-installed; then + dokku_log_fail "Required docker compose plugin is not installed" + fi + + local TMP_COMPOSE_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") + trap "rm -rf '$TMP_COMPOSE_FILE' >/dev/null" RETURN INT TERM EXIT + + fn-openresty-template-compose-file "$TMP_COMPOSE_FILE" + cat "$TMP_COMPOSE_FILE" +} + +cmd-openresty-start() { + declare desc="Starts the openresty server" + declare cmd="openresty:start" + [[ "$1" == "$cmd" ]] && shift 1 + + if ! fn-is-compose-installed; then + dokku_log_fail "Required docker compose plugin is not installed" + fi + + local TMP_COMPOSE_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") + trap "rm -rf '$TMP_COMPOSE_FILE' >/dev/null" RETURN INT TERM EXIT + + fn-plugin-property-write "openresty" "--global" "proxy-status" "started" + fn-openresty-template-compose-file "$TMP_COMPOSE_FILE" + "$DOCKER_BIN" compose -f "$TMP_COMPOSE_FILE" -p openresty up -d --quiet-pull +} + +cmd-openresty-stop() { + declare desc="Starts the openresty server" + declare cmd="openresty:stop" + [[ "$1" == "$cmd" ]] && shift 1 + + if ! fn-is-compose-installed; then + dokku_log_fail "Required docker compose plugin is not installed" + fi + + local TMP_COMPOSE_FILE=$(mktemp "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX") + trap "rm -rf '$TMP_COMPOSE_FILE' >/dev/null" RETURN INT TERM EXIT + + fn-plugin-property-write "openresty" "--global" "proxy-status" "stopped" + fn-openresty-template-compose-file "$TMP_COMPOSE_FILE" + "$DOCKER_BIN" compose -f "$TMP_COMPOSE_FILE" -p openresty down --remove-orphans +} diff --git a/plugins/openresty-vhosts/commands b/plugins/openresty-vhosts/commands new file mode 100755 index 00000000000..b87c8aa1433 --- /dev/null +++ b/plugins/openresty-vhosts/commands @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +[[ " openresty:help help " == *" $1 "* ]] || exit "$DOKKU_NOT_IMPLEMENTED_EXIT" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/help-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +case "$1" in + help | openresty:help) + cmd-openresty-help "$@" + ;; + + *) + exit "$DOKKU_NOT_IMPLEMENTED_EXIT" + ;; +esac diff --git a/plugins/openresty-vhosts/core-post-deploy b/plugins/openresty-vhosts/core-post-deploy new file mode 100755 index 00000000000..c8ad3ea0cae --- /dev/null +++ b/plugins/openresty-vhosts/core-post-deploy @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-openresty-vhosts-core-post-deploy() { + declare desc="openresty-vhosts core-post-deploy plugin trigger" + declare trigger="core-post-deploy" + declare APP="$1" + local tls_internal + + if [[ "$(plugn trigger proxy-type "$APP")" != "openresty" ]]; then + return + fi + + dokku_log_info1 "Routing app via openresty" + if [[ -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" ]]; then + rm -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" + rm -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes" + fi +} + +trigger-openresty-vhosts-core-post-deploy "$@" diff --git a/plugins/openresty-vhosts/core-post-extract b/plugins/openresty-vhosts/core-post-extract new file mode 100755 index 00000000000..e30bba6c649 --- /dev/null +++ b/plugins/openresty-vhosts/core-post-extract @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +fn-openresty-vhosts-copy-from-image() { + declare APP="$1" IMAGE_NAME="$2" CONF_PATH="$3" + + mkdir -p "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP" + rm -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes."* + copy_dir_from_image "$IMAGE_NAME" "$CONF_PATH" "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID" || true + if [[ ! -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID" ]]; then + touch "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" + fi +} + +fn-openresty-vhosts-copy-from-directory() { + declare APP="$1" SOURCECODE_WORK_DIR="$2" CONF_PATH="$3" + + pushd "$SOURCECODE_WORK_DIR" >/dev/null + mkdir -p "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP" + + if [[ -z "$CONF_PATH" ]]; then + touch "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" + return + fi + + if [[ ! -d "$CONF_PATH" ]]; then + touch "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" + return + fi + + rm -f "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes."* + mkdir p "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID/" + cp -f "$CONF_PATH"/* "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID/" + popd &>/dev/null || pushd "/tmp" >/dev/null +} + +trigger-openresty-vhosts-core-post-extract() { + declare desc="openresty-vhosts post-extract plugin trigger" + declare trigger="post-extract" + declare APP="$1" SOURCECODE_WORK_DIR="$2" + local CONF_PATH="openresty-http-includes" + local app_source_image + + app_source_image="$(plugn trigger git-get-property "$APP" "source-image")" + if [[ -n "$app_source_image" ]]; then + fn-openresty-vhosts-copy-from-image "$APP" "$app_source_image" "$CONF_PATH" + else + fn-openresty-vhosts-copy-from-directory "$APP" "$SOURCECODE_WORK_DIR" "$CONF_PATH" + fi +} + +trigger-openresty-vhosts-core-post-extract "$@" diff --git a/plugins/openresty-vhosts/docker-args-process-deploy b/plugins/openresty-vhosts/docker-args-process-deploy new file mode 100755 index 00000000000..1f78bdb3364 --- /dev/null +++ b/plugins/openresty-vhosts/docker-args-process-deploy @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/internal-functions" + +trigger-openresty-vhosts-docker-args-process-deploy() { + declare desc="openresty-vhosts docker-args-process-deploy plugin trigger" + declare trigger="docker-args-process-deploy" + declare APP="$1" IMAGE_SOURCE_TYPE="$2" IMAGE_TAG="$3" PROC_TYPE="$4" CONTAINER_INDEX="$5" + local include_dir output value DATA + local STDIN=$(cat) + + if [[ "$PROC_TYPE" != "web" ]]; then + return + fi + + if [[ "$(plugn trigger proxy-type "$APP")" != "openresty" ]] && [[ "$(plugn trigger proxy-type "$APP")" != "nginx" ]]; then + return + fi + + if [[ "$(plugn trigger proxy-is-enabled "$APP")" != "true" ]]; then + return + fi + + if ! plugn trigger domains-vhost-enabled "$APP" 2>/dev/null; then + return + fi + + # ensure we have a port mapping + plugn trigger ports-configure "$APP" + + include_dir="$(fn-openresty-get-http-includes-dir "$APP")" + if [[ -d "$include_dir" ]]; then + pushd "$include_dir" >/dev/null + for filename in *; do + if [[ ! -f "$include_dir/$filename" ]]; then + continue + fi + + if [[ $filename != *.conf ]]; then + continue + fi + + DATA="$(base64 -w 0 <"$include_dir/$filename")" + output="$output '--label=openresty.include-http-$filename=$DATA'" + done + popd &>/dev/null || pushd "/tmp" >/dev/null + fi + + is_app_listening="false" + local APP_PORT_MAP="$(plugn trigger ports-get "$APP")" + while IFS= read -r port_map; do + proxy_scheme="$(awk -F ':' '{ print $1 }' <<<"$port_map")" + proxy_host_port="$(awk -F ':' '{ print $2 }' <<<"$port_map")" + proxy_container_port="$(awk -F ':' '{ print $3 }' <<<"$port_map")" + + if [[ "$proxy_scheme" == "http" ]]; then + is_app_listening="true" + if [[ -z "$proxy_container_http_port_candidate" ]]; then + proxy_container_http_port_candidate="$proxy_container_port" + proxy_host_http_port_candidate="$proxy_host_port" + fi + + if [[ "$proxy_host_port" == "80" ]] && [[ -z "$proxy_container_http_port" ]]; then + proxy_container_http_port="$proxy_container_port" + fi + fi + + if [[ "$proxy_scheme" == "https" ]]; then + is_app_listening="true" + if [[ -z "$proxy_container_https_port_candidate" ]]; then + proxy_container_https_port_candidate="$proxy_container_port" + proxy_host_https_port_candidate="$proxy_host_port" + fi + + if [[ "$proxy_host_port" == "443" ]] && [[ -z "$proxy_container_https_port" ]]; then + proxy_container_https_port="$proxy_container_port" + fi + fi + done <<<"$APP_PORT_MAP" + + if [[ -n "$letsencrypt_email" ]] && [[ -z "$proxy_container_https_port" ]]; then + proxy_container_https_port_candidate="$proxy_container_http_port_candidate" + proxy_host_https_port_candidate="$proxy_host_http_port_candidate" + if [[ -n "$proxy_container_http_port" ]]; then + proxy_container_https_port_candidate="$proxy_container_http_port" + proxy_host_http_port_candidate=443 + fi + fi + + letsencrypt_value="false" + if [[ -n "$(fn-openresty-letsencrypt-email)" ]]; then + if [[ -n "$proxy_container_https_port" ]] || [[ -n "$proxy_container_https_port_candidate" ]]; then + letsencrypt_value="true" + fi + fi + + output="$output '--label=openresty.letsencrypt=$letsencrypt_value'" + + value="$(fn-openresty-access-log-format "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.access-log-format=$value'" + value="$(fn-openresty-bind-address-ipv4 "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.bind-address-ipv4=$value'" + value="$(fn-openresty-bind-address-ipv6 "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.bind-address-ipv6=$value'" + value="$(fn-openresty-client-max-body-size "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.client-max-body-size=$value'" + value="$(fn-openresty-hsts-include-subdomains "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.hsts-include-subdomains=$value'" + value="$(fn-openresty-hsts-max-age "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.hsts-max-age=$value'" + value="$(fn-openresty-hsts-preload "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.hsts-preload=$value'" + value="$(fn-openresty-hsts-is-enabled "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.hsts=$value'" + value="443" + [[ -n "$value" ]] && output="$output '--label=openresty.https-port=$value'" + value="$(plugn trigger domains-list "$APP" | xargs)" + [[ -n "$value" ]] && output="$output '--label=openresty.domains=$value'" + value="$(plugn trigger network-get-property "$APP" initial-network)" + [[ -n "$value" ]] && output="$output '--label=openresty.initial-network=$value'" + value="$(echo "$APP_PORT_MAP" | xargs)" + [[ -n "$value" ]] && output="$output '--label=openresty.port-mapping=$value'" + value="$(fn-openresty-proxy-buffer-size "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-buffer-size=$value'" + value="$(fn-openresty-proxy-buffering "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-buffering=$value'" + value="$(fn-openresty-proxy-buffers "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-buffers=$value'" + value="$(fn-openresty-proxy-busy-buffers-size "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-busy-buffer-size=$value'" + value="$(fn-openresty-proxy-connect-timeout "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-connect-timeout=$value'" + value="$(fn-openresty-proxy-read-timeout "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-read-timeout=$value'" + value="$(fn-openresty-proxy-send-timeout "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.proxy-send-timeout=$value'" + value="$(fn-openresty-send-timeout "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.send-timeout=$value'" + value="$(fn-openresty-x-forwarded-for-value "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.x-forwarded-for-value=$value'" + value="$(fn-openresty-x-forwarded-port-value "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.x-forwarded-port-value=$value'" + value="$(fn-openresty-x-forwarded-proto-value "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.x-forwarded-proto-value=$value'" + value="$(fn-openresty-x-forwarded-ssl "$APP")" + [[ -n "$value" ]] && output="$output '--label=openresty.x-forwarded-ssl=$value'" + + echo -n "$STDIN$output" +} + +trigger-openresty-vhosts-docker-args-process-deploy "$@" diff --git a/plugins/openresty-vhosts/help-functions b/plugins/openresty-vhosts/help-functions new file mode 100755 index 00000000000..d8fc19e7a5f --- /dev/null +++ b/plugins/openresty-vhosts/help-functions @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-help() { + declare desc="help command" + declare CMD="$1" + local plugin_name="openresty" + local plugin_description="Manage the openresty proxy integration" + + 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 <] [], Displays an openresty report for one or more apps + openresty:set (), Set or clear an openresty property for an app + openresty:show-config , Display openresty compose config + openresty:start, Starts the openresty server + openresty:stop, Stops the openresty server +help_content +} diff --git a/plugins/openresty-vhosts/install b/plugins/openresty-vhosts/install new file mode 100755 index 00000000000..643146b17c6 --- /dev/null +++ b/plugins/openresty-vhosts/install @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-openresty-install() { + declare desc="installs the openresty plugin" + declare trigger="install" + + mkdir -p "${DOKKU_LIB_ROOT}/data/openresty" "${DOKKU_LIB_ROOT}/data/openresty/.docker-letsencrypt" + chown -R "${DOKKU_SYSTEM_USER}:${DOKKU_SYSTEM_GROUP}" "${DOKKU_LIB_ROOT}/data/openresty" + + fn-plugin-property-setup "openresty" +} + +trigger-openresty-install "$@" diff --git a/plugins/openresty-vhosts/internal-functions b/plugins/openresty-vhosts/internal-functions new file mode 100755 index 00000000000..c2279926339 --- /dev/null +++ b/plugins/openresty-vhosts/internal-functions @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +fn-get-pagesize() { + declare desc="return the underlying system's memory page size" + declare todo="port to common functions" + + "$PLUGIN_CORE_AVAILABLE_PATH/nginx-vhosts/pagesize" +} + +fn-openresty-access-log-format() { + declare desc="get the configured access log format" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "access-log-format" "" +} + +fn-openresty-access-log-path() { + declare desc="get the configured access log path" + declare APP="$1" + local OPENRESTY_LOG_ROOT="$(fn-openresty-log-root)" + + fn-plugin-property-get-default "openresty" "$APP" "access-log-path" "${OPENRESTY_LOG_ROOT}/${APP}-access.log" +} + +fn-openresty-bind-address-ipv4() { + declare desc="get the configured ipv4 bind address" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "bind-address-ipv4" "" +} + +fn-openresty-bind-address-ipv6() { + declare desc="get the configured ipv6 bind address" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "bind-address-ipv6" "::" +} + +fn-openresty-client-max-body-size() { + declare desc="get the configured client max body size" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "client-max-body-size" "" +} + +fn-openresty-get-http-includes-dir() { + declare desc="get any include dir if available" + declare APP="$1" + + if [[ -d "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID.missing" ]]; then + return + fi + + if [[ -d "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID" ]]; then + echo "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes.$DOKKU_PID" + elif [[ -d "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes" ]]; then + echo "${DOKKU_LIB_ROOT}/data/openresty-vhosts/app-$APP/openresty-http-includes" + fi +} + +fn-openresty-hsts-include-subdomains() { + declare APP="$1" + fn-plugin-property-get-default "openresty" "$APP" "hsts-include-subdomains" "true" +} + +fn-openresty-hsts-max-age() { + declare APP="$1" + fn-plugin-property-get-default "openresty" "$APP" "hsts-max-age" "15724800" +} + +fn-openresty-hsts-preload() { + declare APP="$1" + fn-plugin-property-get-default "openresty" "$APP" "hsts-preload" "false" +} + +fn-openresty-hsts-is-enabled() { + declare APP="$1" + local hsts_is_enabled="$(fn-plugin-property-get-default "openresty" "$APP" "hsts" "")" + + if [[ "$hsts_is_enabled" == "" ]]; then + hsts_is_enabled="$(fn-plugin-property-get-default "openresty" "--global" "hsts" "true")" + fi + + echo "$hsts_is_enabled" +} + +fn-openresty-image() { + fn-plugin-property-get-default "openresty" "--global" "image" "dokku/openresty-docker-proxy:0.5.6" +} + +fn-openresty-letsencrypt-email() { + fn-plugin-property-get-default "openresty" "--global" "letsencrypt-email" "" +} + +fn-openresty-letsencrypt-server() { + fn-plugin-property-get-default "openresty" "--global" "letsencrypt-server" "https://acme-v02.api.letsencrypt.org/directory" +} + +fn-openresty-log-root() { + declare desc="get the openresty log root" + local OPENRESTY_LOG_ROOT="/var/log/openresty" + echo "$OPENRESTY_LOG_ROOT" +} + +fn-openresty-logs() { + declare desc="shows the logs for the openresty container" + declare TAIL="$1" NUM="$2" + local dokku_logs_args=("--tail" "$NUM") + + if [[ "$TAIL" == "true" ]]; then + dokku_logs_args+=("--follow") + fi + + "$DOCKER_BIN" logs openresty-openresty-1 "${dokku_logs_args[@]}" +} + +fn-openresty-logs-usage() { + declare desc="logs specific usage" + echo "Usage: dokku openresty:logs" + echo " display recent openresty log output" + echo "" + echo " -n, --num NUM # the number of lines to display" + echo " -t, --tail # continually stream logs" +} + +fn-openresty-proxy-buffer-size() { + declare desc="get the configured proxy buffer size" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-buffer-size" "$(fn-get-pagesize)" +} + +fn-openresty-proxy-buffering() { + declare desc="get the configured proxy buffering" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-buffering" "on" +} + +fn-openresty-proxy-buffers() { + declare desc="get the configured proxy buffers" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-buffers" "8 $(fn-get-pagesize)" +} + +fn-openresty-proxy-busy-buffers-size() { + declare desc="get the configured proxy busy buffers size" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-busy-buffers-size" "$(($(fn-get-pagesize) * 2))" +} + +fn-openresty-proxy-connect-timeout() { + declare desc="get the configured proxy connect timeout" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-connect-timeout" "60s" +} + +fn-openresty-proxy-read-timeout() { + declare desc="get the configured proxy read timeout" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-read-timeout" "60s" +} + +fn-openresty-proxy-send-timeout() { + declare desc="get the configured proxy send timeout" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "proxy-send-timeout" "60s" +} + +fn-openresty-send-timeout() { + declare desc="get the configured proxy send timeout" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "send-timeout" "60s" +} + +fn-openresty-template-compose-file() { + declare desc="templates out the compose file" + declare OUTPUT_PATH="$1" + local COMPOSE_TEMPLATE="$PLUGIN_AVAILABLE_PATH/openresty-vhosts/templates/compose.yml.sigil" + + CUSTOM_COMPOSE_TEMPLATE="$(plugn trigger openresty-template-source "$APP")" + if [[ -n "$CUSTOM_COMPOSE_TEMPLATE" ]]; then + COMPOSE_TEMPLATE="$CUSTOM_COMPOSE_TEMPLATE" + fi + + local SIGIL_PARAMS=(OPENRESTY_DATA_DIR="${DOKKU_LIB_ROOT}/data/openresty/.docker-letsencrypt" + OPENRESTY_IMAGE="$(fn-openresty-image)" + OPENRESTY_LETSENCRYPT_EMAIL="$(fn-openresty-letsencrypt-email)" + OPENRESTY_LETSENCRYPT_SERVER="$(fn-openresty-letsencrypt-server)") + + sigil -f "$COMPOSE_TEMPLATE" "${SIGIL_PARAMS[@]}" | cat -s >"$OUTPUT_PATH" +} + +fn-openresty-x-forwarded-for-value() { + declare desc="get the configured x-forwarded-for value" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "x-forwarded-for-value" "\$remote_addr" +} + +fn-openresty-x-forwarded-port-value() { + declare desc="get the configured x-forwarded-port value" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "x-forwarded-port-value" "\$server_port" +} + +fn-openresty-x-forwarded-proto-value() { + declare desc="get the configured x-forwarded-proto value" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "x-forwarded-proto-value" "\$scheme" +} + +fn-openresty-x-forwarded-ssl() { + declare desc="get the configured x-forwarded-ssl value" + declare APP="$1" + + fn-plugin-property-get-default "openresty" "$APP" "x-forwarded-ssl" "" +} diff --git a/plugins/openresty-vhosts/plugin.toml b/plugins/openresty-vhosts/plugin.toml new file mode 100644 index 00000000000..d02688cf18f --- /dev/null +++ b/plugins/openresty-vhosts/plugin.toml @@ -0,0 +1,4 @@ +[plugin] +description = "dokku core openresty-vhosts plugin" +version = "0.30.10" +[plugin.config] diff --git a/plugins/openresty-vhosts/pre-restore b/plugins/openresty-vhosts/pre-restore new file mode 100755 index 00000000000..8903a23ae7f --- /dev/null +++ b/plugins/openresty-vhosts/pre-restore @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +trigger-openresty-pre-restore() { + declare desc="pre-restore the openresty proxy" + declare trigger="install" + + if [[ "$(fn-plugin-property-get "openresty" "--global" "proxy-status")" != "started" ]]; then + return + fi + + if ! cmd-openresty-start; then + dokku_log_warn "Failed to restore openresty proxy, requests may not route as expected" + fi +} + +trigger-openresty-pre-restore "$@" diff --git a/plugins/openresty-vhosts/report b/plugins/openresty-vhosts/report new file mode 100755 index 00000000000..05081e23a6b --- /dev/null +++ b/plugins/openresty-vhosts/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-report-single "$@" diff --git a/plugins/openresty-vhosts/subcommands/default b/plugins/openresty-vhosts/subcommands/default new file mode 100755 index 00000000000..0d187aa0c77 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/default @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/help-functions" + +cmd-openresty-help "openresty:help" diff --git a/plugins/openresty-vhosts/subcommands/logs b/plugins/openresty-vhosts/subcommands/logs new file mode 100755 index 00000000000..ff8a9fe66fe --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/logs @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-logs "$@" diff --git a/plugins/openresty-vhosts/subcommands/report b/plugins/openresty-vhosts/subcommands/report new file mode 100755 index 00000000000..5bf0b5380c6 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/report @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-report "$@" diff --git a/plugins/openresty-vhosts/subcommands/set b/plugins/openresty-vhosts/subcommands/set new file mode 100755 index 00000000000..079e0019e95 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/set @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x + +cmd-openresty-set() { + declare desc="set or clear an openresty property for an app" + declare cmd="openresty:set" + [[ "$1" == "$cmd" ]] && shift 1 + declare APP="$1" KEY="$2" VALUE="$3" + local VALID_KEYS=("access-log-format" "access-log-path" "bind-address-ipv4" "bind-address-ipv6" "client-max-body-size" "error-log-path" "hsts" "hsts-include-subdomains" "hsts-preload" "hsts-max-age" "image" "log-level" "letsencrypt-email" "letsencrypt-server" "proxy-read-timeout" "proxy-buffer-size" "proxy-buffering" "proxy-buffers" "proxy-busy-buffers-size" "x-forwarded-for-value" "x-forwarded-port-value" "x-forwarded-proto-value" "x-forwarded-ssl") + local GLOBAL_KEYS=("image" "log-level" "letsencrypt-email" "letsencrypt-server") + + [[ -z "$KEY" ]] && dokku_log_fail "No key specified" + + if ! fn-in-array "$KEY" "${VALID_KEYS[@]}"; then + dokku_log_fail "Invalid key specified, valid keys include: access-log-format, access-log-path, bind-address-ipv4, bind-address-ipv6, client-max-body-size, error-log-path, hsts, hsts-include-subdomains, hsts-preload, hsts-max-age, image, log-level, letsencrypt-email, letsencrypt-server, proxy-read-timeout, proxy-buffer-size, proxy-buffering, proxy-buffers, proxy-busy-buffers-size, x-forwarded-for-value, x-forwarded-port-value, x-forwarded-proto-value, x-forwarded-ssl" + fi + + if ! fn-in-array "$KEY" "${GLOBAL_KEYS[@]}"; then + if [[ "$APP" == "--global" ]]; then + dokku_log_fail "The key '$KEY' cannot be set globally" + fi + verify_app_name "$APP" + fi + + if [[ -n "$VALUE" ]]; then + dokku_log_info2_quiet "Setting ${KEY} to ${VALUE}" + fn-plugin-property-write "openresty" "$APP" "$KEY" "$VALUE" + else + dokku_log_info2_quiet "Unsetting ${KEY}" + fn-plugin-property-delete "openresty" "$APP" "$KEY" + fi +} + +cmd-openresty-set "$@" diff --git a/plugins/openresty-vhosts/subcommands/show-config b/plugins/openresty-vhosts/subcommands/show-config new file mode 100755 index 00000000000..98b6e4b83ae --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/show-config @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" + +cmd-openresty-show-config "$@" diff --git a/plugins/openresty-vhosts/subcommands/start b/plugins/openresty-vhosts/subcommands/start new file mode 100755 index 00000000000..7f42b198474 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/start @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" + +cmd-openresty-start "$@" diff --git a/plugins/openresty-vhosts/subcommands/stop b/plugins/openresty-vhosts/subcommands/stop new file mode 100755 index 00000000000..308149693b7 --- /dev/null +++ b/plugins/openresty-vhosts/subcommands/stop @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_AVAILABLE_PATH/openresty-vhosts/command-functions" + +cmd-openresty-stop "$@" diff --git a/plugins/openresty-vhosts/templates/compose.yml.sigil b/plugins/openresty-vhosts/templates/compose.yml.sigil new file mode 100644 index 00000000000..36e2fd5e2bf --- /dev/null +++ b/plugins/openresty-vhosts/templates/compose.yml.sigil @@ -0,0 +1,27 @@ +--- +version: "3.7" + +services: + openresty: + image: "{{ $.OPENRESTY_IMAGE }}" + + environment: + - OPENRESTY_LABEL_PREFIX=openresty. + {{ if $.OPENRESTY_LETSENCRYPT_EMAIL }} + - OPENRESTY_LETSENCRYPT_EMAIL={{ $.OPENRESTY_LETSENCRYPT_EMAIL }}" + - OPENRESTY_LETSENCRYPT_CA={{ $.OPENRESTY_LETSENCRYPT_SERVER }} + {{ end }} + + network_mode: bridge + + ports: + - "80:80" + {{ if $.OPENRESTY_LETSENCRYPT_EMAIL }} + - "443:443" + {{ end }} + + restart: unless-stopped + + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - "{{ $.OPENRESTY_DATA_DIR }}:/etc/resty-auto-ssl" diff --git a/plugins/ports/functions.go b/plugins/ports/functions.go index 84216613615..7bea56ec3d4 100644 --- a/plugins/ports/functions.go +++ b/plugins/ports/functions.go @@ -30,9 +30,13 @@ func clearPorts(appName string) error { } func doesCertExist(appName string) bool { - b, _ := common.PlugnTriggerOutput("certs-exists", []string{appName}...) - certsExists := strings.TrimSpace(string(b[:])) - return certsExists == "true" + certsExists, _ := common.PlugnTriggerOutputAsString("certs-exists", []string{appName}...) + if certsExists == "true" { + return true + } + + certsForce, _ := common.PlugnTriggerOutputAsString("certs-force", []string{appName}...) + return certsForce == "true" } func filterAppPortMaps(appName string, scheme string, hostPort int) []PortMap { diff --git a/plugins/traefik-vhosts/certs-force b/plugins/traefik-vhosts/certs-force new file mode 100755 index 00000000000..665717badb1 --- /dev/null +++ b/plugins/traefik-vhosts/certs-force @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eo pipefail +[[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +source "$PLUGIN_AVAILABLE_PATH/traefik-vhosts/internal-functions" + +trigger-traefik-vhosts-certs-force() { + declare desc="traefik-vhosts certs-force plugin trigger" + declare trigger="certs-force" + declare APP="$1" + + if [[ "$(plugn trigger proxy-type "$APP")" != "traefik" ]]; then + return + fi + + if [[ -n "$(fn-traefik-letsencrypt-email)" ]]; then + echo true + fi +} + +trigger-traefik-vhosts-certs-force "$@" diff --git a/tests/unit/caddy.bats b/tests/unit/caddy.bats index 76a7c370eee..d48d41196d9 100644 --- a/tests/unit/caddy.bats +++ b/tests/unit/caddy.bats @@ -145,5 +145,5 @@ teardown() { run /bin/bash -c "dokku --quiet ports:report $TEST_APP --ports-map-detected" echo "output: $output" echo "status: $status" - assert_output "http:80:5000" + assert_output "http:80:5000 https:443:5000" } diff --git a/tests/unit/haproxy.bats b/tests/unit/haproxy.bats index 31c78173089..dc4f0921829 100644 --- a/tests/unit/haproxy.bats +++ b/tests/unit/haproxy.bats @@ -162,5 +162,5 @@ teardown() { run /bin/bash -c "dokku --quiet ports:report $TEST_APP --ports-map-detected" echo "output: $output" echo "status: $status" - assert_output "http:80:5000" + assert_output "http:80:5000 https:443:5000" } diff --git a/tests/unit/openresty.bats b/tests/unit/openresty.bats new file mode 100644 index 00000000000..f188bbedce5 --- /dev/null +++ b/tests/unit/openresty.bats @@ -0,0 +1,189 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + global_setup + dokku nginx:stop + dokku openresty:set --global letsencrypt-server https://acme-staging-v02.api.letsencrypt.org/directory + dokku openresty:set --global letsencrypt-email + dokku openresty:start + create_app +} + +teardown() { + global_teardown + destroy_app + dokku openresty:stop + dokku nginx:start +} + +@test "(openresty) openresty:help" { + run /bin/bash -c "dokku openresty" + echo "output: $output" + echo "status: $status" + assert_output_contains "Manage the openresty proxy integration" + help_output="$output" + + run /bin/bash -c "dokku openresty:help" + echo "output: $output" + echo "status: $status" + assert_output_contains "Manage the openresty proxy integration" + assert_output "$help_output" +} + +@test "(openresty) single domain" { + run /bin/bash -c "dokku proxy:set $TEST_APP openresty" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@dokku.me:$TEST_APP convert_to_dockerfile + echo "output: $output" + echo "status: $status" + assert_success + + assert_http_localhost_success "http" "$TEST_APP.dokku.me" "80" "" "python/http.server" +} + +@test "(openresty) multiple domains" { + run /bin/bash -c "dokku proxy:set $TEST_APP openresty" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku domains:add $TEST_APP $TEST_APP.dokku.me" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku domains:add $TEST_APP $TEST_APP-2.dokku.me" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@dokku.me:$TEST_APP convert_to_dockerfile + echo "output: $output" + echo "status: $status" + assert_success + + assert_http_localhost_success "http" "$TEST_APP.dokku.me" "80" "" "python/http.server" + assert_http_localhost_success "http" "$TEST_APP-2.dokku.me" "80" "" "python/http.server" +} + +@test "(openresty) ssl" { + run /bin/bash -c "dokku builder-herokuish:set $TEST_APP allowed true" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku proxy:set $TEST_APP openresty" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app + echo "output: $output" + echo "status: $status" + assert_success + assert_http_localhost_success "http" "$TEST_APP.dokku.me" "80" "" "python/http.server" + + run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "http:80:5000" + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"openresty.letsencrypt\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "false" + + run /bin/bash -c "dokku openresty:set --global letsencrypt-email test@example.com" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku openresty:stop" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku openresty:start" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku ps:rebuild $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku ps:inspect $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"openresty.letsencrypt\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "true" + + run /bin/bash -c "dokku ports:report $TEST_APP --ports-map-detected" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "http:80:5000 https:443:5000" +} + +@test "(openresty) includes" { + run /bin/bash -c "dokku proxy:set $TEST_APP openresty" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@dokku.me:$TEST_APP add_openresty_include + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku ps:inspect $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker inspect $TEST_APP.web.1 --format '{{ index .Config.Labels \"openresty.include-http-example.conf\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "IyBmb3JjZSB0aGUgY2hhcmFjdGVyIHNldCB0byB1dGYtOApjaGFyc2V0IFVURi04Owo=" + + run /bin/bash -c "docker logs openresty-openresty-1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker exec openresty-openresty-1 /usr/local/openresty/nginx/sbin/nginx -t" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker exec openresty-openresty-1 cat /etc/nginx/sites-enabled/sites.conf" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "force the character set to utf-8" + assert_output_contains "charset UTF-8;" +} + +add_openresty_include() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + mkdir -p "$APP_REPO_DIR/openresty-http-includes" + touch "$APP_REPO_DIR/openresty-http-includes/example.conf" + echo "# force the character set to utf-8" >>"$APP_REPO_DIR/openresty-http-includes/example.conf" + echo "charset UTF-8;" >>"$APP_REPO_DIR/openresty-http-includes/example.conf" +} diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index c37496fe8d9..6cdca2e2ab2 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -223,12 +223,19 @@ assert_http_success() { } assert_http_localhost_success() { - local scheme="$1" domain="$2" port="${3:-80}" path="${4:-}" + local scheme="$1" domain="$2" port="${3:-80}" path="${4:-}" content="${5:-}" run curl --connect-to "$domain:$port:localhost:$port" -kSso /dev/null -w "%{http_code}" "$scheme://$domain:$port$path" echo "curl: curl --connect-to $domain:$port:localhost:$port -kSso /dev/null -w %{http_code} $scheme://$domain:$port$path" echo "output: $output" echo "status: $status" assert_output "200" + + if [[ -n "$content" ]]; then + run curl --connect-to "$domain:$port:localhost:$port" -kSs "$scheme://$domain:$port$path" + echo "output: $output" + echo "status: $status" + assert_output "$content" + fi } assert_ssl_domain() { diff --git a/tests/unit/traefik.bats b/tests/unit/traefik.bats index 0ed0ec274a7..835b974cda1 100644 --- a/tests/unit/traefik.bats +++ b/tests/unit/traefik.bats @@ -241,7 +241,7 @@ teardown() { run /bin/bash -c "dokku --quiet ports:report $TEST_APP --ports-map-detected" echo "output: $output" echo "status: $status" - assert_output "http:80:5000" + assert_output "http:80:5000 https:443:5000" } @test "(traefik) show-config without auth set" {