This commit is contained in:
Luca Bilke 2025-01-13 17:50:50 +01:00
parent 729cb75ab4
commit 2735859028
16 changed files with 211 additions and 171 deletions

View file

@ -4,31 +4,17 @@
from __future__ import annotations
import copy
from dataclasses import dataclass, field, replace
from dataclasses import replace
from pathlib import Path
from typing import TYPE_CHECKING, Any
import yaml
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import Result, State
if TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
@dataclass(frozen=True)
class Result:
changed: bool = False
diff: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class State:
module: AnsibleModule
result: Result
compose_filepath: str
before: dict[str, Any]
after: dict[str, Any]
def recursive_update(
default: dict[Any, Any],
update: dict[Any, Any],
@ -67,6 +53,9 @@ def get_state(module: AnsibleModule) -> State:
before=before,
after={
"name": module.params["name"],
"services": {},
"networks": {},
"volumes": {},
},
)
@ -75,12 +64,12 @@ def update_project(state: State) -> State:
"""Ensure that networks/volumes that exist in services also exist in the project."""
project = copy.deepcopy(state.after)
project_services = project.get("services", {})
project_networks = project.get("networks", {})
project_volumes = project.get("volumes", {})
project_services: dict[str, Any] = project.get("services", {})
project_networks: dict[str, Any] = project.get("networks", {})
project_volumes: dict[str, Any] = project.get("volumes", {})
for service in project_services:
if service_volumes := 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(
{
@ -90,7 +79,7 @@ def update_project(state: State) -> State:
},
)
if service_network_names := service.get("networks").keys():
if service_network_names := project_service.get("networks", {}).keys():
project_networks.update(
{
service_network_name: None

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -16,6 +16,8 @@ BASE_ARGS: dict[str, Any] = {}
def run_helper(
state: State,
helper: Callable[[State], State] = lambda _: _,
service_name: str,
params: dict[str, Any],
helper: Callable[[State, str, dict[str, Any]], State] = lambda a, _b, _c: a,
) -> State:
return helper(state)
return helper(state, service_name, params)

View file

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)

View file

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -25,9 +25,9 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
middleware: str = params["middleware"]
settings: dict[str, str] = params["settings"]
proxy_type: str = state.module.params["proxy_type"]
name: str = state.module.params.get(
"name",
f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}",
name: str = (
params.get("name")
or f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}"
)
prefix = f"traefik.{proxy_type}.middlewares.{name}"

View file

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -31,10 +31,7 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
middlewares: list[str] | None = params.get("middlewares")
certresolver: str | None = params.get("certresolver")
proxy_type: str = params["proxy_type"]
name: str = params.get(
"name",
f"{project_name}_{service_name}_{proxy_type}",
)
name: str = params.get("name") or f"{project_name}_{service_name}_{proxy_type}"
prefix = f"traefik.{proxy_type}.routers.{name}"

View file

@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -23,10 +23,7 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
project_name: str = state.module.params["name"]
port: int | None = params.get("port")
proxy_type: str = params["proxy_type"]
name: str = params.get(
"name",
f"{project_name}_{service_name}_{proxy_type}",
)
name: str = params.get("name") or f"{project_name}_{service_name}_{proxy_type}"
prefix = f"traefik.{proxy_type}.services.{name}"

View file

@ -0,0 +1,25 @@
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
@dataclass(frozen=True)
class Result:
changed: bool = False
diff: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class State:
module: AnsibleModule
result: Result
compose_filepath: str
before: dict[str, Any]
after: dict[str, Any]

View file

