working copy

This commit is contained in:
Luca Bilke 2025-01-19 18:16:18 +01:00
parent be661e09de
commit 009fb3304b
Signed by: luca
GPG key ID: C9E851809C1A5BDE
20 changed files with 322 additions and 175 deletions

View file

@ -11,15 +11,15 @@ module: slurp
version_added: 1.0.0
short_description: Lookup version of the slurp module
description:
- Instead of running the slurp module against local host, I wrote this module
- Instead of running the slurp module against local host, I wrote this module
author:
- "Luca Bilke (@ssnailed)"
- "Luca Bilke (@ssnailed)"
options:
_terms:
_terms:
description: path(s) of files to read
required: True
seealso:
- ref: playbook_task_paths
- ref: playbook_task_paths
description: Search paths used for relative files.
"""
@ -55,7 +55,10 @@ class LookupModule(LookupBase):
try:
lookupfile = self.find_file_in_search_path(
variables, "files", term, ignore_missing=True,
variables,
"files",
term,
ignore_missing=True,
)
display.vvvv(f"File lookup using {lookupfile} as file")

View file

@ -6,7 +6,7 @@ from __future__ import annotations
import copy
from dataclasses import replace
from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, TypeVar, cast
import yaml
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import Result, State
@ -15,13 +15,6 @@ if TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
def clean_none(obj: dict[str, Any]) -> dict[str, Any]:
obj = copy.deepcopy(obj)
for k in copy.deepcopy(obj):
if obj.get(k, "sentinel") is None:
del obj[k]
return obj
def recursive_update(
default: dict[str, Any],
update: dict[str, Any],
@ -31,11 +24,11 @@ def recursive_update(
for k, v in update.items():
if isinstance(v, dict):
v = cast(dict[str, Any], v)
default[k] = recursive_update(default.get(k) or {}, v)
default[k] = recursive_update(default.get(k, {}), v)
elif isinstance(v, list):
v = cast(list[Any], v)
new = cast(list[Any], (default.get(k) or []))
new = cast(list[Any], (default.get(k, [])))
new.extend(v)
default[k] = new
@ -47,7 +40,23 @@ def recursive_update(
def get_state(module: AnsibleModule) -> State:
"""Create a new state object, loading the compose file into "before" if it exists."""
compose_filepath = f"{module.params['project_dir']}/{module.params['name']}/docker-compose.yml"
T = TypeVar("T")
# def clean_params[T](obj: T) -> T:
def clean_params(obj: T) -> T:
# NOTE: ansible sets None as default for arguments, which is a pain in
# the ass for the purposes of this module
obj = copy.deepcopy(obj)
if isinstance(obj, dict):
return {key: clean_params(value) for key, value in obj.items() if value is not None} # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType, reportReturnType]
if isinstance(obj, list):
return [clean_params(item) for item in obj if item is not None] # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType, reportReturnType]
return obj
compose_filepath = f"{module.params['project_dir']}/docker-compose.yml"
try:
with Path(compose_filepath).open("r") as fp:
@ -57,6 +66,7 @@ def get_state(module: AnsibleModule) -> State:
return State(
module=module,
params=clean_params(module.params),
result=Result(),
compose_filepath=compose_filepath,
before=before,
@ -70,32 +80,31 @@ def get_state(module: AnsibleModule) -> State:
def update_project(state: State) -> State:
"""Ensure that networks/volumes that exist in services also exist in the project."""
"""Properly configure top level volume/network elements."""
project = copy.deepcopy(state.after)
services: dict[str, Any] = project.get("services", {})
settings: dict[str, Any] = state.params.get("settings", {})
project_services: dict[str, Any] = project.get("services", {})
project_networks: dict[str, Any] = project.get("networks", {})
project_volumes: dict[str, Any] = project.get("volumes", {})
volume_sources = [
vol["source"] for service in services.values() for vol in service.get("volumes", [])
]
for project_service in [x for x in project_services.values() if x]:
if service_volumes := project_service.get("volumes"):
service_volume_names = [x["source"] for x in service_volumes]
project_volumes.update(
{
service_volume_name: None
for service_volume_name in service_volume_names
if service_volume_name not in project_volumes
},
)
network_names = [
network for service in services.values() for network in service.get("networks", {})
]
if service_network_names := project_service.get("networks", {}).keys():
project_networks.update(
{
service_network_name: None
for service_network_name in service_network_names
if service_network_name not in project_networks
},
)
duplicate_sources = [source for source in volume_sources if volume_sources.count(source) > 1]
project["volumes"] = {source: None for source in duplicate_sources} | {
source: {"external": True}
for source in volume_sources
if source in settings.get("external_volumes", [])
}
project["networks"] = {
network: ({"external": True} if network in settings.get("external_networks", []) else None)
for network in network_names
}
return replace(state, after=project)
@ -120,13 +129,12 @@ def set_result(state: State) -> State: # noqa: C901
return False
if isinstance(before, list):
before = sorted(cast(list[Any], before))
after = sorted(after)
before = cast(list[Any], before)
if len(before) != len(after):
return True
for index in before.enumerate():
for index, _ in enumerate(before):
if _changed(before[index], after[index]):
return True

View file

@ -5,10 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import (
clean_none,
recursive_update,
)
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import recursive_update
if TYPE_CHECKING:
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
@ -25,10 +22,10 @@ def apply_update(
def get_default_args(state: State, helper_name: str) -> dict[str, Any]:
settings: dict[str, Any] = state.module.params.get("settings") or {}
label_default_args: dict[str, Any] = settings.get("label_default_args") or {}
default_args: dict[str, Any] = label_default_args.get(helper_name) or {}
return clean_none(default_args)
settings: dict[str, Any] = state.params.get("settings", {})
label_default_args: dict[str, Any] = settings.get("label_default_args", {})
default_args: dict[str, Any] = label_default_args.get(helper_name, {})
return default_args
def run_helper(

View file

@ -9,13 +9,13 @@ if TYPE_CHECKING:
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"stop": {"type": "bool", "default": True},
"stop": {"type": "bool", "required": True},
}
def helper(state: State, _service_name: str, params: dict[str, Any]) -> dict[str, Any]:
stop: bool = params["stop"]
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
update: dict[str, Any] = {}

View file

@ -10,19 +10,19 @@ if TYPE_CHECKING:
EXTRA_ARGS = {
"proxy_type": {"type": "str", "default": "http"},
"name": {"type": "string"},
"name": {"type": "str"},
"middleware": {"type": "str", "required": True},
"settings": {"type": "list", "required": True},
"settings": {"type": "dict", "required": True},
}
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
middleware: str = params["middleware"]
settings: dict[str, str] = params["settings"]
proxy_type: str = params["proxy_type"]
name: str = (
params.get("name") or f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}"
params.get("name", f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}")
)
prefix = f"traefik.{proxy_type}.middlewares.{name}"

View file

@ -20,24 +20,24 @@ EXTRA_ARGS = {
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
rule: str = params["rule"]
traefik_service: str | None = params.get("service")
entrypoints: list[str] | None = params.get("entrypoints")
middlewares: list[str] | None = params.get("middlewares")
certresolver: str | None = params.get("certresolver")
proxy_type: str = params["proxy_type"]
name: str = params.get("name") or f"{project_name}_{service_name}_{proxy_type}"
name: str = params.get("name", f"{project_name}_{service_name}_{proxy_type}")
prefix = f"traefik.{proxy_type}.routers.{name}"
labels = {
"traefik.enable": True,
f"traefik.{prefix}.rule": rule,
f"{prefix}.rule": rule,
}
if certresolver:
labels[f"traefik.{prefix}.tls.certresolver"] = certresolver
labels[f"{prefix}.tls.certresolver"] = certresolver
if entrypoints:
labels[f"{prefix}.entrypoints"] = ",".join(entrypoints)

View file

@ -10,16 +10,16 @@ if TYPE_CHECKING:
EXTRA_ARGS = {
"proxy_type": {"type": "str", "default": "http"},
"name": {"type": "string"},
"name": {"type": "str"},
"port": {"type": "int"},
}
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
port: int | None = params.get("port")
proxy_type: str = params["proxy_type"]
name: str = params.get("name") or f"{project_name}_{service_name}_{proxy_type}"
name: str = params.get("name", f"{project_name}_{service_name}_{proxy_type}")
prefix = f"traefik.{proxy_type}.services.{name}"

View file

@ -19,6 +19,7 @@ class Result:
@dataclass(frozen=True)
class State:
module: AnsibleModule
params: dict[str, Any]
result: Result
compose_filepath: str
before: dict[str, Any]

View file

@ -7,10 +7,7 @@ import copy
from dataclasses import replace
from typing import TYPE_CHECKING, Any, Callable
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import (
clean_none,
recursive_update,
)
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import recursive_update
if TYPE_CHECKING:
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
@ -22,7 +19,7 @@ BASE_ARGS: dict[str, Any] = {
def get_base_definition(state: State, service_name: str) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
return {
"container_name": f"{project_name}_{service_name}",
"hostname": f"{project_name}_{service_name}",
@ -38,19 +35,19 @@ def get_base_definition(state: State, service_name: str) -> dict[str, Any]:
def get_default_definition(state: State, service_name: str) -> dict[str, Any]:
settings: dict[str, Any] = state.module.params.get("settings") or {}
default_definition: dict[str, Any] = settings.get("default_definition") or {}
service_default_definitions: dict[str, Any] = settings.get("service_default_definitions") or {}
service_default_definition: dict[str, Any] = service_default_definitions.get(service_name) or {}
settings: dict[str, Any] = state.params.get("settings", {})
default_definition: dict[str, Any] = settings.get("default_definition", {})
service_default_definitions: dict[str, Any] = settings.get("service_default_definitions", {})
service_default_definition: dict[str, Any] = service_default_definitions.get(service_name, {})
return default_definition | service_default_definition
def get_default_args(state: State, helper_name: str) -> dict[str, Any]:
settings: dict[str, Any] = state.module.params.get("settings") or {}
service_default_args: dict[str, Any] = settings.get("service_default_args") or {}
default_args: dict[str, Any] = service_default_args.get(helper_name) or {}
return clean_none(default_args)
settings: dict[str, Any] = state.params.get("settings", {})
service_default_args: dict[str, Any] = settings.get("service_default_args", {})
default_args: dict[str, Any] = service_default_args.get(helper_name, {})
return default_args
def apply_update(state: State, service_name: str, update: dict[str, Any]) -> State:
@ -58,7 +55,7 @@ def apply_update(state: State, service_name: str, update: dict[str, Any]) -> Sta
service = project["services"].get(service_name, {})
service = recursive_update(service, update)
volumes: list[dict[str, Any]] = service.get("volumes") or []
volumes: list[dict[str, Any]] = service.get("volumes", [])
unique_volumes = list({vol["source"]: vol for vol in volumes if "target" in vol}.values())
service["volumes"] = unique_volumes

View file

@ -6,34 +6,24 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_docker.plugins.module_utils import label, spec
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import clean_none
if TYPE_CHECKING:
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import (
State,
)
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
FORCE_ARGS = {
"name": {"type": "str", "required": True},
"definition": {"type": "dict", "required": True},
"internal_network": {"type": "bool", "default": False},
"label_helpers": spec.label_argument_spec(),
}
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
internal_network: bool = params["internal_network"]
def helper(state: State, service_params: dict[str, Any]) -> dict[str, Any]:
update: dict[str, Any] = {}
if internal_network:
networks = update.get("networks", {})
networks["internal"] = None
update["networks"] = networks
for name, args in [(x, y) for x, y in params.get("label_helpers", {}).items() if y]:
label_params = label.common.get_default_args(state, name) | clean_none(args)
helper = getattr(label, name).helper
update = label.common.run_helper(state, params["name"], update, label_params, helper)
for name, labels_params in service_params.get("label_helpers", {}).items():
for label_params in labels_params:
params = label.common.get_default_args(state, name) | label_params
helper = getattr(label, name).helper
update |= label.common.run_helper(state, service_params["name"], update, params, helper)
return update

View file

@ -15,7 +15,7 @@ EXTRA_ARGS = {
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
archive: str | None = params.get("archive")
backup_volumes: list[str] | None = params["backup_volumes"]
service_name = params["name"]

View file

@ -19,7 +19,7 @@ EXTRA_ARGS = {
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
backup: bool = params["backup"]
database: str = params["database"]
username: str = params["username"]

View file

@ -18,7 +18,7 @@ EXTRA_ARGS = {
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
project_name: str = state.module.params["name"]
project_name: str = state.params["name"]
backup: bool = params["backup"]
database: str = params["database"]
username: str = params["username"]

View file

@ -48,7 +48,8 @@ def label_argument_spec() -> dict[str, Any]:
for module_name, module in get_modules(label):
label_args["options"][module_name] = {
"type": "dict",
"type": "list",
"elements": "dict",
"options": get_module_options(module, label.common.BASE_ARGS),
}
@ -80,6 +81,8 @@ def settings_spec() -> dict[str, Any]:
"service_default_args": {"type": "dict", "options": {}},
"label_default_args": {"type": "dict", "options": {}},
"service_default_definitions": {"type": "dict", "options": {}},
"external_networks": {"type": "list", "elements": "str"},
"external_volumes": {"type": "list", "elements": "str"},
},
}

View file

@ -5,8 +5,6 @@
from __future__ import annotations
from dataclasses import asdict
# TODO: break this down per module
# TODO: generate this by reassembling
# TODO: add note about not setting container_name or host_name in defaults
@ -43,6 +41,16 @@ options:
- Settings/Defaults for the module.
type: dict
suboptions:
external_networks:
description:
- Networks to mark as external.
type: list
elements: str
external_volumes:
description:
- Volumes to mark as external.
type: list
elements: str
default_definition:
description:
- Default definition for all containers.
@ -150,23 +158,11 @@ options:
description:
- Name of the service.
type: str
image:
description:
- Image to use for service.
type: str
defaults:
description:
- Service definition to be overwritten.
type: dict
overwrite:
definition:
description:
- Service definition to overwrite with.
type: dict
internal_network:
description:
- If true, add internal network to service.
type: bool
default: false
required: true
label_helpers:
description:
- Label helper configurations.
@ -175,7 +171,8 @@ options:
docker_volume_backupper:
description:
- Docker Volume Backupper label helper configuration.
type: dict
type: list
elements: dict
suboptions:
stop:
description:
@ -185,7 +182,8 @@ options:
traefik_middleware:
description:
- Traefik Middleware label helper configuration.
type: dict
type: list
elements: dict
suboptions:
proxy_type:
description:
@ -210,7 +208,8 @@ options:
traefik_router:
description:
- Traefik Router label helper configuration.
type: dict
type: list
elements: dict
suboptions:
proxy_type:
description:
@ -248,7 +247,8 @@ options:
traefik_service:
description:
- Traefik Service label helper configuration.
type: dict
type: list
elements: dict
suboptions:
proxy_type:
description:
@ -454,6 +454,7 @@ options:
type: dict
"""
from dataclasses import asdict
from importlib.util import find_spec
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
@ -490,11 +491,9 @@ def main() -> None:
except Exception as e: # noqa: BLE001
module.fail_json(f"Error while reading existing compose file: {e}") # pyright: ignore[reportUnknownMemberType]
for name, services_params in [(x, y) for x, y in module.params["services"].items() if y]:
for name, services_params in state.params.get("services", {}).items():
for index, service_params in enumerate(services_params):
params = service.common.get_default_args(state, name) | common.clean_none(
service_params
)
params = service.common.get_default_args(state, name) | service_params
params["_index"] = index
helper = getattr(service, name).helper
state = service.common.run_helper(state, params, helper)
@ -514,5 +513,6 @@ def main() -> None:
module.exit_json(**ret) # pyright: ignore[reportUnknownMemberType]
if __name__ == "__main__":
main()

