This commit is contained in:
Luca Bilke 2024-12-28 21:22:44 +01:00
parent edce5ceee0
commit 508a9db2f2
16 changed files with 674 additions and 90 deletions

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import ansible_collections.ssnailed.ez_compose.plugins.module_utils.service.common as service
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -28,4 +28,4 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
"docker-volume-backup.stop-during-backup": project_name,
}
return service.update(service_name, state, update)
return service.common.update(state, {"name": service_name}, update)

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import ansible_collections.ssnailed.ez_compose.plugins.module_utils.service.common as service
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -38,4 +38,4 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
"labels": labels,
}
return service.update(service_name, state, update)
return service.common.update(state, {"name": service_name}, update)

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import ansible_collections.ssnailed.ez_compose.plugins.module_utils.service.common as service
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -59,4 +59,4 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
"labels": labels,
}
return service.update(service_name, state, update)
return service.common.update(state, {"name": service_name}, update)

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
import ansible_collections.ssnailed.ez_compose.plugins.module_utils.service.common as service
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -39,4 +39,4 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
"labels": labels,
}
return service.update(service_name, state, update)
return service.common.update(state, {"name": service_name}, update)

View file

@ -20,7 +20,6 @@ if TYPE_CHECKING:
BASE_ARGS: dict[str, Any] = {
"name": {"type": "str", "required": True},
"image": {"type": "str", "required": True},
"defaults": {"type": "dict"},
"overwrite": {"type": "dict"},
}
@ -36,18 +35,16 @@ def apply_base(state: State, params: dict[str, Any]) -> State:
"environment": {},
"labels": {},
"volumes": [],
"networks": {
"networks": {
"internal": None,
},
}
return update(params["name"], state, new)
return update(state, params, new)
def set_definition(state: State, params: dict[str, Any], param_name: str) -> State:
def apply_definition(state: State, params: dict[str, Any], definition: dict[str, Any]) -> State:
service_name: str = params["name"]
definition: dict[str, Any] = params.get(param_name, {})
project = copy.deepcopy(state.after)
services: dict[str, Any] = project["services"]
service: dict[str, Any] = services[service_name]
@ -59,7 +56,15 @@ def set_definition(state: State, params: dict[str, Any], param_name: str) -> Sta
return replace(state, after=project)
def update(service_name: str, state: State, update: dict[str, Any]) -> State:
def apply_settings(state: State, params: dict[str, Any]) -> State:
update = (
state.module.params.get("settings", {}).get("service_defaults", {}).get(params["name"], {})
)
return update(state, params, update)
def update(state: State, params: dict[str, Any], update: dict[str, Any]) -> State:
service_name: str = params["name"]
project = copy.deepcopy(state.after)
_ = recursive_update(project["services"][service_name], update)
@ -73,7 +78,7 @@ def run_helper(
helper: Callable[[State, dict[str, Any]], State] = lambda x, _: x,
) -> State:
state = apply_base(state, params)
state = set_definition(state, params, "defaults")
state = apply_settings(state, params)
state = helper(state, params)
state = set_definition(state, params, "overwrite")
state = apply_definition(state, params, params.get("overwrite", {}))
return update_project(state)

View file

@ -36,4 +36,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
for name, args in params["label_helpers"].items():
state = getattr(label, name).helper(state, params["name"], args)
return service.common.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -5,9 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.service import (
common as service,
)
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -22,4 +20,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
"privileged": True,
}
return service.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -5,9 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.service import (
common as service,
)
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -35,4 +33,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
"volumes": volumes,
}
return service.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -5,9 +5,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.service import (
common as service,
)
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -60,4 +58,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
"volumes": volumes,
}
return service.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -6,9 +6,7 @@ from __future__ import annotations
import shlex
from typing import TYPE_CHECKING, Any
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.service import (
common as service,
)
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -84,4 +82,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
"labels": labels,
}
return service.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -6,9 +6,7 @@ from __future__ import annotations
import shlex
from typing import TYPE_CHECKING, Any
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.service import (
common as service,
)
from ansible_collections.ssnailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.ssnailed.ez_compose.plugins.module_utils.common import (
@ -75,4 +73,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
"labels": labels,
}
return service.update(params["name"], state, update)
return service.common.update(state, params, update)

View file

