diff --git a/bundles/docker-engine/files/check_docker_container b/bundles/docker-engine/files/check_docker_container new file mode 100644 index 0000000..2d8216a --- /dev/null +++ b/bundles/docker-engine/files/check_docker_container @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +from json import loads +from subprocess import check_output +from sys import argv + +try: + container_name = argv[1] + + docker_ps = check_output([ + 'docker', + 'container', + 'ls', + '--all', + '--format', + 'json', + '--filter', + f'name={container_name}' + ]) + + containers = loads(f"[{','.join([l for l in docker_ps.decode().splitlines() if l])}]") + + if not containers: + print(f'CRITICAL: container {container_name} not found!') + exit(2) + + if len(containers) > 1: + print(f'Found more than one container matching {container_name}!') + print(docker_ps) + exit(3) + + if containers[0]['State'] != 'running': + print(f'WARNING: container {container_name} not "running"') + exit(2) + + print(f"OK: {containers[0]['Status']}") +except Exception as e: + print(repr(e)) + exit(2) diff --git a/bundles/docker-engine/files/docker-wrapper b/bundles/docker-engine/files/docker-wrapper new file mode 100644 index 0000000..c225ceb --- /dev/null +++ b/bundles/docker-engine/files/docker-wrapper @@ -0,0 +1,50 @@ +#!/bin/bash + +[[ -n "$DEBUG" ]] && set -x + +ACTION="$1" + +set -euo pipefail + +if [[ -z "$ACTION" ]] +then + echo "Usage: $0 start|stop" + exit 1 +fi + +PUID="$(id -u "docker-${name}")" +PGID="$(id -g "docker-${name}")" + +if [ "$ACTION" == "start" ] +then + docker run -d \ + --name "${name}" \ + --env "PUID=$PUID" \ + --env "PGID=$PGID" \ + --env "TZ=${timezone}" \ +% for k, v in sorted(environment.items()): + --env "${k}=${v}" \ +% endfor + --network host \ +% for host_port, container_port in sorted(ports.items()): + --expose "127.0.0.1:${host_port}:${container_port}" \ +% endfor +% for host_path, container_path in sorted(volumes.items()): + --volume "/var/opt/docker-engine/${name}/${host_path}:${container_path}" \ +% endfor + --restart unless-stopped \ + "${image}" + +elif [ "$ACTION" == "stop" ] +then + docker stop "${name}" + docker rm "${name}" + +else + echo "Unknown action $ACTION" + exit 1 +fi + +% if node.has_bundle('nftables'): +systemctl reload nftables +% endif diff --git a/bundles/docker-engine/files/docker-wrapper.service b/bundles/docker-engine/files/docker-wrapper.service new file mode 100644 index 0000000..a908c86 --- /dev/null +++ b/bundles/docker-engine/files/docker-wrapper.service @@ -0,0 +1,14 @@ +[Unit] +Description=docker-engine app ${name} +After=network.target +Requires=${' '.join(sorted(requires))} + +[Service] +WorkingDirectory=/var/opt/docker-engine/${name}/ +ExecStart=/opt/docker-engine/${name} start +ExecStop=/opt/docker-engine/${name} stop +Type=simple +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target diff --git a/bundles/docker-engine/items.py b/bundles/docker-engine/items.py new file mode 100644 index 0000000..9e52eca --- /dev/null +++ b/bundles/docker-engine/items.py @@ -0,0 +1,99 @@ +from bundlewrap.metadata import metadata_to_json + +deps = { + 'pkg_apt:docker-ce', + 'pkg_apt:docker-ce-cli', +} + +directories['/opt/docker-engine'] = { + 'purge': True, +} +directories['/var/opt/docker-engine'] = {} + +files['/etc/docker/daemon.json'] = { + 'content': metadata_to_json(node.metadata.get('docker-engine/config')), + 'triggers': { + 'svc_systemd:docker:restart', + }, + # install config before installing packages to ensure the config is + # applied to the first start as well + 'before': deps, +} + +svc_systemd['docker'] = { + 'needs': deps, +} + +files['/usr/local/share/icinga/plugins/check_docker_container'] = { + 'mode': '0755', +} + +for app, config in node.metadata.get('docker-engine/containers', {}).items(): + volumes = config.get('volumes', {}) + + files[f'/opt/docker-engine/{app}'] = { + 'source': 'docker-wrapper', + 'content_type': 'mako', + 'context': { + 'environment': config.get('environment', {}), + 'image': config['image'], + 'name': app, + 'ports': config.get('ports', {}), + 'timezone': node.metadata.get('timezone'), + 'volumes': volumes, + }, + 'mode': '0755', + 'triggers': { + f'svc_systemd:docker-{app}:restart', + }, + } + + users[f'docker-{app}'] = { + 'home': f'/var/opt/docker-engine/{app}', + 'groups': { + 'docker', + }, + 'after': { + # provides docker group + 'pkg_apt:docker-ce', + }, + } + + files[f'/usr/local/lib/systemd/system/docker-{app}.service'] = { + 'source': 'docker-wrapper.service', + 'content_type': 'mako', + 'context': { + 'name': app, + 'requires': { + *set(config.get('requires', set())), + 'docker.service', + } + }, + 'triggers': { + 'action:systemd-reload', + f'svc_systemd:docker-{app}:restart', + }, + } + + svc_systemd[f'docker-{app}'] = { + 'needs': { + *deps, + f'file:/opt/docker-engine/{app}', + f'file:/usr/local/lib/systemd/system/docker-{app}.service', + f'user:docker-{app}', + 'svc_systemd:docker', + *set(config.get('needs', set())), + }, + } + + for volume in volumes: + directories[f'/var/opt/docker-engine/{app}/{volume}'] = { + 'owner': f'docker-{app}', + 'group': f'docker-{app}', + 'needed_by': { + f'svc_systemd:docker-{app}', + }, + # don't do anything if the directory exists, docker images + # mangle owners + 'unless': f'test -d /var/opt/docker-engine/{app}/{volume}', + } diff --git a/bundles/docker-engine/metadata.py b/bundles/docker-engine/metadata.py new file mode 100644 index 0000000..fa55b5e --- /dev/null +++ b/bundles/docker-engine/metadata.py @@ -0,0 +1,83 @@ +defaults = { + 'apt': { + 'packages': { + 'docker-ce': {}, + 'docker-ce-cli': {}, + 'docker-compose-plugin': {}, + }, + 'repos': { + 'docker': { + 'items': { + 'deb https://download.docker.com/linux/debian {os_release} stable', + }, + }, + }, + }, + 'backups': { + 'paths': { + '/var/opt/docker-engine', + }, + }, + 'hosts': { + 'entries': { + '172.17.0.1': { + 'host.docker.internal', + }, + }, + }, + 'docker-engine': { + 'config': { + 'iptables': False, + 'no-new-privileges': True, + }, + }, + 'zfs': { + 'datasets': { + 'tank/docker-data': { + 'mountpoint': '/var/opt/docker-engine', + }, + }, + }, +} + + +@metadata_reactor.provides( + 'icinga2_api/docker-engine/services', +) +def monitoring(metadata): + services = { + 'DOCKER PROCESS': { + 'command_on_monitored_host': '/usr/lib/nagios/plugins/check_procs -C dockerd -c 1:', + }, + } + + for app in metadata.get('docker-engine/containers', {}): + services[f'DOCKER CONTAINER {app}'] = { + 'command_on_monitored_host': f'sudo /usr/local/share/icinga/plugins/check_docker_container {app}' + } + + return { + 'icinga2_api': { + 'docker-engine': { + 'services': services, + }, + }, + } + + +@metadata_reactor.provides( + 'zfs/datasets', +) +def zfs(metadata): + datasets = {} + + for app in metadata.get('docker-engine/containers', {}): + datasets[f'tank/docker-data/{app}'] = { + 'mountpoint': f'/var/opt/docker-engine/{app}' + } + + return { + 'zfs': { + 'datasets': datasets, + }, + } diff --git a/bundles/nftables/metadata.py b/bundles/nftables/metadata.py index 8212d3c..15f34d4 100644 --- a/bundles/nftables/metadata.py +++ b/bundles/nftables/metadata.py @@ -29,7 +29,7 @@ defaults = { }, } -if not node.has_bundle('vmhost'): +if not node.has_bundle('vmhost') and not node.has_bundle('docker-engine'): # see comment in bundles/vmhost/items.py defaults['apt']['packages']['iptables'] = { 'installed': False, diff --git a/data/apt/files/gpg-keys/docker.asc b/data/apt/files/gpg-keys/docker.asc new file mode 100644 index 0000000..ee7872e --- /dev/null +++ b/data/apt/files/gpg-keys/docker.asc @@ -0,0 +1,62 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFit2ioBEADhWpZ8/wvZ6hUTiXOwQHXMAlaFHcPH9hAtr4F1y2+OYdbtMuth +lqqwp028AqyY+PRfVMtSYMbjuQuu5byyKR01BbqYhuS3jtqQmljZ/bJvXqnmiVXh +38UuLa+z077PxyxQhu5BbqntTPQMfiyqEiU+BKbq2WmANUKQf+1AmZY/IruOXbnq +L4C1+gJ8vfmXQt99npCaxEjaNRVYfOS8QcixNzHUYnb6emjlANyEVlZzeqo7XKl7 +UrwV5inawTSzWNvtjEjj4nJL8NsLwscpLPQUhTQ+7BbQXAwAmeHCUTQIvvWXqw0N +cmhh4HgeQscQHYgOJjjDVfoY5MucvglbIgCqfzAHW9jxmRL4qbMZj+b1XoePEtht +ku4bIQN1X5P07fNWzlgaRL5Z4POXDDZTlIQ/El58j9kp4bnWRCJW0lya+f8ocodo +vZZ+Doi+fy4D5ZGrL4XEcIQP/Lv5uFyf+kQtl/94VFYVJOleAv8W92KdgDkhTcTD +G7c0tIkVEKNUq48b3aQ64NOZQW7fVjfoKwEZdOqPE72Pa45jrZzvUFxSpdiNk2tZ +XYukHjlxxEgBdC/J3cMMNRE1F4NCA3ApfV1Y7/hTeOnmDuDYwr9/obA8t016Yljj +q5rdkywPf4JF8mXUW5eCN1vAFHxeg9ZWemhBtQmGxXnw9M+z6hWwc6ahmwARAQAB +tCtEb2NrZXIgUmVsZWFzZSAoQ0UgZGViKSA8ZG9ja2VyQGRvY2tlci5jb20+iQI3 +BBMBCgAhBQJYrefAAhsvBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEI2BgDwO +v82IsskP/iQZo68flDQmNvn8X5XTd6RRaUH33kXYXquT6NkHJciS7E2gTJmqvMqd +tI4mNYHCSEYxI5qrcYV5YqX9P6+Ko+vozo4nseUQLPH/ATQ4qL0Zok+1jkag3Lgk +jonyUf9bwtWxFp05HC3GMHPhhcUSexCxQLQvnFWXD2sWLKivHp2fT8QbRGeZ+d3m +6fqcd5Fu7pxsqm0EUDK5NL+nPIgYhN+auTrhgzhK1CShfGccM/wfRlei9Utz6p9P +XRKIlWnXtT4qNGZNTN0tR+NLG/6Bqd8OYBaFAUcue/w1VW6JQ2VGYZHnZu9S8LMc +FYBa5Ig9PxwGQOgq6RDKDbV+PqTQT5EFMeR1mrjckk4DQJjbxeMZbiNMG5kGECA8 +g383P3elhn03WGbEEa4MNc3Z4+7c236QI3xWJfNPdUbXRaAwhy/6rTSFbzwKB0Jm +ebwzQfwjQY6f55MiI/RqDCyuPj3r3jyVRkK86pQKBAJwFHyqj9KaKXMZjfVnowLh +9svIGfNbGHpucATqREvUHuQbNnqkCx8VVhtYkhDb9fEP2xBu5VvHbR+3nfVhMut5 +G34Ct5RS7Jt6LIfFdtcn8CaSas/l1HbiGeRgc70X/9aYx/V/CEJv0lIe8gP6uDoW +FPIZ7d6vH+Vro6xuWEGiuMaiznap2KhZmpkgfupyFmplh0s6knymuQINBFit2ioB +EADneL9S9m4vhU3blaRjVUUyJ7b/qTjcSylvCH5XUE6R2k+ckEZjfAMZPLpO+/tF +M2JIJMD4SifKuS3xck9KtZGCufGmcwiLQRzeHF7vJUKrLD5RTkNi23ydvWZgPjtx +Q+DTT1Zcn7BrQFY6FgnRoUVIxwtdw1bMY/89rsFgS5wwuMESd3Q2RYgb7EOFOpnu +w6da7WakWf4IhnF5nsNYGDVaIHzpiqCl+uTbf1epCjrOlIzkZ3Z3Yk5CM/TiFzPk +z2lLz89cpD8U+NtCsfagWWfjd2U3jDapgH+7nQnCEWpROtzaKHG6lA3pXdix5zG8 +eRc6/0IbUSWvfjKxLLPfNeCS2pCL3IeEI5nothEEYdQH6szpLog79xB9dVnJyKJb +VfxXnseoYqVrRz2VVbUI5Blwm6B40E3eGVfUQWiux54DspyVMMk41Mx7QJ3iynIa +1N4ZAqVMAEruyXTRTxc9XW0tYhDMA/1GYvz0EmFpm8LzTHA6sFVtPm/ZlNCX6P1X +zJwrv7DSQKD6GGlBQUX+OeEJ8tTkkf8QTJSPUdh8P8YxDFS5EOGAvhhpMBYD42kQ +pqXjEC+XcycTvGI7impgv9PDY1RCC1zkBjKPa120rNhv/hkVk/YhuGoajoHyy4h7 +ZQopdcMtpN2dgmhEegny9JCSwxfQmQ0zK0g7m6SHiKMwjwARAQABiQQ+BBgBCAAJ +BQJYrdoqAhsCAikJEI2BgDwOv82IwV0gBBkBCAAGBQJYrdoqAAoJEH6gqcPyc/zY +1WAP/2wJ+R0gE6qsce3rjaIz58PJmc8goKrir5hnElWhPgbq7cYIsW5qiFyLhkdp +YcMmhD9mRiPpQn6Ya2w3e3B8zfIVKipbMBnke/ytZ9M7qHmDCcjoiSmwEXN3wKYI +mD9VHONsl/CG1rU9Isw1jtB5g1YxuBA7M/m36XN6x2u+NtNMDB9P56yc4gfsZVES +KA9v+yY2/l45L8d/WUkUi0YXomn6hyBGI7JrBLq0CX37GEYP6O9rrKipfz73XfO7 +JIGzOKZlljb/D9RX/g7nRbCn+3EtH7xnk+TK/50euEKw8SMUg147sJTcpQmv6UzZ +cM4JgL0HbHVCojV4C/plELwMddALOFeYQzTif6sMRPf+3DSj8frbInjChC3yOLy0 +6br92KFom17EIj2CAcoeq7UPhi2oouYBwPxh5ytdehJkoo+sN7RIWua6P2WSmon5 +U888cSylXC0+ADFdgLX9K2zrDVYUG1vo8CX0vzxFBaHwN6Px26fhIT1/hYUHQR1z +VfNDcyQmXqkOnZvvoMfz/Q0s9BhFJ/zU6AgQbIZE/hm1spsfgvtsD1frZfygXJ9f +irP+MSAI80xHSf91qSRZOj4Pl3ZJNbq4yYxv0b1pkMqeGdjdCYhLU+LZ4wbQmpCk +SVe2prlLureigXtmZfkqevRz7FrIZiu9ky8wnCAPwC7/zmS18rgP/17bOtL4/iIz +QhxAAoAMWVrGyJivSkjhSGx1uCojsWfsTAm11P7jsruIL61ZzMUVE2aM3Pmj5G+W +9AcZ58Em+1WsVnAXdUR//bMmhyr8wL/G1YO1V3JEJTRdxsSxdYa4deGBBY/Adpsw +24jxhOJR+lsJpqIUeb999+R8euDhRHG9eFO7DRu6weatUJ6suupoDTRWtr/4yGqe +dKxV3qQhNLSnaAzqW/1nA3iUB4k7kCaKZxhdhDbClf9P37qaRW467BLCVO/coL3y +Vm50dwdrNtKpMBh3ZpbB1uJvgi9mXtyBOMJ3v8RZeDzFiG8HdCtg9RvIt/AIFoHR +H3S+U79NT6i0KPzLImDfs8T7RlpyuMc4Ufs8ggyg9v3Ae6cN3eQyxcK3w0cbBwsh +/nQNfsA6uu+9H7NhbehBMhYnpNZyrHzCmzyXkauwRAqoCbGCNykTRwsur9gS41TQ +M8ssD1jFheOJf3hODnkKU+HKjvMROl1DK7zdmLdNzA1cvtZH/nCC9KPj1z8QC47S +xx+dTZSx4ONAhwbS/LN3PoKtn8LPjY9NP9uDWI+TWYquS2U+KHDrBDlsgozDbs/O +jCxcpDzNmXpWQHEtHU7649OXHP7UeNST1mCUCH5qdank0V1iejF6/CfTFU4MfcrG +YT90qFF93M3v01BbxP+EIY2/9tiIPbrd +=0YYh +-----END PGP PUBLIC KEY BLOCK-----