View file

@ -16,10 +16,12 @@ ez_docker_shared_volumes: []
# - name: "shared_volume"
# Directory to put compose projects into
ez_docker_project_dir: "/var/lib/ez_compose"
ez_docker_project_dir: "/var/lib/ez_docker"
# Settings for the module
ez_docker_settings:
external_networks: "{{ ez_docker_shared_networks | map(attribute='name') }}"
external_volumes: "{{ ez_docker_shared_volumes | map(attribute='name') }}"
default_definition: {}
# environment:
# TZ: "Europe/Berlin"
@ -36,14 +38,6 @@ ez_docker_settings:
service_default_args: {}
# docker_volume_backupper:
# archive: "/tank/docker-backups"
#
ez_cp_controller_files_root: "{{ playbook_dir }}/files/ez_cp/{{ inventory_hostname }}"
ez_cp_controller_templates_root: "{{ playbook_dir }}/templates/ez_cp/{{ inventory_hostname }}"
# To copy into a non-running container we need the UID/GID for the files
# TODO: implement this
ez_cp_service_owners: {}
# project_name:
# service_name:
# uid: 1000
# gid: 1000
ez_docker_controller_files_root: "{{ playbook_dir }}/files/ez_docker/{{ inventory_hostname }}"
ez_docker_controller_templates_root: "{{ playbook_dir }}/templates/ez_docker/{{ inventory_hostname }}"