@ -6,36 +6,15 @@
from __future__ import annotations
SERVICE_BASE_DOCS = """
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
"""
LABEL_BASE_DOCS = """
type: dict
suboptions:
"""
DOCUMENTATION = f"""
DOCUMENTATION = """
---
module: compose
version_added: 1.0.0
short_description: Simplify docker-compose deployments
seealso:
- name: Compose file reference
description: Complete reference of the compose file spec.
link: https://docs.docker.com/reference/compose-file/
description:
- Easily create docker-compose files using a single module
author:
@ -55,16 +34,75 @@ options:
description:
- Path to store project directory under.
type: path
settings:
description:
- Settings/Defaults for the module.
type: dict
suboptions:
default_definition:
description:
- Default definition for all containers.
- Overwritten by per-service defaults.
type: dict
service_defaults:
description:
- Default definitions for each service
type: dict
suboptions:
custom:
description:
- Default definitions for custom services.
type: dict
docker_in_docker:
description:
- Default definitions for docker in docker services.
type: dict
docker_socket_proxy:
description:
- Default definitions for docker socket proxy services.
type: dict
docker_volume_backupper:
description:
- Default definitions for docker volume backupper services.
type: dict
mariadb:
description:
- Default definitions for MariaDB services.
type: dict
postgres:
description:
- Default definitions for PostgreSQL services.
type: dict
redis:
description:
- Default definitions for Redis services.
type: dict
services:
description:
- Services to create in the project.
type: list
elements: dict
type: dict
suboptions:
custom:
description:
- Custom service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
internal_network:
description:
- If true, add internal network to service.
@ -78,7 +116,8 @@ options:
docker_volume_backupper:
description:
- Docker Volume Backupper label helper configuration.
{LABEL_BASE_DOCS}
type: dict
suboptions:
stop:
description:
- If true, stop the container when backing up.
@ -87,7 +126,8 @@ options:
traefik_middleware:
description:
- Traefik Middleware label helper configuration.
{LABEL_BASE_DOCS}
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
@ -97,8 +137,6 @@ options:
name:
description:
- Name of the middleware.
- Default is generated dynamically like so:
- [project_name]_[service_name]_[proxy_type]_[middleware]
type: string
middleware:
description:
@ -113,7 +151,8 @@ options:
traefik_router:
description:
- Traefik Router label helper configuration.
{LABEL_BASE_DOCS}
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
@ -123,8 +162,6 @@ options:
name:
description:
- Name of the middleware.
- Default is generated dynamically like so:
- [project_name]_[service_name]_[proxy_type]
type: string
rule:
description:
@ -152,7 +189,8 @@ options:
traefik_service:
description:
- Traefik Service label helper configuration.
{LABEL_BASE_DOCS}
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
@ -162,8 +200,6 @@ options:
name:
description:
- Name of the middleware.
- Default is generated dynamically like so:
- [project_name]_[service_name]_[proxy_type]
type: string
port:
description:
@ -172,11 +208,45 @@ options:
docker_in_docker:
description:
- Docker-in-Docker service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
docker_socket_proxy:
description:
- Docker Socket Proxy service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
read_only:
description:
- If true, only allow read access to the docker socket.
@ -185,7 +255,24 @@ options:
docker_volume_backupper:
description:
- Docker Socket Proxy service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
archive:
description:
- Directory to store backups in.
@ -198,7 +285,24 @@ options:
mariadb:
description:
- MariaDB service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
backup:
description:
- If true, add labels for the docker volume backupper.
@ -225,7 +329,24 @@ options:
postgres:
description:
- PostgreSQL service definition.
{SERVICE_BASE_DOCS}
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
backup:
description:
- If true, add labels for the docker volume backupper.
@ -248,8 +369,26 @@ options:
redis:
description:
- Redis service definition.
{SERVICE_BASE_DOCS}
""" # noqa: E501
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
"""
from typing import Any
@ -287,7 +426,7 @@ def label_argument_spec() -> dict[str, Any]:
def service_argument_spec() -> dict[str, Any]:
service_args: dict[str, Any] = {
"type": "dict",
"options": {},
"suboptions": {},
"required": True,
}
@ -300,18 +439,43 @@ def service_argument_spec() -> dict[str, Any]:
**getattr(service, module).EXTRA_ARGS,
}
# TODO: move to service.common
if module == "custom":
options["label_helpers"] = label_argument_spec()
service_args["options"][module] = {
"type": "list",
"elements": "dict",
"options": options,
service_args["suboptions"][module] = {
"type": "dict",
"suboptions": options,
}
return service_args
def settings_spec() -> dict[str, Any]:
settings: dict[str, Any] = {
"type": "dict",
"suboptions": {
"default_definition": {
"type": "dict",
},
"service_defaults": {
"type": "dict",
"suboptions": {},
},
},
}
for module in service.__all__:
if module == "common":
continue
settings["suboptions"]["service_defaults"]["suboptions"][module] = {
"type": "dict",
}
return settings
def main() -> None:
module = AnsibleModule(
argument_spec={
@ -324,6 +488,7 @@ def main() -> None:
"type": "path",
"default": "/var/lib/ez_compose",
},
"settings": settings_spec(),
"services": service_argument_spec(),
},
)

