├── .gitattributes ├── renovate.json ├── compose-cd-cleanup.service ├── compose-cd-cleanup.timer ├── compose-cd.timer ├── compose-cd.service ├── set_version.sh ├── LICENSE ├── DESIGN.md ├── README.md └── compose-cd /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/ export-ignore 2 | test/ export-ignore 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /compose-cd-cleanup.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=compose-cd cleanup 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/compose-cd cleanup 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /compose-cd-cleanup.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=compose-cd cleanup timer 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Unit=compose-cd-cleanup.service 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /compose-cd.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Continuous Deployment for docker-compose 3 | 4 | [Timer] 5 | OnUnitActiveSec=1min 6 | Unit=compose-cd.service 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /compose-cd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Continuous Deployment for docker-compose 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart=/usr/bin/compose-cd update 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /set_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | new_ver="$1" 4 | sed -i -e "s/compose_cd_ver=\".*\"/compose_cd_ver=\"${new_ver}\"/g" compose-cd 5 | git status 6 | git add compose-cd 7 | git commit -m "release: ${new_ver}" 8 | git tag "${new_ver}" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 sksat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design Doc: compose-cd 2 | 3 | ## 背景 4 | 5 | - CI/CD(Continuous Integration/Deployment)は最高 6 | - ここでの雰囲気は以下 7 | - CI: 全部Git管理してcommit毎に色々検証 8 | - これでPull Request上でマズいものはチェックする 9 | - CD: HEADのものを自動でどーんとデプロイ 10 | - k8s+ArgoCDによる最高の体験 11 | - `docker-compose` でもContinuous Deploymentしたい! 12 | - k8s+ArgoCDは素晴らしいが,環境構築もメンテもダルい 13 | - コストに見合わない単純な(`docker-compose`で十分な)やつでもどうにかならんか 14 | - 開発場所とデプロイ先を分けたい 15 | - デプロイ先で `docker-compose build` とかやりたくない 16 | - → イメージは(ちゃんとビルド・プッシュして)コンテナレジストリから 17 | - → 設定ファイル群はGit管理 18 | - `git pull` と `docker-compose up -d`だけでデプロイしたい(するようにしている) 19 | 20 | ## 課題 21 | 22 | - `docker-compose` で立てたサービスの更新がダルい 23 | - イメージの更新がダルい 24 | - 例: `:latest` 25 | - `:v1.0` とかでも実際のイメージ(digest)は裏でどんどん更新されていくことは多い 26 | - 新しいものがあるなら更新したい(逆に,真にイメージを固定したいならdigest pinningするべき) 27 | - 設定とかも更新したい 28 | - `git pull` して `docker-compose down/up -d` するだけではある 29 | - めんどい 30 | 31 | 32 | ## 概要 33 | 34 | - 定期的に以下を実行するスクリプト 35 | - `git pull` 36 | - `docker-compose pull` 37 | - `docker-compose down` 38 | - `docker-compose up -d` 39 | 40 | ## 実装 41 | 42 | ### compose-cd(本体) 43 | 44 | - Shell script(Bash) 45 | - 移植性を考えるとBashはやや微妙だが,`docker-compose`を使うような環境では問題ないだろう 46 | - 定期実行部以外のすべての実装はこのファイル1つに集約(ポン置きで動く) 47 | - 設定ファイルは `/etc/compose-cd/config` (`compose-cd install`時に生成) 48 | - グローバルな設定を `HOGE=value` の形でしておき,実態としては `source` するだけ 49 | - `DISCORD_WEBHOOK`: 50 | ### 定期実行部分 51 | 52 | - `compose-cd update` を定期的に実行する 53 | - systemd timerで実装 54 | - 定期実行以外に要求は無いので実際はcronでも雑なスクリプトでのループでもよい -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compose-cd 2 | [![shellcheck](https://github.com/sksat/compose-cd/actions/workflows/shellcheck.yml/badge.svg)](https://github.com/sksat/compose-cd/actions/workflows/shellcheck.yml) 3 | [![latest release](https://img.shields.io/github/v/release/sksat/compose-cd)](https://github.com/sksat/compose-cd/releases/latest) 4 | ![release date](https://img.shields.io/github/release-date/sksat/compose-cd) 5 | [![license](https://img.shields.io/github/license/sksat/compose-cd)](https://github.com/sksat/compose-cd/blob/main/LICENSE) 6 | ![stars](https://img.shields.io/github/stars/sksat/compose-cd?style=social) 7 | ![downloads](https://img.shields.io/github/downloads/sksat/compose-cd/total) 8 | ![code size](https://img.shields.io/github/languages/code-size/sksat/compose-cd) 9 | 10 | Continuous Deployment for docker-compose 11 | 12 | ## Install 13 | ```sh 14 | $ wget https://github.com/sksat/compose-cd/releases/latest/download/compose-cd.tar.zst 15 | $ tar xvf compose-cd.tar.zst 16 | $ ./compose-cd install 17 | --search-root "/srv" 18 | --git-pull-user 19 | --discord-webhook "https://discord.com/api/webhooks/*****" 20 | ``` 21 | 22 | ## Dependencies 23 | - `bash` 24 | - `find` 25 | - `getopt` 26 | - `git` 27 | - `curl` 28 | - `jq` 29 | - `sudo` 30 | - `docker` 31 | - `docker-compose` 32 | - `systemd` 33 | 34 | ## How to use 35 | 36 | ```sh 37 | $ mkdir /srv && cd /srv 38 | $ git clone # example: https://github.com/sksat/mc.yohane.su 39 | ``` 40 | 41 | Please add `.compose-cd` file to the same directory of `docker-compose.yml`. 42 | 43 | ## How it Works 44 | 45 | `compose-cd` finds `docker-compose` services including `.compose-cd` in the same directory as `docker-compose` configuration(`docker-compose.yml`). 46 | 47 | The main feature of `compose-cd` is `compose-cd update` command. 48 | It loads common configuration from `/etc/compose-cd/config` and update services under `SEARCH_ROOT`. 49 | This "update" includes following 50 | 51 | - `git pull` 52 | - `docker-compose pull` 53 | 54 | `compose-cd update` runs every minute by systemd timer([compose-cd.timer](https://github.com/sksat/compose-cd/blob/main/compose-cd.timer)). 55 | 56 | ## FAQ 57 | 58 | ### compose-cd manages a Git repository? 59 | No. `compose-cd` manages `docker-compose` services including `.compose-cd`. 60 | 61 | So, we can create monorepo that includes `compose-cd` managed `docker-compose` services. 62 | In this use case, it is highly recommended to use `compose-cd` version 0.4+ because `.compose-apply` behavior changed(ref: #30). 63 | 64 | ### How to limit the files that cause a restart? 65 | Please add `.compose-apply` file to the same directory of the `.compose-cd`. 66 | Write the list of files you want to trigger restart in this file. 67 | It supports wildcard. 68 | 69 | example: [mc.yohane.su](https://github.com/sksat/mc.yohane.su/blob/main/.compose-apply) 70 | 71 | ### How to use private repository? 72 | `compose-cd` just executes `git pull` on `GIT_PULL_USER`. 73 | Please use SSH type remote-url. 74 | 75 | GitHub's [deploy keys](https://docs.github.com/en/developers/overview/managing-deploy-keys) would be useful. 76 | Other Git hosting services probably have similar features. 77 | 78 | ### How to use private container image? 79 | `compose-cd` just executes `docker-compose pull`. 80 | Please run `docker login` beforehand. 81 | 82 | ### How to pin container image from configuration files? 83 | Please use following option to prevent pull container image without changes on Git repository. 84 | ```sh 85 | UPDATE_REPO_ONLY=true 86 | UPDATE_IMAGE_BY_REPO=true 87 | ``` 88 | 89 | It is also recommended to [digest pinning](https://docs.renovatebot.com/docker/#digest-pinning) in `docker-compose.yml` like following. 90 | ```yaml 91 | services: 92 | paper: 93 | image: ghcr.io/sksat/papermc-docker:1.18.1@sha256:6b100740af773991eb8f7d15d3f249b54a17c5be679c2a70d0c5b733e63e50a0 94 | ``` 95 | At first glance, updating this configuration may seem to be very tedious, but it is possible to automate this update using [Renovate Bot](https://renovatebot.com). 96 | 97 | Example: 98 | - https://github.com/sksat/mc.yohane.su/pull/232 99 | 100 | ## Blog 101 | - [マイクラサーバをGitHubで運用する](https://sksat.hatenablog.com/entry/2021/08/26/015620) 102 | 103 | ## Slide 104 | - VRC-LT #9 105 | [![slide page 0](https://speakerd.s3.amazonaws.com/presentations/3b08ab8f117b4696ba0f74aaedc91515/slide_0.jpg)](https://speakerdeck.com/sksat/teleka-dot-suwozhi-eruji-shu) 106 | 107 | - さくらのマイクロコミュニティ マイクラサーバ管理者の会 #2 108 | [![slide_page_0](https://files.speakerdeck.com/presentations/5b91a59141ae403580dbee3738f8a549/slide_0.jpg)](https://speakerdeck.com/sksat/mo-guo-falsesabaguan-li-zhe-yo-zi-dong-hua-seyo) 109 | 110 | ## License 111 | MIT. See [LICENSE](./LICENSE) for more details. 112 | -------------------------------------------------------------------------------- /compose-cd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | COMPOSE_CD_VER_MAJOR='0' 4 | COMPOSE_CD_VER_MINOR='6' 5 | COMPOSE_CD_VER_PATCH='1' 6 | COMPOSE_CD_VER_PRE='' 7 | 8 | #function docker-compose(){ 9 | # local p=`pwd` 10 | # echo "[compose-mock]:$p $1 $2" 11 | #} 12 | 13 | function compose() { 14 | # shellcheck disable=SC2068 15 | #echo $COMPOSE_IMPL $@ 16 | 17 | # shellcheck disable=SC2068 18 | $COMPOSE_IMPL $@ 19 | } 20 | 21 | function version() { 22 | local compose_cd_ver 23 | 24 | compose_cd_ver="${COMPOSE_CD_VER_MAJOR}.${COMPOSE_CD_VER_MINOR}.${COMPOSE_CD_VER_PATCH}" 25 | if [ -n "${COMPOSE_CD_VER_PRE}" ]; then 26 | compose_cd_ver="${compose_cd_ver}-${COMPOSE_CD_VER_PRE}" 27 | fi 28 | echo "${compose_cd_ver}" 29 | } 30 | 31 | function usage() { 32 | echo "usage> compose-cd [COMMAND]" 33 | echo "commands:" 34 | echo " help show this usage" 35 | echo " install install compose-cd" 36 | echo " uninstall uninstall compose-cd" 37 | echo " status check status" 38 | echo " update update projects" 39 | } 40 | 41 | function notify_discord() { 42 | local webhook 43 | local msg 44 | local username 45 | 46 | webhook="$1" 47 | msg="$2" 48 | username="$3" 49 | curl --silent -H "Accept: application/json" -H "Content-type: application/json" -X POST \ 50 | -d '{"username":"'"${username}"'","content":'"\"$msg\"}" "$webhook" 51 | } 52 | 53 | # require: DISCORD_WEBHOOK, NOTIFY_USERNAME(optional) 54 | function notify() { 55 | local msg 56 | local username 57 | 58 | if [ -z ${2+x} ]; then 59 | if [ -z "${project}" ]; then 60 | msg="$1" 61 | else 62 | msg="[${project}] $1" 63 | fi 64 | else 65 | msg="$2$1" 66 | fi 67 | 68 | echo "$msg" 69 | 70 | # notify username(default: compose-cd) 71 | username="compose-cd" 72 | if [ -n "${NOTIFY_USERNAME+x}" ]; then 73 | username="${NOTIFY_USERNAME}" 74 | fi 75 | 76 | if [ -n "${DISCORD_WEBHOOK+x}" ]; then 77 | notify_discord "${DISCORD_WEBHOOK}" "$msg" "$username" 78 | fi 79 | } 80 | 81 | function notify_test() { 82 | notify "test" 83 | } 84 | 85 | function compose_log() { 86 | local log_level 87 | local msg 88 | 89 | log_level="$1" 90 | shift 91 | 92 | # shellcheck disable=SC2116 93 | msg=$(echo "$@") 94 | 95 | case $log_level in 96 | "echo") 97 | echo "$@" 98 | ;; 99 | "notify") 100 | notify "$msg" 101 | ;; 102 | esac 103 | } 104 | 105 | function load_global_config() { 106 | if [ ! -e "/etc/compose-cd/config" ]; then 107 | echo "/etc/compose-cd/config not found" 108 | exit 1 109 | fi 110 | # shellcheck disable=SC1091 111 | source /etc/compose-cd/config 112 | 113 | # version compatibility check 114 | if [ "${COMPOSE_CD_VER_MAJOR}" != "${VER_MAJOR}" ]; then 115 | compose_log notify "[warn] major version mismatch!!!: ${COMPOSE_CD_VER_MAJOR} != ${VER_MAJOR}" 116 | fi 117 | if [ "${COMPOSE_CD_VER_MINOR}" != "${VER_MINOR}" ]; then 118 | compose_log notify "[warn] minor version mismatch!!!: ${COMPOSE_CD_VER_MINOR} != ${VER_MINOR}" 119 | fi 120 | 121 | # set default compose implementation 122 | if [ -z ${COMPOSE_IMPL+x} ]; then 123 | COMPOSE_IMPL="docker compose" 124 | fi 125 | } 126 | 127 | function load_config() { 128 | local ret 129 | ret=0 130 | 131 | compose_log echo -n "[$proj:load config] " 132 | if [ ! -e ./.compose-cd ]; then 133 | compose_log echo "config file not found" 134 | return 135 | fi 136 | # shellcheck disable=SC1091 137 | source ./.compose-cd # super config system 138 | if [ -z ${UPDATE_REPO_ONLY+x} ]; then UPDATE_REPO_ONLY=false; fi 139 | if [ -z ${UPDATE_IMAGE_ONLY+x} ]; then UPDATE_IMAGE_ONLY=false; fi 140 | 141 | if [ -z ${UPDATE_IMAGE_BY_REPO+x} ]; then UPDATE_IMAGE_BY_REPO=false; fi 142 | 143 | if [ -z ${PRIVATE_IMAGE+x} ]; then PRIVATE_IMAGE=false; fi 144 | 145 | if [ -z ${REPO_GIT_REMOTE+x} ]; then REPO_GIT_REMOTE="origin"; fi 146 | 147 | if [ -z ${RESTART_WITH_BUILD+x} ]; then RESTART_WITH_BUILD=false; fi 148 | 149 | if [ -v RESTRICT_HOSTNAME_PATTERN ]; then 150 | if ! expr "$(hostname)" : "${RESTRICT_HOSTNAME_PATTERN}" >/dev/null; then 151 | # restricted but not match 152 | ret=1 # skip update 153 | fi 154 | fi 155 | 156 | if "$UPDATE_REPO_ONLY" && "$UPDATE_IMAGE_ONLY"; then 157 | compose_log echo "UPDATE_REPO_ONLY and UPDATE_IMAGE_ONLY are true. This is something wrong." 158 | exit 1 159 | fi 160 | if "$UPDATE_IMAGE_ONLY" && "$UPDATE_IMAGE_BY_REPO"; then 161 | compose_log echo "UPDATE_IMAGE_ONLY and UPDATE_IMAGE_BY_REPO are true. This is something wrong." 162 | exit 1 163 | fi 164 | 165 | compose_log echo "ok" 166 | return $ret 167 | } 168 | 169 | function service_up() { 170 | compose_log notify "starting service..." 171 | if ! "$RESTART_WITH_BUILD"; then 172 | compose up -d 2>/dev/null 173 | else 174 | compose_log notify "start build..." 175 | compose up -d --build 2>/dev/null 176 | compose_log notify "finish build" 177 | fi 178 | compose_log notify "service is up!" 179 | } 180 | 181 | function service_down() { 182 | compose down 2>/dev/null 183 | compose_log notify "service is down" 184 | } 185 | 186 | function get_remote_image() { 187 | local registry 188 | local registry_auth 189 | local image 190 | local tag 191 | local registry_token 192 | local manifest 193 | 194 | registry=$1 195 | image=$2 196 | tag=$3 197 | 198 | if [ "${registry}" = "hub.docker.com" ]; then 199 | registry="registry-1.docker.io" 200 | registry_auth="auth.docker.io" 201 | registry_srv="registry.docker.io" 202 | else 203 | registry_auth=${registry} 204 | registry_srv=${registry} 205 | fi 206 | 207 | #registry_auth="ghcr.io" 208 | #registry_auth="auth.docker.io" 209 | 210 | #echo ${registry_auth} 211 | 212 | if ${PRIVATE_IMAGE}; then 213 | #echo "private image" 214 | registry_token=$(curl --silent \ 215 | -u "${DOCKER_USERNAME}:${DOCKER_PASSWORD}" \ 216 | "https://${registry_auth}/token?scope=repository:${image}:pull&service=${registry_srv}" | 217 | jq -r '.token') 218 | else 219 | #echo "public image" 220 | registry_token=$(curl --silent \ 221 | "https://${registry_auth}/token?scope=repository:${image}:pull&service=${registry_srv}" | 222 | jq -r '.token') 223 | fi 224 | 225 | #echo $registry_token 226 | 227 | compose_log echo -n "${tag} " 228 | manifest=$(curl --silent \ 229 | --header "Accept: application/vnd.docker.distribution.manifest.v2+json" \ 230 | --header "Authorization: Bearer ${registry_token}" \ 231 | "https://${registry}/v2/${image}/manifests/${tag}") 232 | 233 | if echo "${manifest}" | grep 'errors'; then 234 | compose_log echo "${manifest}" 1>&2 235 | exit 1 236 | fi 237 | 238 | compose_log echo "${manifest}" | jq -r '.config.digest' 239 | } 240 | 241 | function git() { 242 | # shellcheck disable=SC2153 243 | sudo -u "$GIT_PULL_USER" git "$@" 244 | } 245 | 246 | # require: GIT_PULL_USER 247 | function update_repo() { 248 | local branch 249 | local local_commit 250 | local remote_commit 251 | local apply_list 252 | local apply_list_expand 253 | local is_restart 254 | local git_remote 255 | 256 | git_remote="${REPO_GIT_REMOTE}" 257 | 258 | compose_log echo -n "[$project:update repository] " 259 | 260 | branch=$(git symbolic-ref --short HEAD) 261 | local_commit=$(git rev-parse HEAD) 262 | remote_commit=$(git ls-remote "${git_remote}" "${branch}" | awk '{print $1}') 263 | 264 | if [ -z "$remote_commit" ]; then 265 | compose_log notify "error: could not get remote commit" 266 | compose_log echo "branch: ${branch}" 267 | return 2 268 | fi 269 | 270 | if [[ $local_commit = "$remote_commit" ]]; then 271 | # no update 272 | compose_log echo "pass" 273 | return 1 274 | fi 275 | 276 | compose_log echo "pull start" 277 | local local_commit_link 278 | local remote_commit_link 279 | local_commit_link="[$local_commit]($(git remote get-url "${git_remote}")/commit/${local_commit})" 280 | remote_commit_link="[$remote_commit]($(git remote get-url "${git_remote}")/commit/${remote_commit})" 281 | compose_log notify "local(${local_commit_link}) -> remote(${remote_commit_link})" 282 | 283 | git pull "${git_remote}" "$branch" 284 | 285 | if "$UPDATE_IMAGE_BY_REPO"; then 286 | compose_log notify "pull image by repo..." 287 | compose pull --quiet 288 | fi 289 | 290 | # check apply-list 291 | if [ ! -e .compose-apply ]; then 292 | return 293 | fi 294 | apply_list=".compose-cd 295 | $(cat .compose-apply)" 296 | 297 | apply_list_expand="" 298 | for a in $apply_list; do 299 | local expand 300 | expand=$(find "$a") 301 | apply_list_expand="${apply_list_expand} 302 | ${expand}" 303 | done 304 | 305 | is_restart=false 306 | set -f 307 | for a in $apply_list_expand; do 308 | if echo "$a" | grep -q '\*'; then 309 | compose_log notify "error: remain wildcard" 310 | return 2 311 | fi 312 | 313 | # non top-level .compose-cd(monorepo): #30 314 | a="$(git rev-parse --show-prefix)${a}" 315 | # echo "check: $a" 316 | 317 | # exact match 318 | if git diff --name-only "${local_commit}" | grep "^${a}$"; then 319 | is_restart=true 320 | compose_log notify "apply: $a" 321 | continue 322 | fi 323 | done 324 | 325 | if ! $is_restart; then 326 | compose_log notify "skip restart: outside of .compose-apply" 327 | return 1 328 | fi 329 | } 330 | 331 | function update_image() { 332 | local img_location 333 | local img_tag 334 | local local_img 335 | local remote_img 336 | local remote_img_id 337 | 338 | compose_log echo -n "[$project:update image] " 339 | 340 | if [ -z ${REGISTRY+x} ]; then 341 | compose_log echo "error: REGISTRY is not set" 342 | return 343 | fi 344 | if [ -z ${IMAGE+x} ]; then 345 | compose_log echo "error: REGISTRY is not set" 346 | return 347 | fi 348 | img_location="${REGISTRY}/${IMAGE}" # like ghcr.io/sksat/kuso-subdomain-adder 349 | 350 | if [ -z ${IMG_TAG+x} ]; then 351 | compose_log echo "warning: IMG_TAG is not set" 352 | img_tag="main" 353 | 354 | compose_log echo -n "[$project:update image] " 355 | else 356 | img_tag="${IMG_TAG}" 357 | fi 358 | 359 | local_img=$(docker images --no-trunc --digests "${img_location}" --format '{{.Tag}} {{.ID}}' | grep "$img_tag") 360 | if [ "$local_img" = "" ]; then 361 | compose_log notify "error: local image is null!" "" 362 | docker images --no-trunc --digests "${img_location}" --format '{{.Tag}} {{.ID}}' 363 | return 2 364 | fi 365 | 366 | remote_img=$(get_remote_image "${REGISTRY}" "${IMAGE}" "${img_tag}") 367 | remote_img_id=$(cut -d' ' -f 2 <<<"${remote_img}") 368 | 369 | compose_log echo "remote_img_id: ${remote_img_id}" 370 | if echo "${remote_img}" | grep -q 'null' || [ "$remote_img_id" = "" ]; then 371 | compose_log notify "error: remote image is null!" "" 372 | compose_log echo "registry: ${REGISTRY}" 373 | compose_log echo "image: ${IMAGE}" 374 | compose_log echo "tag: ${img_tag}" 375 | return 2 376 | fi 377 | 378 | if [[ $local_img = "$remote_img" ]]; then 379 | # no update 380 | compose_log echo "pass" 381 | return 1 382 | fi 383 | 384 | compose_log echo "pull start" 385 | compose_log notify "update image: ${local_img} ===> ${remote_img}" 386 | compose pull --quiet 387 | compose_log echo "[$project:update image] done" 388 | } 389 | 390 | function project_update() { 391 | local proj=$1 392 | local rc 393 | local rr 394 | local ri 395 | 396 | local before_script 397 | local after_script 398 | 399 | load_config 400 | rc=$? 401 | if [ $rc = 1 ]; then 402 | # skip update 403 | compose_log echo "skip update" 404 | return 405 | fi 406 | 407 | rr=1 408 | ri=1 409 | if ! $UPDATE_IMAGE_ONLY; then 410 | update_repo 411 | rr=$? 412 | fi 413 | if ! "$UPDATE_REPO_ONLY"; then 414 | update_image 415 | ri=$? 416 | fi 417 | 418 | # no update 419 | if [ $rr = 1 ] && [ $ri = 1 ]; then 420 | compose_log echo "no update" 421 | return 422 | fi 423 | 424 | # error 425 | if [ $rr = 2 ] || [ $ri = 2 ]; then 426 | return 427 | fi 428 | 429 | if [ -z ${BEFORE_RESTART+x} ]; then 430 | before_script="" 431 | else 432 | before_script=${BEFORE_RESTART} 433 | fi 434 | if [ -z ${AFTER_RESTART+x} ]; then 435 | after_script="" 436 | else 437 | after_script=${AFTER_RESTART} 438 | fi 439 | 440 | # before script 441 | if [ -n "${before_script}" ]; then 442 | if ! bash -c "${before_script}"; then 443 | compose_log notify "skip restart: before script exit with $?" 444 | return 445 | fi 446 | fi 447 | 448 | # todo: running check 449 | 450 | compose_log echo "restart service..." 451 | service_down 452 | service_up 453 | 454 | # after script 455 | if [ -n "${after_script}" ]; then 456 | bash -c "${after_script}" 457 | fi 458 | } 459 | 460 | function project_status() { 461 | local proj 462 | local services 463 | 464 | proj=$1 465 | 466 | compose_log echo -n "[$proj:status] " 467 | services=$(compose ps -q) 468 | 469 | if [ -n "$services" ]; then 470 | compose_log echo "up" 471 | else 472 | compose_log echo "down" 473 | fi 474 | } 475 | 476 | function foreach_project() { 477 | local search_root 478 | local cfgs 479 | 480 | if [ -z ${SEARCH_ROOT+x} ]; then 481 | compose_log echo "error: SEARCH_ROOT is not set" 482 | return 483 | fi 484 | search_root=${SEARCH_ROOT} 485 | 486 | # execute in subshell 487 | ( 488 | cd "${search_root}" || { 489 | compose_log echo "error: SEARCH_ROOT(${search_root}) not found" 490 | exit 1 491 | } 492 | if [ -e compose-cd.lock ]; then 493 | compose_log echo "warn: lock file exists" 494 | exit 0 495 | fi 496 | touch compose-cd.lock 497 | cfgs=$(find . -maxdepth 5 -type f -name '.compose-cd') 498 | for c in $cfgs; do 499 | local proj 500 | proj=$(dirname "$c") 501 | project=$proj # global 502 | 503 | ( 504 | cd "$proj" || { 505 | compose_log echo "[$proj] error: project not fouund" 506 | exit 1 507 | } 508 | eval "$1 $proj" 509 | ) 510 | done 511 | rm compose-cd.lock 512 | ) 513 | } 514 | 515 | # install command can take the following options to automatically configure global options: 516 | # -s | --search-root 517 | # -g | --git-pull-user 518 | # -d | --discord-webhook 519 | function install() { 520 | echo "install" 521 | 522 | local search_root 523 | local git_pull_user 524 | local discord_webhook 525 | 526 | # validate options 527 | local options 528 | if ! options=$(getopt --options "s:g:d:" --longoptions "search-root:,git-pull-user:,discord-webhook:" -- "$@"); then 529 | echo "Incorrect options provided" 530 | exit 1 531 | fi 532 | eval set -- "$options" 533 | 534 | while true; do 535 | case $1 in 536 | --search-root | -s) 537 | shift 538 | search_root="$1" 539 | ;; 540 | --git-pull-user | -g) 541 | shift 542 | git_pull_user="$1" 543 | ;; 544 | --discord-webhook | -d) 545 | shift 546 | discord_webhook="$1" 547 | ;; 548 | --) 549 | shift 550 | break 551 | ;; 552 | esac 553 | shift 554 | done 555 | 556 | if [ -z ${search_root+x} ]; then read -rp "search root> " search_root; fi 557 | if [ -z ${git_pull_user+x} ]; then read -rp "git pull user> " git_pull_user; fi 558 | if [ -z ${discord_webhook+x} ]; then read -rp "Discord webhook URL> " discord_webhook; fi 559 | 560 | mkdir -p /etc/compose-cd 561 | tee /etc/compose-cd/config <