View file

@ -12,7 +12,6 @@
)
)
}}
when: not ez_docker_projects
- name: Login in to registries
community.docker.docker_login: "{{ item }}" # noqa: args[module]
@ -32,6 +31,8 @@
- name: Import per-project tasks
ansible.builtin.include_tasks: project.yml
when: >
[(project_definition.key | split('_') | last), "all"] | intersect(ansible_run_tags) | length > 0
loop: "{{ ez_docker_projects }}"
loop_control:
loop_var: project_definition

View file

@ -1,28 +1,43 @@
---
- name: "project | Write compose files: {{ project_definition.key }}"
- name: "project | Set project vars: {{ project_definition.key | split('_') | last }}"
ansible.builtin.set_fact:
project_name: "{{ project_definition.key | split('_') | last }}"
project_dir: "{{ ez_docker_project_dir }}/{{ project_definition.key | split('_') | last }}"
- name: "project | Create project directory: {{ project_name }}"
ansible.builtin.file:
path: "{{ project_dir }}"
state: directory
owner: root
group: docker
mode: "750"
- name: "project | Write compose files: {{ project_name }}"
snailed.ez_docker.compose:
name: "{{ project_definition.key | split('_') | last }}"
name: "{{ project_name }}"
services: "{{ project_definition.value }}"
settings: "{{ ez_docker_settings }}"
project_dir: "{{ ez_docker_project_dir }}"
no_log: true
project_dir: "{{ project_dir }}"
# no_log: true
register: compose
- name: "project | Start project: {{ project_definition.key }}"
- name: "project | Start project: {{ project_name }}"
community.docker.docker_compose_v2:
pull: missing
project_src: "{{ ez_docker_project_dir }}/{{ compose.after.name }}"
project_src: "{{ project_dir }}"
state: "present"
- name: "project | Import per-service tasks: {{ project_definition.key }}"
- name: "project | Import per-service tasks: {{ project_name }}"
ansible.builtin.include_tasks: service.yml
loop: compose.diff.after.services | dict2items
loop: "{{ compose.diff.after.services | dict2items }}"
loop_control:
loop_var: service
no_log: true
- name: "project | Restart docker projects: {{ project_definition.key }}"
- name: "project | Restart docker projects: {{ project_name }}"
community.docker.docker_compose_v2:
services: services_changed
project_src: "{{ ez_docker_project_dir }}/{{ project_definition.key }}"
project_src: "{{ project_dir }}"
state: "restarted"
when: services_changed

