diff --git a/.gitignore b/.gitignore index efd19d7..1d3ed4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -stacklist +config.yml diff --git a/README.md b/README.md index afbedbb..7395b49 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ -# Docker Compose Ops (dc-ops) +# Docker Compose GitOps (dc-ops) -`dc-ops` is a shell script designed to operations automate the local building and updating of several Docker services. -It aims to simplify continuous delivery (CD) processes and infrastructure management in a Docker environment. +`dc-ops` is a simple python script that automates the update and deployment of multiple Docker Compose applications. ## How does it work? +It reads a list of git repositories from a YAML configuration file (`config.yml`), pulls the latest changes for each repo, and runs `docker compose up` for each specified `docker-compose.yml` file. -* The script iterates through each line of the `stacklist` file (skipping comments) or through all cli parameters -* It fetches the latest changes from the remote git repository and checks if there are new commits -* If there is a change it pulls the updates from the remote git repository, otherwise the entry is skipped -* Following this, it runs `docker compose up` which builds, (re)creates and starts the containers +## Features +- Automatically checks for updates in git repositories +- Supports multiple Docker Compose files per repository +- Can be configured to skip certain repositories -The `stacklist` file can list either a directory containing a `docker-compose.yml` file or a precise `docker-compose` file (see `stacklist.example`). -Alternatively, the stacklist can also be passed as a list of parameters. +## Requirements +See `requirements.txt` ## Usage +1. Install the required Python packages: `pip install -r requirements.txt` +2. Adapt `config.yml.example` to your needs and save it as `config.yml` +3. Run `dc-ops` from the shell or from a cron job -* `./dc-ops /path/to/repo /path/to/another-repo /path/with/docker-compose-dev.yml` or -* Update `stacklist` file and run `./dc-ops` (or create a crontab entry) +## Detailed process +For each enabled stack in the config file, the following process will be executed: + +1. Checking directory existence +2. Fetching latest changes from remote repository +3. If there is a new commit, it will pull the changes +4. Running Docker Compose with the defined or default compose-file(s) diff --git a/config.yml.example b/config.yml.example new file mode 100644 index 0000000..b012f92 --- /dev/null +++ b/config.yml.example @@ -0,0 +1,11 @@ +stacks: + - dir: /path/to/first-repo + - dir: /path/to/second-repo + compose-files: + - docker-compose-dev.yml + - dir: /path/to/third-repo + compose-files: + - subdirA/docker-compose.yml + - subdirB/docker-compose.yml + - dir: /path/to/fourth-repo + enabled: false diff --git a/dc-ops b/dc-ops index 21f26ad..4c257eb 100755 --- a/dc-ops +++ b/dc-ops @@ -1,44 +1,70 @@ -#! /bin/bash +#!/usr/bin/env python3 +# -*- encoding: utf-8; py-indent-offset: 4 -*- -## Author: Sebastian Mark -## CC-BY-SA (https://creativecommons.org/licenses/by-sa/4.0/deed.de) -## for civil use only +# Author: Sebastian Mark +# CC-BY-SA (https://creativecommons.org/licenses/by-sa/4.0/deed.de) +# for civil use only -## see README.md +import logging as log +import subprocess +from pathlib import Path -h1() { echo "* $*"; } -msg() { echo "$*"; } +import yaml +from git import GitCommandError, NoSuchPathError, Repo -BASEDIR=$(dirname "$0") +# read config file +configfile = Path(__file__).with_name("config.yml") +with configfile.open("r") as f: + cfg = yaml.safe_load(f.read()) -# use list from file or passed parameters -STACKLIST=$(cat "$BASEDIR/stacklist" 2>/dev/null) -[[ $# -gt 0 ]] && STACKLIST=$* +# init logging +log.basicConfig(format="%(message)s", level=log.INFO) -# iterate list -for LINE in $STACKLIST; do - grep -q "^[[:space:]]*#" <<<"$LINE" && continue # skip comments +# iterate all stacks +for stack in cfg["stacks"]: + # skip disabled stacks + if "enabled" in stack and not stack["enabled"]: + continue - # determine if passed a directory or a file - STACKDIR=$LINE - if [[ -f $LINE ]]; then - STACKDIR=$(dirname "$LINE") - COMPOSEFILE=$LINE - fi + # header + stackdir = stack["dir"] + log.info(f"* processing {stackdir}") - h1 "processing $STACKDIR" + # create repo instance if it exists + try: + repo = Repo(stackdir) + except NoSuchPathError: + log.error("directory not found") + continue - # skip if directroy not found - cd "$STACKDIR" || continue + # try to fetch latest changes + try: + repo.git.fetch() + except GitCommandError as e: + log.error(str(e)) + continue - # fetch from repo and check for new commits - git fetch --quiet || continue - if [[ $(git rev-parse HEAD) == $(git rev-parse "@{u}") ]]; then - msg "no changes - skipping" - continue - fi + # check for new commits + if repo.rev_parse("HEAD") == repo.rev_parse(f"origin/{repo.active_branch}"): + log.info("no changes - skipping") + continue - # pull new commits and run docker compose - git pull || continue - docker compose --file "${COMPOSEFILE:=docker-compose.yml}" up --build --detach --remove-orphans -done + # pull remote changes to local branch + try: + log.info(repo.git.pull()) + except GitCommandError as e: + log.error(str(e)) + continue + + # run `docker compose` for all compose-files + # (or just for the directory if no compose-file defined) + composefiles = stack.get("compose-files", ["docker-compose.yml"]) + for composefile in composefiles: + try: + subprocess.run( + f"docker compose --file {stackdir}/{composefile} up --build --detach --remove-orphans", # noqa + shell=True, + text=True, + ) + except subprocess.CalledProcessError: + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ee70734 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +gitpython +pyyaml diff --git a/stacklist.example b/stacklist.example deleted file mode 100644 index 1b1ec87..0000000 --- a/stacklist.example +++ /dev/null @@ -1,4 +0,0 @@ -/path/to/repo -/path/to/another-repo -/path/with/docker-compose.yml -/another-path/with/docker-compose-dev.yml