This commit is contained in:
Luca Bilke 2025-01-18 23:31:34 +01:00
parent 2feabd9c23
commit 244cb48f91
Signed by: luca
GPG key ID: C9E851809C1A5BDE
28 changed files with 328 additions and 118 deletions

View file

@ -1 +1 @@
# Ansible Collection - snailed.ez_compose
# Ansible Collection - snailed.ez_docker

View file

@ -1,6 +1,6 @@
namespace: snailed
name: ez_compose
name: ez_docker
version: 1.0.0
@ -15,18 +15,18 @@ license:
- MIT
tags:
- linux
- infrastructure
- docker
- compose
- docker_compose
dependencies: {}
repository: http://git.snaile.de/snailed/ez_compose
repository: http://git.snaile.de/snailed/ez_docker
documentation: http://git.snaile.de/snailed/ez_compose/src/branch/main/docs
documentation: http://git.snaile.de/snailed/ez_docker/src/branch/main/docs
homepage: http://git.snaile.de/snailed/ez_compose
homepage: http://git.snaile.de/snailed/ez_docker
issues: http://git.snaile.de/snailed/ez_compose/issues
issues: http://git.snaile.de/snailed/ez_docker/issues
build_ignore: []

View file

@ -0,0 +1,73 @@
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# ruff: noqa: E402,ANN001,ANN003,ANN201
# pyright: basic
from __future__ import annotations
DOCUMENTATION = """
---
name: container_names
version_added: 1.0.0
short_description: Discover container names from ez_docker projects
description:
- This module is needed by ez_cp to find the names of containers to copy files into.
- It takes a project definition and returns a dict of name/container_name key/value pairs.
author:
- "Luca Bilke (@ssnailed)"
options:
_terms:
description:
- "name: definition dict of project to search in"
type: list
elements: dict
required: true
"""
EXAMPLES = """
- name: Discover container names in projects
ansible.builtin.set_fact:
containers: "{{ lookup('snailed.ez_docker.container_names', ez_docker_projects) }}"
"""
RETURN = """
_list:
description:
- List of discovered services in items format
- [{key: name, value: container_name}, ...]
type: list
"""
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
self.set_options(var_options=variables, direct=kwargs)
ret = []
for term in terms:
project_name = term.key
project = term.value
for service_name, service in project.items():
if not (overwrite := service.get("overwrite")):
overwrite = service.get("definition")
if overwrite and (container_name := overwrite.get("container_name")):
ret.append(
{
"key": service_name,
"value": container_name,
}
)
continue
ret.append(
{
"key": service_name,
"value": f"{project_name}_{service_name}",
}
)
return ret

74
plugins/lookup/slurp.py Normal file
View file

@ -0,0 +1,74 @@
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# ruff: noqa: E402,ANN001,ANN003,ANN201
# pyright: basic
from __future__ import annotations
DOCUMENTATION = """
---
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
author:
- "Luca Bilke (@ssnailed)"
options:
_terms:
description: path(s) of files to read
required: True
seealso:
- ref: playbook_task_paths
description: Search paths used for relative files.
"""
EXAMPLES = """
- ansible.builtin.debug:
msg: "{{ lookup('snailed.ez_docker.slurp', '/etc/foo.txt') | b64decode }}"
"""
RETURN = """
_list:
description:
- content of file(s)
type: list
elements: str
"""
import base64
from ansible.errors import AnsibleError, AnsibleLookupError, AnsibleOptionsError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display
display = Display()
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
ret = []
self.set_options(var_options=variables, direct=kwargs)
for term in terms:
display.debug(f"File lookup term: {term}")
try:
lookupfile = self.find_file_in_search_path(
variables, "files", term, ignore_missing=True,
)
display.vvvv(f"File lookup using {lookupfile} as file")
if lookupfile:
b_contents, _ = self._loader._get_file_contents(lookupfile) # noqa: SLF001 # pyright: ignore[reportOptionalMemberAccess]
contents = base64.b64encode(b_contents)
ret.append(contents)
else:
msg = "file not found, use -vvvvv to see paths searched"
raise AnsibleOptionsError(msg)
except AnsibleError as e:
msg = f"The 'file' lookup had an issue accessing the file '{term}'"
raise AnsibleLookupError(msg, orig_exc=e) # noqa: B904
return ret

View file

@ -9,7 +9,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, Any, cast
import yaml
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import Result, State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import Result, State
if TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]

View file

@ -5,13 +5,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import (
clean_none,
recursive_update,
)
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
BASE_ARGS: dict[str, Any] = {}

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"stop": {"type": "bool", "default": True},

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"proxy_type": {"type": "str", "default": "http"},

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"name": {"type": "str"},

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"proxy_type": {"type": "str", "default": "http"},

View file