View file

@ -4,14 +4,14 @@
container: "{{ service.value.container_name }}"
container_path: "/{{ item.path }}"
content: >-
{{ snailed.ez_docker.slurp(
ez_cp_controller_files_root ~ "/" ~ project.key ~ "/" ~ service.key ~ "/" ~ item.path
{{ lookup('snailed.ez_docker.slurp',
ez_docker_controller_files_root ~ "/" ~ project_name ~ "/" ~ service.key ~ "/" ~ item.path
) }}
content_is_b64: true
mode: "{{ item.mode | int(base=8) }}"
when: item.state == "file"
with_community.general.filetree:
- "{{ ez_cp_controller_files_root }}/{{ project.key }}/{{ service.key }}"
- "{{ ez_docker_controller_files_root }}/{{ project_name }}/{{ service.key }}"
register: copy
- name: "service | Template files into container: {{ service.value.container_name }}"
@ -19,14 +19,14 @@
container: "{{ service.value.container_name }}"
container_path: "/{{ item.path }}"
content: >-
{{ ansible.builtin.template(
ez_cp_controller_templates_root ~ "/" ~ project.key ~ "/" ~ service.key ~ "/" ~ item.path,
convert_data=false
{{ lookup('ansible.builtin.template',
ez_docker_controller_templates_root ~ "/" ~ project_name ~ "/" ~ service.key ~ "/" ~ item.path,
'convert_data=false'
) }}
mode: "{{ item.mode | int(base=8) }}"
when: item.state == "file"
with_community.general.filetree:
- "{{ ez_cp_controller_templates_root }}/{{ project.key }}/{{ service.key }}"
- "{{ ez_docker_controller_templates_root }}/{{ project_name }}/{{ service.key }}"
register: template
- name: "service | Register file changes: {{ service.value.container_name }}"

188
test.json
View file

@ -2,66 +2,204 @@
"ANSIBLE_MODULE_ARGS": {
"_ansible_check_mode": true,
"_ansible_diff": true,
"name": "taskwarrior",
"name": "traefik",
"project_dir": "/var/lib/ez_compose",
"services": {
"custom": [
{
"definition": {
"image": "ghcr.io/gothenburgbitfactory/taskchampion-sync-server:main@sha256:4798edada4b264cdcc82f1c8ea2389cdd5cde02926f74b2361005438056f5729",
"networks": {
"proxy": null
"image": "traefik:v3.3.1",
"labels": {
"homepage.group": "Services",
"homepage.href": "https://traefik.snaile.de/",
"homepage.icon": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/traefik.png",
"homepage.name": "Traefik",
"homepage.widget.password": "redacted",
"homepage.widget.type": "traefik",
"homepage.widget.url": "https://traefik.snaile.de",
"homepage.widget.username": "luca"
},
"networks": {
"proxy": {}
},
"ports": [
{
"app_protocol": "smtp",
"mode": "host",
"name": "smtp",
"protocol": "tcp",
"published": 25,
"target": 25
},
{
"app_protocol": "http",
"mode": "host",
"name": "web",
"protocol": "tcp",
"published": 80,
"target": 80
},
{
"app_protocol": "https",
"mode": "host",
"name": "web-secure",
"protocol": "tcp",
"published": 443,
"target": 443
},
{
"app_protocol": "smtps",
"mode": "host",
"name": "smtp-ssl",
"protocol": "tcp",
"published": 465,
"target": 465
},
{
"app_protocol": "imaps",
"mode": "host",
"name": "imap-ssl",
"protocol": "tcp",
"published": 993,
"target": 993
},
{
"app_protocol": "ssh",
"mode": "host",
"name": "git-ssh",
"protocol": "tcp",
"published": 2222,
"target": 2222
},
{
"app_protocol": "managesieve",
"mode": "host",
"name": "sieve",
"protocol": "tcp",
"published": 4190,
"target": 4190
},
{
"mode": "host",
"name": "matrix-federation",
"protocol": "tcp",
"published": 8448,
"target": 8448
}
],
"volumes": [
{
"source": "sync_data",
"target": "/var/lib/taskchampion-sync-server",
"type": "volume"
"source": "/var/log/traefik",
"target": "/var/log/traefik",
"type": "bind"
},
{
"source": "/etc/traefik",
"target": "/etc/traefik",
"type": "bind"
},
{
"read_only": true,
"source": "/etc/localtime",
"target": "/etc/localtime",
"type": "bind"
},
{
"read_only": true,
"source": "/var/run/docker.sock",
"target": "/var/run/docker.sock",
"type": "bind"
}
]
},
"label_helpers": {
"docker_volume_backupper": {},
"traefik_router": {
"rule": "Host(`taskwarrior-sync.snailed.de`)"
}
"docker_volume_backupper": null,
"traefik_middleware": [
{
"middleware": "basicAuth",
"name": "traefik_basicauth",
"proxy_type": "http",
"settings": {
"users": "redacted"
}
}
],
"traefik_router": [
{
"certresolver": null,
"entrypoints": null,
"middlewares": ["traefik_basicauth"],
"name": null,
"proxy_type": "http",
"rule": "Host(`traefik.snaile.de`)",
"service": "api@internal"
}
],
"traefik_service": null
},
"name": "sync"
"name": "traefik"
}
],
"docker_volume_backupper": [
{
"backup_volumes": ["sync_data"]
}
]
"docker_in_docker": null,
"docker_socket_proxy": null,
"docker_volume_backupper": null,
"mariadb": null,
"postgres": null,
"redis": null
},
"settings": {
"external_networks": ["proxy"],
"default_definition": {
"environment": {
"TZ": "Europe/Berlin"
}
},
"label_default_args": {
"docker_volume_backupper": null,
"traefik_middleware": null,
"traefik_router": {
"certresolver": "letsencrypt",
"entrypoints": ["web-secure"]
}
"entrypoints": ["web-secure"],
"middlewares": ["hsts"],
"name": null,
"proxy_type": null,
"rule": null,
"service": null
},
"traefik_service": null
},
"service_default_args": {
"custom": null,
"docker_in_docker": null,
"docker_socket_proxy": null,
"docker_volume_backupper": {
"archive": "/tank/docker-backups"
}
"archive": "/tank/docker-backups",
"backup_volumes": null
},
"mariadb": null,
"postgres": null,
"redis": null
},
"service_default_definitions": {
"docker_volume_backupper": {
"environment": {
"BACKUP_CRON_EXPRESSION": "0 6 * * *",
"BACKUP_RETENTION_DAYS": "7"
"BACKUP_RETENTION_DAYS": "7",
"EXEC_FORWARD_OUTPUT": true,
"GPG_PASSPHRASE": "redacted",
"GZIP_PARALLELISM": "2",
"NOTIFICATION_URLS": "redacted",
"SSH_HOST_NAME": "redacted",
"SSH_PASSWORD": "redacted",
"SSH_PORT": "23",
"SSH_REMOTE_PATH": "/home/docker-backups",
"SSH_USER": "redacted"
},
"image": "offen/docker-volume-backup:v2.43.0"
"image": "offen/docker-volume-backup:v2.43.1"
},
"custom": null,
"docker_in_docker": {
"image": "docker:27.4.0-dind"
"image": "docker:27.4.1-dind"
},
"mariadb": {
"image": "mariadb:11.6.2"
@ -70,7 +208,7 @@
"image": "postgres:16.6-alpine"
},
"redis": {
"image": "redis:7.4.1-alpine"
"image": "redis:7.4.2-alpine"
},
"docker_socket_proxy": {
"image": "tecnativa/docker-socket-proxy:0.3.0"