@ -1,3 +1,6 @@
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
__all__ = [
"common",
"custom",

View file

@ -13,13 +13,12 @@ from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
)
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
BASE_ARGS: dict[str, Any] = {
"name": {"type": "str", "required": True},
"image": {"type": "str", "required": True},
"name": {"type": "str"},
"overwrite": {"type": "dict"},
}
@ -30,7 +29,6 @@ def apply_base(state: State, params: dict[str, Any]) -> State:
new: dict[str, Any] = {
"container_name": f"{project_name}_{params['name']}",
"hostname": f"{project_name}_{params['name']}",
"image": params["image"],
"restart": "unless-stopped",
"environment": {},
"labels": {},
@ -75,14 +73,17 @@ def update(state: State, params: dict[str, Any], update: dict[str, Any]) -> Stat
service_name: str = params["name"]
project = copy.deepcopy(state.after)
project["services"][service_name] = project["services"].get(service_name, {})
_ = recursive_update(project["services"][service_name], update)
# FIX: this silently throws out misconfigured volumes
unique_volumes = {
vol["target"]: vol
for vol in project["services"][service_name].get("volumes", [])
if "target" in vol
}.values()
unique_volumes = dict(
{
vol["target"]: vol
for vol in project["services"][service_name].get("volumes", [])
if "target" in vol
}.values(),
)
project["services"][service_name]["volumes"] = unique_volumes
return replace(state, after=project)
@ -93,8 +94,14 @@ def run_helper(
params: dict[str, Any],
helper: Callable[[State, dict[str, Any]], State] = lambda x, _: x,
) -> State:
if not params.get("name"):
params["name"] = str.split(helper.__module__, ".")[-1]
if not params.get("overwrite"):
params["overwrite"] = params.get("definition", {})
state = apply_base(state, params)
state = apply_settings(state, params)
state = helper(state, params)
state = apply_definition(state, params, params.get("overwrite", {}))
state = apply_definition(state, params, params["overwrite"])
return update_project(state)

View file

@ -5,19 +5,18 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import (
label,
service,
)
from ansible_collections.snailed.ez_compose.plugins.module_utils import label, service, spec
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
# NOTE: Label helper arguments are added in the compose module itself
EXTRA_ARGS = {
FORCE_ARGS = {
"name": {"type": "str", "required": True},
"definition": {"type": "dict", "required": True},
"internal_network": {"type": "bool", "default": False},
"label_helpers": spec.label_argument_spec(),
}
@ -33,7 +32,8 @@ def helper(state: State, params: dict[str, Any]) -> State:
update["networks"] = networks
for name, args in params["label_helpers"].items():
state = getattr(label, name).helper(state, params["name"], args)
for name, args in [(x, y) for x, y in params.get("label_helpers", {}).items() if y]:
helper = getattr(label, name).helper
state = label.common.run_helper(state, params["name"], args, helper)
return service.common.update(state, params, update)

View file

@ -8,21 +8,21 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
EXTRA_ARGS = {
"archive": {"type": "path"},
"backup_volumes": {"type": "list", "elements": "str"},
"backup_volumes": {"type": "list", "elements": "str", "required": True},
}
def helper(state: State, params: dict[str, Any]) -> State:
project_name: str = state.module.params["name"]
archive: str | None = params.get("archive")
backup_volumes: list[str] | None = params.get("backup_volumes", [])
backup_volumes: list[str] | None = params["backup_volumes"]
service_name = params["name"]
project_name = params["project_name"]
volumes: list[dict[str, Any]] = [
{

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -23,7 +23,7 @@ EXTRA_ARGS = {
def helper(state: State, params: dict[str, Any]) -> State:
project_name: str = state.module.params["project_name"]
project_name: str = state.module.params["name"]
backup: bool = params["backup"]
database: str = params["database"]
username: str = params["username"]

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from ansible_collections.snailed.ez_compose.plugins.module_utils import service
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)
@ -22,7 +22,7 @@ EXTRA_ARGS = {
def helper(state: State, params: dict[str, Any]) -> State:
project_name: str = state.module.params["project_name"]
project_name: str = state.module.params["name"]
backup: bool = params["backup"]
database: str = params["database"]
username: str = params["username"]

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
State,
)

View file

@ -0,0 +1,112 @@
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
from __future__ import annotations
from copy import deepcopy
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from types import ModuleType
from ansible_collections.snailed.ez_compose.plugins.module_utils import (
label,
service,
)
def get_modules(parent_module: ModuleType) -> list[tuple[str, ModuleType]]:
return [
(name, getattr(parent_module, name)) for name in parent_module.__all__ if name != "common"
]
def get_module_options(
module: ModuleType,
base_args: dict[str, Any] | None = None,
) -> dict[str, Any]:
if not base_args:
base_args = {}
if force_args := getattr(module, "FORCE_ARGS", None):
return force_args
if extra_args := getattr(module, "EXTRA_ARGS", None):
return {
**base_args,
**extra_args,
}
return base_args
def label_argument_spec() -> dict[str, Any]:
label_args: dict[str, Any] = {
"type": "dict",
"options": {},
}
for module_name, module in get_modules(label):
label_args["options"][module_name] = {
"type": "dict",
"options": get_module_options(module, label.common.BASE_ARGS),
}
return label_args
def service_argument_spec() -> dict[str, Any]:
service_args: dict[str, Any] = {
"type": "dict",
"options": {},
"required": True,
}
for module_name, module in get_modules(service):
service_args["options"][module_name] = {
"type": "list",
"elements": "dict",
"options": get_module_options(module, service.common.BASE_ARGS),
}
return service_args
def settings_spec() -> dict[str, Any]:
settings: dict[str, Any] = {
"type": "dict",
"options": {
"default_definition": {"type": "dict"},
"service_default_args": {"type": "dict", "options": {}},
"label_default_args": {"type": "dict", "options": {}},
"service_default_definitions": {"type": "dict", "options": {}},
},
}
for module_name, module in get_modules(service):
settings["options"]["service_default_definitions"]["options"][module_name] = {
"type": "dict",
}
service_args: dict[str, Any] = deepcopy(get_module_options(module))
for arg in service_args.values():
arg.pop("required", None)
settings["options"]["service_default_args"]["options"][module_name] = {
"type": "dict",
"options": service_args,
}
for module_name, module in get_modules(label):
label_args: dict[str, Any] = deepcopy(get_module_options(module))
for arg in label_args.values():
arg.pop("required", None)
settings["options"]["label_default_args"]["options"][module_name] = {
"type": "dict",
"options": label_args,
}
return settings

View file

@ -4,7 +4,10 @@
# ruff: noqa: E402
from __future__ import annotations
from pprint import pp
# TODO: break this down per module
# TODO: generate this by reassembling
DOCUMENTATION = """
---
module: compose
@ -449,113 +452,16 @@ options:
type: dict
"""
from copy import deepcopy
from importlib.util import find_spec
from typing import Any
from ansible.module_utils.basic import AnsibleModule # pyright: ignore[reportMissingTypeStubs]
from ansible_collections.snailed.ez_compose.plugins.module_utils import (
common,
label,
service,
spec,
)
def label_argument_spec() -> dict[str, Any]:
label_args: dict[str, Any] = {
"type": "dict",
"options": {},
}
for module in label.__all__:
if module == "common":
continue
options = {
**label.common.BASE_ARGS,
**getattr(label, module).EXTRA_ARGS,
}
label_args["options"][module] = {
"type": "dict",
"options": options,
}
return label_args
def service_argument_spec() -> dict[str, Any]:
service_args: dict[str, Any] = {
"type": "list",
"elements": "dict",
"suboptions": {},
"required": True,
}
for module in service.__all__:
if module == "common":
continue
options = {
**service.common.BASE_ARGS,
**getattr(service, module).EXTRA_ARGS,
}
# TODO: move to service.common
if module == "custom":
options["label_helpers"] = label_argument_spec()
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_default_args": {"type": "dict", "suboptions": {}},
"service_default_definitions": {"type": "dict", "suboptions": {}},
},
}
for module in service.__all__:
if module == "common":
continue
settings["suboptions"]["service_default_definitions"]["suboptions"][module] = {
"type": "dict",
}
args = deepcopy(getattr(service, module).EXTRA_ARGS)
for arg in args.values():
arg.pop("required", None)
settings["suboptions"]["service_default_args"]["suboptions"][module] = {
"type": "dict",
"suboptions": args,
}
for module in label.__all__:
if module == "common":
continue
args = deepcopy(getattr(label, module).EXTRA_ARGS)
for arg in args.values():
arg.pop("required", None)
settings["suboptions"]["label_default_args"]["suboptions"][module] = {
"type": "dict",
"suboptions": args,
}
return settings
def main() -> None:
module = AnsibleModule(
argument_spec={
@ -568,8 +474,8 @@ def main() -> None:
"type": "path",
"default": "/var/lib/ez_compose",
},
"settings": settings_spec(),
"services": service_argument_spec(),
"settings": spec.settings_spec(),
"services": spec.service_argument_spec(),
},
)
@ -578,9 +484,11 @@ def main() -> None:
state = common.get_state(module)
for name, definitions in module.params["services"].items():
for definition in definitions:
state = getattr(service, name).helper(state, definition)
for name, services_params in [(x, y) for x, y in module.params["services"].items() if y]:
for service_params in services_params:
helper = getattr(service, name).helper
state = service.common.run_helper(state, service_params, helper)
pp(state.after)
if __name__ == "__main__":