WIP
This commit is contained in:
parent
5750a5a495
commit
c5c0e3f6f3
25 changed files with 301 additions and 283 deletions
.lazy.luaLICENSEpyproject.tomlrequirements-dev.txt
plugins
module_utils
common.py
label
__init__.pycommon.pydocker_volume_backupper.pytraefik_middleware.pytraefik_router.pytraefik_service.py
models.pyservice
__init__.pycommon.pycustom.pydocker_in_docker.pydocker_socket_proxy.pydocker_volume_backupper.pymariadb.pypostgres.pyredis.py
spec.pymodules
test.jsontests/units/plugins/module_utils
|
@ -19,7 +19,7 @@ return {
|
|||
type = "python",
|
||||
request = "launch",
|
||||
name = "custom",
|
||||
program = root .. "/modules/compose.py",
|
||||
program = root .. "/plugins/modules/compose.py",
|
||||
console = "integratedTerminal",
|
||||
pythonPath = os.getenv("VIRTUAL_ENV") .. "/bin/python",
|
||||
args = { root .. "/test.json" },
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright 2024 Luca Bilke
|
||||
Copyright 2025 Luca Bilke
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
import copy
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import yaml
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import Result, State
|
||||
|
@ -21,22 +21,19 @@ def recursive_update(
|
|||
) -> dict[Any, Any]:
|
||||
default = copy.deepcopy(default)
|
||||
|
||||
for key in update: # noqa: PLC0206
|
||||
if isinstance(update[key], dict) and (
|
||||
isinstance(default.get(key), dict) or default.get(key) is None
|
||||
):
|
||||
default[key] = recursive_update(default.get(key, {}), update[key])
|
||||
for k, v in update.items():
|
||||
if isinstance(v, dict):
|
||||
v = cast(dict[Any, Any], v)
|
||||
default[k] = recursive_update(default.get(k) or {}, v)
|
||||
|
||||
elif isinstance(update[key], list) and (
|
||||
isinstance(default.get(key), list) or default.get(key) is None
|
||||
):
|
||||
# default_set = set(default.get(key, []))
|
||||
# custom_set = set(update[key])
|
||||
# default[key] = list(default_set.union(custom_set))
|
||||
default[key] = default.get(key, []).extend(update[key])
|
||||
elif isinstance(v, list):
|
||||
v = cast(list[Any], v)
|
||||
new = cast(list[Any], (default.get(k) or []))
|
||||
new.extend(v)
|
||||
default[k] = new
|
||||
|
||||
else:
|
||||
default[key] = update[key]
|
||||
default[k] = v
|
||||
|
||||
return default
|
||||
|
||||
|
@ -96,6 +93,49 @@ def update_project(state: State) -> State:
|
|||
return replace(state, after=project)
|
||||
|
||||
|
||||
def set_result(state: State) -> State: # noqa: C901
|
||||
def _changed(before: Any, after: Any) -> bool: # noqa: ANN401, C901, PLR0911
|
||||
if type(before) is not type(after):
|
||||
return True
|
||||
|
||||
if isinstance(before, dict):
|
||||
before = cast(dict[str, Any], before)
|
||||
|
||||
if len(before) != len(after):
|
||||
return True
|
||||
|
||||
for key in before:
|
||||
if key not in after:
|
||||
return True
|
||||
if _changed(before[key], after[key]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
if isinstance(before, list):
|
||||
before = sorted(cast(list[Any], before))
|
||||
after = sorted(after)
|
||||
|
||||
if len(before) != len(after):
|
||||
return True
|
||||
|
||||
for index in before.enumerate():
|
||||
if _changed(before[index], after[index]):
|
||||
return True
|
||||
|
||||
return before != after
|
||||
|
||||
result = Result(
|
||||
_changed(state.before, state.after),
|
||||
{
|
||||
"before": state.before,
|
||||
"after": state.after,
|
||||
},
|
||||
)
|
||||
|
||||
return replace(state, result=result)
|
||||
|
||||
|
||||
def write_compose(state: State) -> State:
|
||||
file = state.compose_filepath
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
__all__ = [
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.service import common
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
|
||||
BASE_ARGS: dict[str, Any] = {}
|
||||
|
@ -18,6 +18,7 @@ def run_helper(
|
|||
state: State,
|
||||
service_name: str,
|
||||
params: dict[str, Any],
|
||||
helper: Callable[[State, str, dict[str, Any]], State] = lambda a, _b, _c: a,
|
||||
helper: Callable[[State, str, dict[str, Any]], dict[str, Any]] = lambda _a, _b, _c: {},
|
||||
) -> State:
|
||||
return helper(state, service_name, params)
|
||||
update = helper(state, service_name, params)
|
||||
return common.apply_update(state, service_name, update)
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"stop": {"type": "bool", "default": True},
|
||||
}
|
||||
|
||||
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
||||
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"]
|
||||
|
||||
|
@ -28,4 +24,4 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
|||
"docker-volume-backup.stop-during-backup": project_name,
|
||||
}
|
||||
|
||||
return service.common.update(state, {"name": service_name}, update)
|
||||
return update
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"proxy_type": {"type": "str", "default": "http"},
|
||||
|
@ -20,22 +16,19 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
middleware: str = params["middleware"]
|
||||
settings: dict[str, str] = params["settings"]
|
||||
proxy_type: str = state.module.params["proxy_type"]
|
||||
name: str = (
|
||||
params.get("name")
|
||||
or f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}"
|
||||
params.get("name") or f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}"
|
||||
)
|
||||
|
||||
prefix = f"traefik.{proxy_type}.middlewares.{name}"
|
||||
|
||||
labels = {f"{prefix}.{middleware}.{key}": var for key, var in settings.items()}
|
||||
|
||||
update = {
|
||||
return {
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return service.common.update(state, {"name": service_name}, update)
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"name": {"type": "str"},
|
||||
|
@ -23,7 +19,7 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
rule: str = params["rule"]
|
||||
traefik_service: str | None = params.get("service")
|
||||
|
@ -52,8 +48,6 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
|||
if middlewares:
|
||||
labels[f"{prefix}.middlewares"] = ",".join(middlewares)
|
||||
|
||||
update = {
|
||||
return {
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return service.common.update(state, {"name": service_name}, update)
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"proxy_type": {"type": "str", "default": "http"},
|
||||
|
@ -19,7 +15,7 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, service_name: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
port: int | None = params.get("port")
|
||||
proxy_type: str = params["proxy_type"]
|
||||
|
@ -32,8 +28,6 @@ def helper(state: State, service_name: str, params: dict[str, Any]) -> State:
|
|||
if port:
|
||||
labels[f"{prefix}.loadbalancer.server.port"] = str(port)
|
||||
|
||||
update = {
|
||||
return {
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return service.common.update(state, {"name": service_name}, update)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|||
@dataclass(frozen=True)
|
||||
class Result:
|
||||
changed: bool = False
|
||||
diff: dict[str, Any] = field(default_factory=dict)
|
||||
diff: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
__all__ = [
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
# FIX: This entire library is broken and needs to be fixed
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import replace
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
|
||||
recursive_update,
|
||||
update_project,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import recursive_update
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
BASE_ARGS: dict[str, Any] = {
|
||||
"name": {"type": "str"},
|
||||
|
@ -25,12 +18,11 @@ BASE_ARGS: dict[str, Any] = {
|
|||
}
|
||||
|
||||
|
||||
def apply_base(state: State, params: dict[str, Any]) -> State:
|
||||
def get_base_definition(state: State, service_name: str) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
|
||||
new: dict[str, Any] = {
|
||||
"container_name": f"{project_name}_{params['name']}",
|
||||
"hostname": f"{project_name}_{params['name']}",
|
||||
return {
|
||||
"container_name": f"{project_name}_{service_name}",
|
||||
"hostname": f"{project_name}_{service_name}",
|
||||
"restart": "unless-stopped",
|
||||
"environment": {},
|
||||
"labels": {},
|
||||
|
@ -38,51 +30,26 @@ def apply_base(state: State, params: dict[str, Any]) -> State:
|
|||
"networks": {
|
||||
"internal": None,
|
||||
},
|
||||
} | (
|
||||
}
|
||||
|
||||
|
||||
def get_default_definition(state: State, service_name: str) -> dict[str, Any]:
|
||||
return (
|
||||
state.module.params.get("settings", {})
|
||||
.get("default_definition", {})
|
||||
.get(params["name"], {})
|
||||
.get(service_name, {})
|
||||
)
|
||||
|
||||
return update(state, params, new)
|
||||
|
||||
|
||||
def apply_definition(state: State, params: dict[str, Any], definition: dict[str, Any]) -> State:
|
||||
def apply_update(state: State, service_name: str, update: dict[str, Any]) -> State:
|
||||
project = copy.deepcopy(state.after)
|
||||
services: dict[str, Any] = project["services"]
|
||||
service: dict[str, Any] = services[params["name"]]
|
||||
|
||||
service = recursive_update(service, definition)
|
||||
|
||||
return update(state, params, service)
|
||||
|
||||
|
||||
def apply_settings(state: State, params: dict[str, Any]) -> State:
|
||||
settings = state.module.params.get("settings", {})
|
||||
params = settings.get("service_default_args", {}).get(params["name"], {}) | params
|
||||
|
||||
return update(
|
||||
state,
|
||||
params,
|
||||
settings.get("service_default_definitions", {}).get(params["name"], {}),
|
||||
)
|
||||
|
||||
|
||||
def update(state: State, params: dict[str, Any], update: dict[str, Any]) -> State:
|
||||
project = copy.deepcopy(state.after)
|
||||
|
||||
project["services"][params["name"]] = recursive_update(
|
||||
project["services"].get(params["name"], {}),
|
||||
update,
|
||||
)
|
||||
|
||||
new_definition = project["services"][params["name"]]
|
||||
new_volumes: list[dict[str, Any]] = new_definition.get("volumes") or []
|
||||
service = project["services"].get(service_name, {})
|
||||
project["services"][service_name] = recursive_update(service, update)
|
||||
|
||||
volumes: list[dict[str, Any]] = project["services"][service_name].get("volumes") or []
|
||||
# FIX: this silently throws out misconfigured volumes
|
||||
unique_volumes = list({vol["source"]: vol for vol in new_volumes if "target" in vol}.values())
|
||||
|
||||
project["services"][params["name"]]["volumes"] = unique_volumes
|
||||
unique_volumes = list({vol["source"]: vol for vol in volumes if "target" in vol}.values())
|
||||
project["services"][service_name]["volumes"] = unique_volumes
|
||||
|
||||
return replace(state, after=project)
|
||||
|
||||
|
@ -90,7 +57,7 @@ def update(state: State, params: dict[str, Any], update: dict[str, Any]) -> Stat
|
|||
def run_helper(
|
||||
state: State,
|
||||
params: dict[str, Any],
|
||||
helper: Callable[[State, dict[str, Any]], State] = lambda x, _: x,
|
||||
helper: Callable[[State, dict[str, Any]], dict[str, Any]] = lambda _a, _b: {},
|
||||
) -> State:
|
||||
if not params.get("name"):
|
||||
params["name"] = str.split(helper.__module__, ".")[-1]
|
||||
|
@ -98,8 +65,11 @@ def run_helper(
|
|||
if not (overwrite := params.get("overwrite")):
|
||||
overwrite = params.get("definition", {})
|
||||
|
||||
state = apply_base(state, params)
|
||||
state = apply_settings(state, params)
|
||||
state = helper(state, params)
|
||||
state = apply_definition(state, params, overwrite)
|
||||
return update_project(state)
|
||||
base_definition = get_base_definition(state, params["name"])
|
||||
default_definition = get_default_definition(state, params["name"])
|
||||
helper_update = helper(state, params)
|
||||
|
||||
state = apply_update(state, params["name"], base_definition)
|
||||
state = apply_update(state, params["name"], default_definition)
|
||||
state = apply_update(state, params["name"], helper_update)
|
||||
return apply_update(state, params["name"], overwrite)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils import label, service, spec
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils import label, spec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import (
|
||||
|
@ -20,7 +20,7 @@ FORCE_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
|
||||
internal_network: bool = params["internal_network"]
|
||||
|
||||
update: dict[str, Any] = {}
|
||||
|
@ -36,4 +36,4 @@ def helper(state: State, params: dict[str, Any]) -> State:
|
|||
helper = getattr(label, name).helper
|
||||
state = label.common.run_helper(state, params["name"], args, helper)
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
return update
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
update = {
|
||||
def helper(_state: State, _params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"privileged": True,
|
||||
}
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"read_only": {"type": "bool", "default": True},
|
||||
}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
def helper(_state: State, params: dict[str, Any]) -> dict[str, Any]:
|
||||
read_only = params["read_only"]
|
||||
|
||||
volumes = [
|
||||
|
@ -29,8 +25,6 @@ def helper(state: State, params: dict[str, Any]) -> State:
|
|||
},
|
||||
]
|
||||
|
||||
update = {
|
||||
return {
|
||||
"volumes": volumes,
|
||||
}
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"archive": {"type": "path"},
|
||||
|
@ -18,7 +14,7 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
archive: str | None = params.get("archive")
|
||||
backup_volumes: list[str] | None = params["backup_volumes"]
|
||||
|
@ -53,9 +49,7 @@ def helper(state: State, params: dict[str, Any]) -> State:
|
|||
},
|
||||
)
|
||||
|
||||
update = {
|
||||
return {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
}
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
@ -6,12 +6,8 @@ from __future__ import annotations
|
|||
import shlex
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"backup": {"type": "bool", "default": True},
|
||||
|
@ -22,7 +18,7 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
backup: bool = params["backup"]
|
||||
database: str = params["database"]
|
||||
|
@ -76,10 +72,8 @@ def helper(state: State, params: dict[str, Any]) -> State:
|
|||
},
|
||||
)
|
||||
|
||||
update = {
|
||||
return {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
@ -6,12 +6,8 @@ from __future__ import annotations
|
|||
import shlex
|
||||
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.models import (
|
||||
State,
|
||||
)
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {
|
||||
"backup": {"type": "bool", "default": True},
|
||||
|
@ -21,7 +17,7 @@ EXTRA_ARGS = {
|
|||
}
|
||||
|
||||
|
||||
def helper(state: State, params: dict[str, Any]) -> State:
|
||||
def helper(state: State, params: dict[str, Any]) -> dict[str, Any]:
|
||||
project_name: str = state.module.params["name"]
|
||||
backup: bool = params["backup"]
|
||||
database: str = params["database"]
|
||||
|
@ -67,10 +63,8 @@ def helper(state: State, params: dict[str, Any]) -> State:
|
|||
},
|
||||
)
|
||||
|
||||
update = {
|
||||
return {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return service.common.update(state, params, update)
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
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_compose.plugins.module_utils.models import State
|
||||
|
||||
EXTRA_ARGS = {}
|
||||
|
||||
|
||||
def helper(state: State) -> State:
|
||||
return state
|
||||
def helper(_state: State, _params: dict[str, Any]) -> dict[str, Any]:
|
||||
return {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
|
||||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
# ruff: noqa: E402
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
# TODO: break this down per module
|
||||
# TODO: generate this by reassembling
|
||||
DOCUMENTATION = """
|
||||
|
@ -476,12 +478,16 @@ def main() -> None:
|
|||
"settings": spec.settings_spec(),
|
||||
"services": spec.service_argument_spec(),
|
||||
},
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if not find_spec("yaml"):
|
||||
module.fail_json("PyYAML seems to be missing on host") # pyright: ignore[reportUnknownMemberType]
|
||||
module.fail_json("PyYAML needs to be installed on the host to use this plugin") # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
state = common.get_state(module)
|
||||
try:
|
||||
state = common.get_state(module)
|
||||
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 index, service_params in enumerate(services_params):
|
||||
|
@ -489,7 +495,20 @@ def main() -> None:
|
|||
helper = getattr(service, name).helper
|
||||
state = service.common.run_helper(state, service_params, helper)
|
||||
|
||||
print("fuck")
|
||||
state = common.update_project(state)
|
||||
state = common.set_result(state)
|
||||
|
||||
if state.result.changed and not module.check_mode:
|
||||
try:
|
||||
state = common.write_compose(state)
|
||||
except Exception as e: # noqa: BLE001
|
||||
module.fail_json(f"Error while writing new compose file: {e}") # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
ret = asdict(state.result)
|
||||
if not module._diff: # noqa: SLF001
|
||||
del ret["diff"]
|
||||
|
||||
module.exit_json(**ret) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"ANSIBLE_MODULE_ARGS": {
|
||||
"name": "taskwarrior",
|
||||
"project_dir": "/var/lib/ez_compose",
|
||||
"services": {
|
||||
"custom": [
|
||||
{
|
||||
"definition": {
|
||||
"image": "ghcr.io/gothenburgbitfactory/taskchampion-sync-server:main@sha256:4798edada4b264cdcc82f1c8ea2389cdd5cde02926f74b2361005438056f5729",
|
||||
"volumes": [
|
||||
{
|
||||
"source": "sync_data",
|
||||
"target": "/var/lib/taskchampion-sync-server",
|
||||
"type": "volume"
|
||||
}
|
||||
]
|
||||
},
|
||||
"label_helpers": {
|
||||
"docker_volume_backupper": {},
|
||||
"traefik_router": {
|
||||
"entrypoints": ["web-secure"],
|
||||
"rule": "Host(`taskwarrior-sync.snailed.de`)"
|
||||
}
|
||||
},
|
||||
"name": "sync"
|
||||
}
|
||||
],
|
||||
"docker_volume_backupper": [
|
||||
{
|
||||
"backup_volumes": ["sync_data"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"default_definition": {
|
||||
"environment": {
|
||||
"TZ": "Europe/Berlin"
|
||||
}
|
||||
},
|
||||
"label_default_args": {
|
||||
"traefik_router": {
|
||||
"certresolver": "letsencrypt"
|
||||
}
|
||||
},
|
||||
"service_default_args": {
|
||||
"docker_volume_backupper": {
|
||||
"archive": "/tank/docker-backups"
|
||||
}
|
||||
},
|
||||
"service_default_definitions": {
|
||||
"docker_volume_backupper": {
|
||||
"environment": {
|
||||
"BACKUP_CRON_EXPRESSION": "0 6 * * *",
|
||||
"BACKUP_RETENTION_DAYS": "7",
|
||||
"EXEC_FORWARD_OUTPUT": true,
|
||||
"GPG_PASSPHRASE": "UYi9wgpWKgZBep",
|
||||
"GZIP_PARALLELISM": "2",
|
||||
"NOTIFICATION_URLS": "smtp://root%40snaile.de:RzBNcz@5iq5gGw@tripoli.snaile.de:465/?FromAddress=root@snaile.de&ToAddresses=luca@bil.ke&Encryption=ImplicitTLS",
|
||||
"SSH_HOST_NAME": "u374707.your-storagebox.de",
|
||||
"SSH_PASSWORD": "8WHePPymUsPUaXeS",
|
||||
"SSH_PORT": "23",
|
||||
"SSH_REMOTE_PATH": "/home/docker-backups",
|
||||
"SSH_USER": "u374707-sub1"
|
||||
},
|
||||
"image": "offen/docker-volume-backup:v2.43.0"
|
||||
},
|
||||
"docker_in_docker": {
|
||||
"image": "docker:27.4.0-dind"
|
||||
},
|
||||
"mariadb": {
|
||||
"image": "mariadb:11.6.2"
|
||||
},
|
||||
"postgres": {
|
||||
"image": "postgres:16.6-alpine"
|
||||
},
|
||||
"redis": {
|
||||
"image": "redis:7.4.1-alpine"
|
||||
},
|
||||
"docker_socket_proxy": {
|
||||
"image": "tecnativa/docker-socket-proxy:0.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,19 +17,20 @@ ignore = [
|
|||
"D104",
|
||||
]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"tests/units/**" = [
|
||||
"S101",
|
||||
"PT009",
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
line-ending = "lf"
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "numpy"
|
||||
|
||||
[tool.basedpyright]
|
||||
typeCheckingMode = "strict"
|
||||
|
||||
# Handled by ruff
|
||||
reportPrivateUsage = false
|
||||
reportIgnoreCommentWithoutRule = false
|
||||
reportUnusedImport = false
|
||||
reportUnusedClass = false
|
4
requirements-dev.txt
Normal file
4
requirements-dev.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
# vim: ft=requirements
|
||||
ansible-core==2.17.*
|
||||
pytest
|
||||
PyYAML
|
123
tests/units/plugins/module_utils/test_common.py
Normal file
123
tests/units/plugins/module_utils/test_common.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Copyright: (c) 2025, Luca Bilke <luca@bil.ke>
|
||||
# MIT License (see LICENSE)
|
||||
|
||||
from typing import Any
|
||||
from unittest import TestCase
|
||||
|
||||
import pytest
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils import common
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("test_input", "expected"),
|
||||
[ # pyright: ignore[reportUnknownArgumentType]
|
||||
# Basic nested update (using existing test variables)
|
||||
(
|
||||
(
|
||||
{"one": {"one": "keep", "two": "rewrite"}},
|
||||
{"one": {"two": "new"}},
|
||||
),
|
||||
{"one": {"one": "keep", "two": "new"}},
|
||||
),
|
||||
# Deep nested update
|
||||
(
|
||||
(
|
||||
{"a": {"b": {"c": "old", "d": "keep"}}},
|
||||
{"a": {"b": {"c": "new"}}},
|
||||
),
|
||||
{"a": {"b": {"c": "new", "d": "keep"}}},
|
||||
),
|
||||
# Adding new keys at different levels
|
||||
(
|
||||
(
|
||||
{"x": {"y": "original"}},
|
||||
{"x": {"z": "new", "y": "updated"}, "new_key": "value"},
|
||||
),
|
||||
{"x": {"y": "updated", "z": "new"}, "new_key": "value"},
|
||||
),
|
||||
# Empty dict cases
|
||||
(
|
||||
(
|
||||
{},
|
||||
{"new": "data"},
|
||||
),
|
||||
{"new": "data"},
|
||||
),
|
||||
(
|
||||
(
|
||||
{"existing": "data"},
|
||||
{},
|
||||
),
|
||||
{"existing": "data"},
|
||||
),
|
||||
# Lists within dictionaries
|
||||
(
|
||||
(
|
||||
{"items": ["a", "b"], "nested": {"list": ["1", "2"]}},
|
||||
{"items": ["c"], "nested": {"list": ["3"]}},
|
||||
),
|
||||
{"items": ["a", "b", "c"], "nested": {"list": ["1", "2", "3"]}},
|
||||
),
|
||||
# Lists of dictionaries
|
||||
(
|
||||
(
|
||||
{
|
||||
"configs": [
|
||||
{"name": "config1", "value": "old"},
|
||||
{"name": "config2", "enabled": True},
|
||||
],
|
||||
},
|
||||
{
|
||||
"configs": [
|
||||
{"name": "config3", "value": "new"},
|
||||
{"name": "config4", "enabled": False},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
"configs": [
|
||||
{"name": "config1", "value": "old"},
|
||||
{"name": "config2", "enabled": True},
|
||||
{"name": "config3", "value": "new"},
|
||||
{"name": "config4", "enabled": False},
|
||||
],
|
||||
},
|
||||
),
|
||||
# Nested lists of dictionaries
|
||||
(
|
||||
(
|
||||
{
|
||||
"services": {
|
||||
"web": [
|
||||
{"port": 80, "protocol": "http"},
|
||||
{"port": 443, "protocol": "https"},
|
||||
],
|
||||
},
|
||||
},
|
||||
{"services": {"web": [{"port": 8080, "protocol": "http"}]}},
|
||||
),
|
||||
{
|
||||
"services": {
|
||||
"web": [
|
||||
{"port": 80, "protocol": "http"},
|
||||
{"port": 443, "protocol": "https"},
|
||||
{"port": 8080, "protocol": "http"},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
# Mixed types update
|
||||
(
|
||||
(
|
||||
{"mixed": {"num": 42, "list": [1, 2], "str": "old"}},
|
||||
{"mixed": {"num": 43, "list": [3], "str": "new"}},
|
||||
),
|
||||
{"mixed": {"num": 43, "list": [1, 2, 3], "str": "new"}},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_recursive_update(
|
||||
test_input: tuple[dict[str, Any], dict[str, Any]],
|
||||
expected: dict[str, Any],
|
||||
) -> None:
|
||||
TestCase().assertDictEqual(common.recursive_update(*test_input), expected)
|
Loading…
Add table
Reference in a new issue