@ -7,13 +7,13 @@ import copy
from dataclasses import replace
from typing import TYPE_CHECKING, Any, Callable
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_docker.plugins.module_utils.common import (
clean_none,
recursive_update,
)
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
BASE_ARGS: dict[str, Any] = {
"name": {"type": "str"},

View file

@ -5,11 +5,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import label, spec
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import clean_none
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_compose.plugins.module_utils.models import (
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import (
State,
)

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {}

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"read_only": {"type": "bool", "default": True},

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"archive": {"type": "path"},

View file

@ -7,7 +7,7 @@ import shlex
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"backup": {"type": "bool", "default": True},

View file

@ -7,7 +7,7 @@ import shlex
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {
"backup": {"type": "bool", "default": True},

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
from ansible_collections.snailed.ez_docker.plugins.module_utils.models import State
EXTRA_ARGS = {}

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from types import ModuleType
from ansible_collections.snailed.ez_compose.plugins.module_utils import (
from ansible_collections.snailed.ez_docker.plugins.module_utils import (
label,
service,
)

View file

@ -9,6 +9,7 @@ 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
DOCUMENTATION = """
---
module: compose
@ -456,7 +457,7 @@ options:
from importlib.util import find_spec
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
from ansible_collections.snailed.ez_compose.plugins.module_utils import (
from ansible_collections.snailed.ez_docker.plugins.module_utils import (
common,
service,
spec,
@ -473,7 +474,7 @@ def main() -> None:
},
"project_dir": {
"type": "path",
"default": "/var/lib/ez_compose",
"default": "/var/lib/ez_docker",
},
"settings": spec.settings_spec(),
"services": spec.service_argument_spec(),

View file

@ -14,18 +14,13 @@ ignore = [
"COM812",
"D100",
"D101",
"D102",
"D103",
"D104",
"D203",
"D213",
]
[tool.ruff.lint.per-file-ignores]
"tests/units/**" = [
"S101",
"PT009",
]
[tool.ruff.format]
line-ending = "lf"

View file

@ -1,74 +0,0 @@
---
- name: Login in to registries
community.docker.docker_login:
registry_url: "{{ item.registry }}"
username: "{{ item.username }}"
password: "{{ item.password }}"
loop: "{{ compose_registry_logins }}"
no_log: true
when: >-
ez_compose_state | default(None) == "present"
and ez_compose_registry_logins is defined
and ez_compose_registry_logins
- name: Ensure shared networks exist
community.docker.docker_network:
name: "{{ item.name }}"
attachable: "{{ item.attachable | default(omit) }}"
connected: "{{ item.connected | default(omit) }}"
driver: "{{ item.driver | default(omit) }}"
driver_options: "{{ item.driver_options | default(omit) }}"
enable_ipv6: "{{ item.enable_ipv6 | default(omit) }}"
force: "{{ item.force | default(omit) }}"
ipam_config: "{{ item.ipam_config | default(omit) }}"
ipam_driver: "{{ item.ipam_driver | default(omit) }}"
labels: "{{ item.labels | default(omit) }}"
scope: "{{ item.scope | default(omit) }}"
state: "{{ item.state | default(omit) }}"
loop: "{{ ez_compose_shared_networks }}"
when: >-
ez_compose_state | default(None) == "present"
and ez_compose_shared_networks is defined
and ez_compose_shared_networks
- name: Ensure shared volumes exist
community.docker.docker_network:
name: "{{ item.name }}"
driver: "{{ item.driver | default(omit) }}"
driver_options: "{{ item.driver_options | default(omit) }}"
labels: "{{ item.labels | default(omit) }}"
recreate: "{{ item.recreate | default(omit) }}"
state: "{{ item.state | default(omit) }}"
loop: "{{ ez_compose_shared_volumes }}"
when: >-
ez_compose_state | default(None) == "present"
and ez_compose_shared_volumes is defined
and ez_compose_shared_volumes
- name: Discover project definitions
ansible.builtin.set_fact:
ez_compose_projects: >-
{{
hostvars[inventory_hostname]
| dict2items
| selectattr(
'key', 'in', (
hostvars[inventory_hostname].keys()
| select('match', '^ez_compose_project_')
)
)
}}
when: >-
ez_compose_projects is not defined
or not ez_compose_projects
- name: Import project tasks
ansible.builtin.include_tasks: project.yml
when: >-
[project.key | split('_') | last, 'compose-all'] | intersect(ansible_run_tags) | length > 0
and not
[project.key | split('_') | last, 'compose-all'] | intersect(ansible_skip_tags) | length > 0
loop: "{{ ez_compose_projects }}"
loop_control:
loop_var: project
# no_log: true

View file

@ -1,7 +0,0 @@
---
- name: "project | Write compose file: {{ project.name }}"
snailed.ez_compose.compose:
name: "{{ project.key | split('_') | last }}"
services: "{{ project.value }}"
settings: "{{ ez_compose_settings }}"
project_dir: "{{ ez_compose_project_dir }}"

View file

@ -0,0 +1,49 @@
---
# Registries to log in to with community.docker.docker_login
ez_docker_registry_logins: []
# - username: "example"
# password: "example"
# registry_url: "example.registry.url"
# A list of docker compose network definitions to create with
# community.docker.docker_network and mark as external in projects
ez_docker_shared_networks: []
# - name: "shared_network"
# A list of docker compose volume definitions to create with
# community.docker.docker_volume and mark as external in projects
ez_docker_shared_volumes: []
# - name: "shared_volume"
# Directory to put compose projects into
ez_docker_project_dir: "/var/lib/ez_compose"
# Settings for the module
ez_docker_settings:
default_definition: {}
# environment:
# TZ: "Europe/Berlin"
label_default_args: {}
# traefik_router:
# certresolver: "letsencrypt"
# entrypoints: ["web-secure"]
service_default_definitions: {}
# docker_volume_backupper:
# image: "offen/docker-volume-backup:v2.43.0"
# environment:
# BACKUP_CRON_EXPRESSION: "0 6 * * *"
# BACKUP_RETENTION_DAYS: "7"
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

View file

@ -0,0 +1,38 @@
---
- name: Discover project definitions
ansible.builtin.set_fact:
ez_docker_projects: >-
{{
hostvars[inventory_hostname]
| dict2items
| selectattr(
'key', 'in', (
hostvars[inventory_hostname].keys()
| select('match', '^ez_docker_project_')
)
)
}}
when: not ez_docker_projects
- name: Login in to registries
community.docker.docker_login: "{{ item }}" # noqa: args[module]
loop: "{{ ez_docker_registry_logins }}"
no_log: true
when: ez_docker_registry_logins
- name: Ensure shared networks exist # noqa: args[module]
community.docker.docker_network: "{{ item }}"
loop: "{{ ez_docker_shared_networks }}"
when: ez_docker_shared_networks
- name: Ensure shared volumes exist # noqa: args[module]
community.docker.docker_volume: "{{ item }}"
loop: "{{ ez_docker_shared_volumes }}"
when: ez_docker_shared_volumes
- name: Import per-project tasks
ansible.builtin.include_tasks: project.yml
loop: "{{ ez_docker_projects }}"
loop_control:
loop_var: project_definition
no_log: true

View file

@ -0,0 +1,27 @@
---
- name: "project | Write compose files: {{ project_definition.key }}"
snailed.ez_docker.compose:
name: "{{ project_definition.key | split('_') | last }}"
services: "{{ project_definition.value }}"
settings: "{{ ez_docker_settings }}"
project_dir: "{{ ez_docker_project_dir }}"
no_log: true
- name: "project | Start project: {{ project.name }}"
community.docker.docker_compose_v2:
pull: missing
project_src: "{{ ez_docker_project_dir }}/{{ project_definition.key }}"
state: "present"
- name: "project | Import per-service tasks: {{ project_definition.key }}"
ansible.builtin.include_tasks: service.yml
with_snailed.ez_docker.container_names: "{{ project_definition }}"
loop_control:
loop_var: service
- name: "project | Restart docker projects: {{ project_definition.key }}"
community.docker.docker_compose_v2:
services: services_changed
project_src: "{{ ez_docker_project_dir }}/{{ project_definition.key }}"
state: "restarted"
when: services_changed

View file

@ -0,0 +1,34 @@
---
- name: "service | Copy files into container: {{ service.key }}"
community.docker.docker_container_copy_into:
container: "{{ service.value }}"
container_path: "/{{ item.path }}"
content: >-
{{ snailed.ez_docker.slurp(
ez_cp_controller_files_root ~ "/" ~ project.key ~ "/" ~ 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 }}"
register: copy
- name: "service | Template files into container: {{ service.key }}"
community.docker.docker_container_copy_into:
container: "{{ service.value }}"
container_path: "/{{ item.path }}"
content: >-
{{ ansible.builtin.template(
ez_cp_controller_templates_root ~ "/" ~ project.key ~ "/" ~ 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 }}"
register: template
- name: "service | Register file changes: {{ service.key }}"
ansible.builtin.set_fact:
services_changed: "{{ services_changed | default([]) + [service.key] }}"

View file

@ -23,7 +23,6 @@
"label_helpers": {
"docker_volume_backupper": {},
"traefik_router": {
"entrypoints": ["web-secure"],
"rule": "Host(`taskwarrior-sync.snailed.de`)"
}
},
@ -44,7 +43,8 @@
},
"label_default_args": {
"traefik_router": {
"certresolver": "letsencrypt"
"certresolver": "letsencrypt",
"entrypoints": ["web-secure"]
}
},
"service_default_args": {