View file

View file

@ -0,0 +1,75 @@
---
- 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_shared_registry_logins is defined
and ez_compose_shared_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:
_project_keys: >-
{{
hostvars[inventory_hostname].keys()
| select('match', '^ez_compose_project_')
}}
ez_compose_projects: >-
{{
hostvars[inventory_hostname]
| dict2items
| selectattr('key', 'in', _project_keys)
| items2dict
}}
when: >-
ez_compose_projects is not defined
or not ez_compose_projects
- name: Import project tasks
ansible.builtin.include_tasks: project.yml
when: >-
[project.name, 'compose-all'] | intersect(ansible_run_tags) | length > 0
and not
[project.name, 'compose-all'] | intersect(ansible_skip_tags) | length > 0
loop: "{{ ez_compose_projects }}"
loop_control:
loop_var: project
no_log: true

View file

@ -0,0 +1,7 @@
---
- name: "project | Write compose file: {{ project.name }}"
ssnailed.ez_compose.compose:
name: "{{ project.name }}"
services: "{{ project.services }}"
settings: "{{ ez_compose_settings }}"
project_dir: "{{ ez_compose_project_dir }}"

342
test.yml Normal file
View file

@ -0,0 +1,342 @@
---
module: compose
short_description: Simplify docker-compose deployments
description:
- Easily create docker-compose files using a single module
author:
- "Luca Bilke (@ssnailed)"
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
name:
description:
- Name of the compose project to create or modify.
aliases: [project]
type: str
project_dir:
description:
- Path to store project directory under.
type: path
services:
description:
- Services to create in the project.
type: list
elements: dict
suboptions:
custom:
description:
- Custom service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
internal_network:
description:
- If true, add internal network to service.
type: bool
default: false
label_helpers:
description:
- Label helper configurations.
type: dict
suboptions:
docker_volume_backupper:
description:
- Docker Volume Backupper label helper configuration.
type: dict
suboptions:
stop:
description:
- If true, stop the container when backing up.
type: bool
default: true
traefik_middleware:
description:
- Traefik Middleware label helper configuration.
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
type: str
choices: [http, tcp, udp]
default: http
name:
description:
- Name of the middleware.
- 'Default is generated dynamically like so:
{{ project_name }}_{{ service_name }}_{{ proxy_type }}_{{ middleware }}'
type: string
middleware:
description:
- The traefik middleware to use.
type: str
required: true
settings:
description:
- Middleware options.
type: dict
required: true
traefik_router:
description:
- Traefik Router label helper configuration.
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
type: str
choices: [http, tcp, udp]
default: http
name:
description:
- Name of the middleware.
- 'Default is generated dynamically like so:
"{{ project_name }}_{{ service_name }}_{{ proxy_type }}"'
type: string
rule:
description:
- Routing rule to match.
type: str
required: true
service:
description:
- Traefik service to point at.
type: str
certresolver:
description:
- Certresolver to use.
type: str
entrypoints:
description:
- Entrypoints to listen on.
type: list
elements: str
middlewares:
description:
- Middlewares to use.
type: list
elements: str
traefik_service:
description:
- Traefik Service label helper configuration.
type: dict
suboptions:
proxy_type:
description:
- Traefik proxy type.
type: str
choices: [http, tcp, udp]
default: http
name:
description:
- Name of the middleware.
- 'Default is generated dynamically like so:
"{{ project_name }}_{{ service_name }}_{{ proxy_type }}"'
type: string
port:
description:
- Port to forward to.
type: int
docker_in_docker:
description:
- Docker-in-Docker service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
docker_socket_proxy:
description:
- Docker Socket Proxy service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
read_only:
description:
- If true, only allow read access to the docker socket.
type: bool
default: true
docker_volume_backupper:
description:
- Docker Socket Proxy service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
archive:
description:
- Directory to store backups in.
type: path
backup_volumes:
description:
- List of volume names of volumes to backup.
type: list
elements: str
mariadb:
description:
- MariaDB service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
backup:
description:
- If true, add labels for the docker volume backupper.
type: bool
database:
description:
- Name of database.
type: str
required: true
username:
description:
- Username for database.
type: str
required: true
password:
description:
- Password for database.
type: str
required: true
root_password:
description:
- Root password for database.
type: str
postgres:
description:
- PostgreSQL service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict
backup:
description:
- If true, add labels for the docker volume backupper.
type: bool
database:
description:
- Name of database.
type: str
required: true
username:
description:
- Username for database.
type: str
required: true
password:
description:
- Password for database.
type: str
required: true
redis:
description:
- Redis service definition.
type: dict
suboptions:
name:
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:
description:
- Service definition to overwrite with.
type: dict