diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 6df2401..ea97a17 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -4,6 +4,10 @@ on: pull_request: branches: - 'main' + paths: + - 'Dockerfile' + - 'src/*.sh' + - 'tests/*.sh' workflow_dispatch: jobs: diff --git a/README.md b/README.md index 567f33e..4422d62 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ docker service create \ shizunge/gantry ``` -Or with docker compose, see the [example](examples/README.md). +The [examples folder](examples/README.md) contains example docker compose files, and more methods to launch *Gantry*, like [at a specific time](examples/cronjob) and [via webhook](examples/webhook). You can also run *Gantry* as a script directly on the host outside the container ``` @@ -36,14 +36,14 @@ You can also run *Gantry* as a script directly on the host outside the container You can configure the most behaviors of *Gantry* via environment variables. -### Common ones +### Common | Environment Variable | Default |Description | |-----------------------|---------|------------| | GANTRY_LOG_LEVEL | INFO | Control how many logs generated by *Gantry*. Valid values are `NONE`, `ERROR`, `WARN`, `INFO`, `DEBUG`. | | GANTRY_NODE_NAME | | Add node name to logs. If not set, *Gantry* will use the host name of the Docker Swarm's manager, which is read from either the Docker daemon socket of current node or `DOCKER_HOST`. | -| GANTRY_POST_RUN_CMD | | Command(s) to `eval` after each updating iteration. | -| GANTRY_PRE_RUN_CMD | | Command(s) to `eval` before each updating iteration. | +| GANTRY_POST_RUN_CMD | | Command(s) to `eval` after each updating iteration. For [example](examples/prune-and-watchtower), you can use this to remove unused containers, networks and images and update standalone docker containers. | +| GANTRY_PRE_RUN_CMD | | Command(s) to `eval` before each updating iteration. For [example](examples/prune-and-watchtower), you can use this to remove unused containers, networks and images and update standalone docker containers. | | GANTRY_SLEEP_SECONDS | 0 | Interval between two updates. Set it to 0 to run *Gantry* once and then exit. When this is a non-zero value, after an updating, *Gantry* will sleep until the next scheduled update. The actual sleep time is this value minus time spent on updating services. | | TZ | | Set timezone for time in logs. | @@ -68,34 +68,34 @@ You can configure the most behaviors of *Gantry* via environment variables. | Environment Variable | Default | Description | |-----------------------|---------|-------------| | GANTRY_SERVICES_EXCLUDED | | A space separated list of services names that are excluded from updating. | -| GANTRY_SERVICES_EXCLUDED_FILTERS | `label=gantry.services.excluded=true` | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter), e.g. `label=project=project-a`. Exclude services which match the given filters from updating. Note that multiple filters will be logical **ANDED**. The default value allows you to add label `gantry.services.excluded=true` to services to exclude them from updating. | -| GANTRY_SERVICES_FILTERS | | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter) that are accepted by `docker service ls --filter` to select services to update, e.g. `label=project=project-a`. Note that multiple filters will be logical **ANDED**. | +| GANTRY_SERVICES_EXCLUDED_FILTERS | `label=gantry.services.excluded=true` | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter), e.g. `label=project=project-a`. Exclude services which match the given filters from updating. The default value allows you to add label `gantry.services.excluded=true` to services to exclude them from updating. Note that multiple filters will be logical **ANDED**. | +| GANTRY_SERVICES_FILTERS | | A space separated list of [filters](https://docs.docker.com/engine/reference/commandline/service_ls/#filter) that are accepted by `docker service ls --filter` to select services to update, e.g. `label=project=project-a`. Note that multiple filters will be logical **ANDED**. Also see [How to filters multiple services by name](docs/faq.md#how-to-filters-multiple-services-by-name). | | GANTRY_SERVICES_SELF | | This is optional. When running as a docker service, *Gantry* will try to find the service name of itself automatically, and update itself firstly. The manifest inspection will be always performed on the *Gantry* service to avoid an infinity loop of updating itself. This can be used to ask *Gantry* to update another service firstly. | ### To check if new images are available | Environment Variable | Default | Description | |-----------------------|---------|-------------| -| GANTRY_MANIFEST_CMD | buildx | Valid values are `buildx`, `manifest`, and `none`.
Set which command for manifest inspection. Also see FAQ section [when to set `GANTRY_MANIFEST_CMD`](docs/faq.md#when-to-set-gantry_manifest_cmd).Set to `none` to skip checking the manifest. As a result of skipping, `docker service update` always runs. In case you add `--force` to `GANTRY_UPDATE_OPTIONS`, you also want to disable the inspection. | +| GANTRY_MANIFEST_CMD | buildx | Valid values are `buildx`, `manifest`, and `none`.
Set which command for manifest inspection. Also see FAQ section [when to set `GANTRY_MANIFEST_CMD`](docs/faq.md#when-to-set-gantry_manifest_cmd).Set to `none` to skip checking the manifest. As a result of skipping, `docker service update` always runs. In case you add `--force` to `GANTRY_UPDATE_OPTIONS`, you also want to disable the inspection. You can apply a different value to a particular service via [labels](#labels). | | GANTRY_MANIFEST_NUM_WORKERS | 1 | The maximum number of `GANTRY_MANIFEST_CMD` that can run in parallel. | -| GANTRY_MANIFEST_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/buildx_imagetools_inspect/#options) added to the `docker buildx imagetools inspect` or [options](https://docs.docker.com/engine/reference/commandline/manifest_inspect/#options) to `docker manifest inspect`, depending on `GANTRY_MANIFEST_CMD` value, for all services. Also see [Labels](#labels) about adding options to a particular service. | +| GANTRY_MANIFEST_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/buildx_imagetools_inspect/#options) added to the `docker buildx imagetools inspect` or [options](https://docs.docker.com/engine/reference/commandline/manifest_inspect/#options) to `docker manifest inspect`, depending on `GANTRY_MANIFEST_CMD` value, for all services. You can apply a different value to a particular service via [labels](#labels). | ### To add options to services update | Environment Variable | Default | Description | |-----------------------|---------|-------------| -| GANTRY_ROLLBACK_ON_FAILURE | true | Set to `true` to enable rollback when updating fails. Set to `false` to disable the rollback. | -| GANTRY_ROLLBACK_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/service_update/#options) added to the `docker service update --rollback` command for all services. Also see [Labels](#labels) about adding options to a particular service. | -| GANTRY_UPDATE_JOBS | false | Set to `true` to update replicated-job or global-job. Set to `false` to disable updating jobs. *Gantry* adds additional options to `docker service update` when there is [no running tasks](docs/faq.md#how-to-update-services-with-no-running-tasks). | +| GANTRY_ROLLBACK_ON_FAILURE | true | Set to `true` to enable rollback when updating fails. Set to `false` to disable the rollback. You can apply a different value to a particular service via [labels](#labels). | +| GANTRY_ROLLBACK_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/service_update/#options) added to the `docker service update --rollback` command for all services. You can apply a different value to a particular service via [labels](#labels). | +| GANTRY_UPDATE_JOBS | false | Set to `true` to update replicated-job or global-job. Set to `false` to disable updating jobs. *Gantry* adds additional options to `docker service update` when there is [no running tasks](docs/faq.md#how-to-update-services-with-no-running-tasks). You can apply a different value to a particular service via [labels](#labels). | | GANTRY_UPDATE_NUM_WORKERS | 1 | The maximum number of updates that can run in parallel. | -| GANTRY_UPDATE_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/service_update/#options) added to the `docker service update` command for all services. Also see [Labels](#labels) about adding options to a particular service. | -| GANTRY_UPDATE_TIMEOUT_SECONDS | 300 | Error out if updating of a single service takes longer than the given time. | +| GANTRY_UPDATE_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/service_update/#options) added to the `docker service update` command for all services. You can apply a different value to a particular service via [labels](#labels). | +| GANTRY_UPDATE_TIMEOUT_SECONDS | 300 | Error out if updating of a single service takes longer than the given time. You can apply a different value to a particular service via [labels](#labels). | ### After updating | Environment Variable | Default | Description | |-----------------------|---------|-------------| -| GANTRY_CLEANUP_IMAGES | true | Set to `true` to clean up the updated images. Set to `false` to disable the cleanup. Before cleaning up, *Gantry* will try to remove any *exited* and *dead* containers that are using the images. | +| GANTRY_CLEANUP_IMAGES | true | Set to `true` to clean up the updated images on all hosts. Set to `false` to disable the cleanup. Before cleaning up, *Gantry* will try to remove any *exited* and *dead* containers that are using the images. | | GANTRY_CLEANUP_IMAGES_OPTIONS | | [Options](https://docs.docker.com/engine/reference/commandline/service_create/#options) added to the `docker service create` command to create a global job for images removal. You can use this to add a label to the service or the containers. | | GANTRY_NOTIFICATION_APPRISE_URL | | Enable notifications on service update with [apprise](https://github.com/caronc/apprise-api). This must point to the notification endpoint (e.g. `http://apprise:8000/notify`) | | GANTRY_NOTIFICATION_CONDITION | all | Valid values are `all` and `on-change`. Specifies the conditions under which notifications are sent. Set to `all` to send notifications every run. Set to `on-change` to send notifications only when there are updates or errors. | @@ -130,8 +130,8 @@ You need to tell *Gantry* to use a named config rather than the default one when Labels can be added to services to modify the behavior of *Gantry* for particular services. When *Gantry* sees the following labels on a service, it will modify the Docker command line only for that service. The value on the label overrides the global environment variables. -| Labels | Description | -|---------|-------------| +| Label | Description | +|--------|-------------| | `gantry.auth.config=` | See [Authentication](#authentication). | | `gantry.services.excluded=true` | Exclude the services from updating if you are using the default [`GANTRY_SERVICES_EXCLUDED_FILTERS`](#to-select-services). | | `gantry.manifest.cmd=` | Override [`GANTRY_MANIFEST_CMD`](#to-check-if-new-images-are-available) | diff --git a/examples/README.md b/examples/README.md index 9f21e18..ace9594 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,9 +2,12 @@ ## [cronjob](./cronjob) -Run *gantry* at the specific time. +Run *Gantry* at a specific time. ## [prune-and-watchtower](./prune-and-watchtower) Remove unused containers, networks and images. Update standalone docker containers. Update docker swarm services. +## [webhook](./webhook) + +Launch *Gantry* via webhook. diff --git a/examples/cronjob/README.md b/examples/cronjob/README.md index 3ef2def..dcb1cff 100644 --- a/examples/cronjob/README.md +++ b/examples/cronjob/README.md @@ -1,4 +1,4 @@ # cronjob -Use [*swarm-cronjob*](https://github.com/crazy-max/swarm-cronjob) to launch *gantry* at a given time. +Use [*swarm-cronjob*](https://github.com/crazy-max/swarm-cronjob) to launch [*Gantry*](https://github.com/shizunge/gantry) at a specific time. diff --git a/examples/webhook/README.md b/examples/webhook/README.md new file mode 100644 index 0000000..49319ab --- /dev/null +++ b/examples/webhook/README.md @@ -0,0 +1,31 @@ +# webhook + +This example describes how to launch [*Gantry*](https://github.com/shizunge/gantry) via [adnanh/webhook](https://github.com/adnanh/webhook). + +## Setup + +We leverage a dockerized webhook image [lwlook/webhook](https://hub.docker.com/r/lwlook/webhook) which is based on the offical Docker image. This allows us to launch the *Gantry* service with simple docker commands. + +[hooks.json](./hooks.json) defines the webhook's behavior. It parses incoming payloads and transforms them into environment variables like `GANTRY_SERVICES_EXCLUDED`, `GANTRY_SERVICES_EXCLUDED_FILTERS` and `GANTRY_SERVICES_FILTERS`. These variables are then used by [run_gantry.sh](./run_gantry.sh) to control *Gantry* behaviors. which means you can update different services by passing different payloads to the webhook. Refer to the [adnanh/webhook](https://github.com/adnanh/webhook) repository for more advanced webhook configurations, including securing the webhook with `trigger-rule`. + +[run_gantry.sh](./run_gantry.sh) is responsible for launching the *Gantry* service. + +## Test + +Use the following command to deploy the Docker Compose stack that includes the webhook service. + +``` +docker stack deploy --detach=true --prune --with-registry-auth --compose-file ./docker-compose.yml webhook +``` + +Use curl to send a POST request to the webhook endpoint. This request tells the *Gantry* to only update the service named "webhook_webhook". + +``` +curl -X POST localhost:9000/hooks/run-gantry -H "Content-Type: application/json" -d '{"GANTRY_SERVICES_FILTERS":"name=webhook_webhook"}' +``` + +Check the webhook service logs to confirm if the webhook was triggered correctly. + +``` +docker service logs webhook_webhook +``` diff --git a/examples/webhook/docker-compose.yml b/examples/webhook/docker-compose.yml new file mode 100644 index 0000000..c591982 --- /dev/null +++ b/examples/webhook/docker-compose.yml @@ -0,0 +1,60 @@ +version: "3.8" + +services: + webhook: + image: lwlook/webhook:latest + command: + - -verbose + - -hooks=/hooks.json + - -hotreload + ports: + - "9000:9000" + configs: + - source: hooks_json + target: /hooks.json + - source: run_gantry + target: /run_gantry.sh + mode: 0550 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + deploy: + placement: + constraints: + - node.role==manager + + # Note: run_gantry.sh does not use this service by default. + # This service is left here to demonstrate a potential approach for reusing the same service + # by scaling its replicas instead of starting a new service each webhook request. + # See function resume_gantry in run_gantry.sh. + # Pros: + # * This approach can work together with other launching methods like crazymax/swarm-cronjob. + # Cons: + # * Concurrency Issues: Sending webhook requests too frequently can increase the chance of the + # webhook failing to launch Gantry correctly for some requests due to the existing service + # potentially handling a previous command. + gantry: + image: shizunge/gantry:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - "GANTRY_NODE_NAME={{.Node.Hostname}}" + - "GANTRY_SLEEP_SECONDS=0" + deploy: + replicas: 0 + placement: + constraints: + - node.role==manager + restart_policy: + condition: none + labels: + # The label can be used to find this service. + # This can be used together with resume_gantry function in the run_gantry.sh + - webhook.run-gantry=true + +configs: + hooks_json: + name: hooks_json + file: ./hooks.json + run_gantry: + name: run_gantry + file: ./run_gantry.sh diff --git a/examples/webhook/hooks.json b/examples/webhook/hooks.json new file mode 100644 index 0000000..5ebc022 --- /dev/null +++ b/examples/webhook/hooks.json @@ -0,0 +1,25 @@ +[ + { + "id": "run-gantry", + "execute-command": "/run_gantry.sh", + "command-working-directory": "/", + "pass-environment-to-command": + [ + { + "source": "payload", + "name": "GANTRY_SERVICES_EXCLUDED", + "envname": "GANTRY_SERVICES_EXCLUDED" + }, + { + "source": "payload", + "name": "GANTRY_SERVICES_EXCLUDED_FILTERS", + "envname": "GANTRY_SERVICES_EXCLUDED_FILTERS" + }, + { + "source": "payload", + "name": "GANTRY_SERVICES_FILTERS", + "envname": "GANTRY_SERVICES_FILTERS" + } + ] + } +] \ No newline at end of file diff --git a/examples/webhook/run_gantry.sh b/examples/webhook/run_gantry.sh new file mode 100644 index 0000000..a06c8b9 --- /dev/null +++ b/examples/webhook/run_gantry.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# Copyright (C) 2024 Shizun Ge +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +_docker_start_replicated_job() { + local args="${*}" + if [ -z "${args}" ]; then + echo "No services set." + echo "Services that are not running:" + docker service ls | grep "0/" + return 0 + fi + for S in ${args}; do + echo -n "Set replicas to 0 to ${S}: " + docker service update --replicas=0 "${S}" + echo -n "Set replicas to 1 to ${S}: " + docker service update --detach --replicas=1 "${S}" + done +} + +_get_number_of_running_tasks() { + local filter="${1}" + local replicas= + if ! replicas=$(docker service ls --filter "${filter}" --format '{{.Replicas}}' | head -n 1); then + return 1 + fi + # https://docs.docker.com/engine/reference/commandline/service_ls/#examples + # The REPLICAS is like "5/5" or "1/1 (3/5 completed)" + # Get the number before the first "/". + local num_runs= + num_runs=$(echo "${replicas}/" | cut -d '/' -f 1) + echo "${num_runs}" +} + +resume_gantry() { + local filter="label=webhook.run-gantry=true" + local service_name= + service_name=$(docker service ls --filter "${filter}" --format "{{.Name}}" | head -n 1) + if [ -z "${service_name}" ]; then + echo "Cannot find a service from ${filter}." + return 1 + fi + local replicas= + if ! replicas=$(_get_number_of_running_tasks "${filter}"); then + echo "Failed to obtain task states of service from ${filter}." + return 1 + fi + if [ "${replicas}" != "0" ]; then + echo "${service_name} is still running. There are ${replicas} running tasks." + return 1 + fi + docker service update --detach --env-add "GANTRY_SERVICES_EXCLUDED=${GANTRY_SERVICES_EXCLUDED:-}" "${service_name}" + docker service update --detach --env-add "GANTRY_SERVICES_EXCLUDED_FILTERS=${GANTRY_SERVICES_EXCLUDED_FILTERS:-}" "${service_name}" + docker service update --detach --env-add "GANTRY_SERVICES_FILTERS=${GANTRY_SERVICES_FILTERS:-}" "${service_name}" + _docker_start_replicated_job "${service_name}" +} + +launch_new_gantry() { + local service_name= + service_name="gantry-$(date +%s)" + docker service create \ + --name "${service_name}" \ + --mode replicated-job \ + --constraint "node.role==manager" \ + --env "GANTRY_SERVICES_EXCLUDED=${GANTRY_SERVICES_EXCLUDED:-}" \ + --env "GANTRY_SERVICES_EXCLUDED_FILTERS=${GANTRY_SERVICES_EXCLUDED_FILTERS:-}" \ + --env "GANTRY_SERVICES_FILTERS=${GANTRY_SERVICES_FILTERS:-}" \ + --label "from-webhook=true" \ + --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ + shizunge/gantry + local return_value=$? + docker service logs --raw "${service_name}" + docker service rm "${service_name}" + return "${return_value}" +} + +main() { + launch_new_gantry "${@}" +} + +main "${@}"