This commit is contained in:
Luca Bilke 2024-10-23 17:15:37 +02:00
parent d1dca887ad
commit 2006d16b30
Signed by: luca
GPG Key ID: F6E11C9BAA7C82F5
13 changed files with 612 additions and 61 deletions

View File

@ -32,6 +32,21 @@ BASE_SERVICE_ARGS = {
"type": "dict",
},
}
BASE_LABEL_ARGS = {
"project_name": {
"type": "str",
"required": True,
},
"name": {
"type": "str",
"required": True,
},
"state": {
"type": "str",
"default": "present",
"choices": ["present", "absent"],
},
}
@dataclass(frozen=True)
@ -51,7 +66,7 @@ class State:
result: Result
compose_filepath: str
before: dict[str, Any]
after: dict[str, Any] = field(default_factory=dict)
after: dict[str, Any]
def _recursive_update(default: dict[Any, Any], update: dict[Any, Any]) -> dict[Any, Any]:
@ -85,64 +100,119 @@ def get_state(module: AnsibleModule) -> State:
result=Result(),
compose_filepath=compose_filepath,
before=before,
after=before,
)
def apply_service_base(state: State) -> State:
params = state.module.params
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
image = state.module.params["image"]
compose = copy.deepcopy(state.before)
services = compose.get("services", {})
update: dict[str, Any] = {
"service_name": f"{project_name}_{service_name}",
"hostname": f"{project_name}_{service_name}",
"image": image,
"restart": "unless-stopped",
"environment": {},
"labels": {},
"volumes": [],
"networks": {
f"{project_name}_internal": None,
},
}
networks = compose.get("networks", {}).update(
{
f"{params['project_name']}_internal": None,
}
)
service = services.get(params["name"], {}).update(
{
"container_name": f"{params['project_name']}_{params['name']}",
"hostname": f"{params['project_name']}_{params['name']}",
"image": params["image"],
"restart": "unless-stopped",
"environment": {},
"labels": {},
"volumes": [],
"networks": {
f"{params['project_name']}_internal": None,
},
}
)
services.update(
{
"networks": networks,
params["name"]: service,
}
)
return replace(state, after=compose)
return update_service(state, update)
def set_defaults(state: State) -> State:
params = state.module.params
compose = copy.deepcopy(state.before)
services = compose["services"]
service = services[params["name"]]
def set_service_defaults(state: State) -> State:
container_name = state.module.params["name"]
defaults = state.module.params["defaults"]
project = copy.deepcopy(state.after)
services = project["services"]
service = services[container_name]
_recursive_update(service, params["defaults"])
_ = _recursive_update(service, defaults)
services.update({params["name"]: service})
services.update({container_name: service})
return replace(state, after=compose)
return replace(state, after=project)
def update_service(state: State, update: dict[str, Any]) -> State:
compose = copy.deepcopy(state.before)
name = state.module.params["name"]
_recursive_update(compose["services"][name], update)
return replace(state, after=compose)
project = copy.deepcopy(state.after)
service_name = state.module.params["name"]
_ = _recursive_update(project["services"][service_name], update)
return replace(state, after=project)
def remove_service(state: State) -> State:
project = copy.deepcopy(state.after)
service_name = state.module.params["name"]
del project["services"][service_name]
return replace(state, after=project)
def remove_labels(state: State, label_names: list[str]) -> State:
project = copy.deepcopy(state.after)
service_name = state.module.params["name"]
service = project["services"].get(service_name, {})
labels = service.get("labels", {})
if labels:
for label in labels:
if label in label_names:
try:
del service["labels"][label]
except KeyError:
pass
service["labels"] = labels
else:
try:
del service["labels"]
except KeyError:
pass
project["services"][service_name] = service
return replace(state, after=project)
def update_project(state: State) -> State:
project = copy.deepcopy(state.after)
project_services = project.get("services", {})
project_networks = project.get("networks", {})
project_volumes = project.get("volumes", {})
for service in project_services:
if service_volumes := service.get("volumes"):
service_volume_names = [x["source"] for x in service_volumes]
project_volumes.update(
{
service_volume_name: None
for service_volume_name in service_volume_names
if service_volume_name not in project_volumes
}
)
if service_network_names := service.get("networks").keys():
project_networks.update(
{
service_network_name: None
for service_network_name in service_network_names
if service_network_name not in project_networks
}
)
return replace(state, after=project)
def write_compose(state: State) -> State:
@ -156,21 +226,50 @@ def write_compose(state: State) -> State:
def run_service(
extra_args: dict[str, Any] = {},
helper: Callable[[State], State] = lambda x: x,
helper: Callable[[State], State] = lambda _: _,
) -> None:
module = AnsibleModule(
argument_spec=BASE_SERVICE_ARGS.update(extra_args),
argument_spec={**BASE_SERVICE_ARGS, **extra_args},
supports_check_mode=True,
)
state = get_state(module)
for f in [apply_service_base, set_defaults, helper]:
state = f(state)
if module.params["state"] == "absent":
state = remove_service(state)
if module.check_mode:
module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]
else:
for f in [apply_service_base, set_service_defaults, helper, update_project]:
state = f(state)
write_compose(state)
exit(state)
module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]
def run_label(
extra_args: dict[str, Any], helper: Callable[[State], State], label_names: list[str]
) -> None:
module = AnsibleModule(
argument_spec={**BASE_LABEL_ARGS, **extra_args},
supports_check_mode=True,
)
state = get_state(module)
if module.params["state"] == "absent":
state = remove_labels(state, label_names)
else:
state = helper(state)
exit(state)
def exit(state: State) -> None:
# TODO: Check diff and set changed variable
if state.module.check_mode:
state.module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]
_ = write_compose(state)
state.module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]

View File

@ -0,0 +1,51 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from typing import Any
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_label,
update_service,
)
def helper(state: State) -> State:
stop = state.module.params.get("stop", True)
project_name = state.module.params["project_name"]
update: dict[str, Any] = {}
if stop:
update["labels"] = {
"docker-volume-backup.stop-during-backup": project_name,
}
return update_service(state, update)
def main():
extra_args = {
"stop": {"type": "bool"},
}
run_label(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,60 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_label,
update_service,
)
def helper(state: State) -> State:
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
middleware = state.module.params["middleware"]
settings = state.module.params["settings"]
proxy_type = state.module.params.get("proxy_type", "http")
middleware_name = state.module.params.get(
"middleware_name",
f"{project_name}_{service_name}_{proxy_type}_{middleware.lower()}",
)
prefix = f"traefik.{proxy_type}.middlewares.{middleware_name}"
labels = {f"{prefix}.{middleware}.{key}": var for key, var in settings.items()}
update = {
"labels": labels,
}
return update_service(state, update)
def main():
extra_args = {
"proxy_type": {"type": "str"},
"middleware_name": {"type": "string"},
"middleware": {"type": "str", "required": True},
"settings": {"type": "list", "required": True},
}
run_label(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,83 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_label,
update_service,
)
def helper(state: State) -> State:
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
rule = state.module.params["rule"]
service = state.module.params.get("service")
entrypoints = state.module.params.get("entrypoints")
middlewares = state.module.params.get("middlewares")
certresolver = state.module.params.get("certresolver")
proxy_type = state.module.params.get("proxy_type", "http")
router_name = state.module.params.get(
"router_name",
f"{project_name}_{service_name}_{proxy_type}",
)
prefix = f"traefik.{proxy_type}.routers.{router_name}"
labels = {
"traefik.enable": True,
f"traefik.{prefix}.rule": rule,
}
if certresolver:
labels[f"traefik.{prefix}.tls.certresolver"] = certresolver
if entrypoints:
labels[f"{prefix}.entrypoints"] = ",".join(entrypoints)
if service:
labels[f"{prefix}.service"] = service
if middlewares:
labels[f"{prefix}.middlewares"] = ",".join(middlewares)
update = {
"labels": labels,
}
return update_service(state, update)
def main():
extra_args = {
"proxy_type": {"type": "str"},
"router_name": {
"type": "str",
},
"rule": {"type": "str", "required": True},
"service": {"type": "str"},
"certresolver": {"type": "str"},
"entrypoints": {"type": "list"},
"middlewares": {"type": "list"},
}
run_label(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,64 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_label,
update_service,
)
from typing import Any
def helper(state: State) -> State:
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
port = state.module.params.get("port")
proxy_type = state.module.params.get("proxy_type", "http")
service_name = state.module.params.get(
"service_name",
f"{project_name}_{service_name}_{proxy_type}",
)
prefix = f"traefik.{proxy_type}.services.{service_name}"
labels: dict[str, Any] = {}
if port:
labels[f"{prefix}.loadbalancer.server.port"] = str(port)
update = {
"labels": labels,
}
return update_service(state, update)
def main():
extra_args = {
"proxy_type": {"type": "str"},
"service_name": {"type": "string"},
"port": {"type": "int"},
}
run_label(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,90 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
import copy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
exit,
get_state,
remove_service,
update_project,
update_service,
)
def helper(state: State) -> State:
project_name = state.module.params["project_name"]
definition = state.module.params["definition"]
internal_network = state.module.params.get("internal_network", False)
update = copy.deepcopy(definition)
networks = update.get("networks", {})
if internal_network:
networks[f"{project_name}_internal"] = None
update["networks"] = networks
return update_service(state, update)
def main():
module = AnsibleModule(
argument_spec={
"project_name": {
"type": "str",
"required": True,
},
"name": {
"type": "str",
"required": True,
},
"internal_network": {
"type": "bool",
},
"state": {
"type": "str",
"default": "present",
"choices": ["present", "absent"],
},
"definition": {
"type": "dict",
"required": True,
},
},
supports_check_mode=True,
)
state = get_state(module)
if module.params["state"] == "absent":
state = remove_service(state)
else:
for f in [helper, update_project]:
state = f(state)
exit(state)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,48 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_service,
update_service,
)
def helper(state: State) -> State:
command = state.module.params.get("command")
update = {
"privileged": True,
}
if command:
update["command"] = command
return update_service(state, update)
def main():
extra_args = {
"command": {"type": "list"},
}
run_service(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,54 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE)
# TODO: write ansible sections
DOCUMENTATION = r"""
"""
EXAMPLES = r"""
"""
RETURN = r"""
"""
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
run_service,
update_service,
)
def helper(state: State) -> State:
read_only = state.module.params.get("read_only", True)
volumes = [
{
"type": "bind",
"source": "/var/run/docker.sock",
"target": "/var/run/docker.sock",
"read_only": read_only,
}
]
update = {
"volumes": volumes,
}
return update_service(state, update)
def main():
extra_args = {
"read_only": {"type": "bool"},
}
run_service(extra_args, helper)
if __name__ == "__main__":
main()

View File

@ -27,7 +27,7 @@ from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
def helper(state: State) -> State:
archive = state.module.params.get("archive")
backup_volumes = state.module.params.get("backup_volumes", [])
name = state.module.params["name"]
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
volumes = [
@ -46,7 +46,7 @@ def helper(state: State) -> State:
"BACKUP_PRUNING_PREFIX": f"{project_name}-",
"BACKUP_STOP_DURING_BACKUP_LABEL": project_name,
"BACKUP_ARCHIVE": "/archive",
"DOCKER_HOST": f"tcp://{project_name}_{name}_socket_proxy:2375",
"DOCKER_HOST": f"tcp://{project_name}_{service_name}_socket_proxy:2375",
}
if archive:

View File

@ -32,12 +32,12 @@ def helper(state: State) -> State:
username = state.module.params["username"]
password = state.module.params["password"]
root_password = state.module.params["root_password"]
name = state.module.params["name"]
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
volumes = [
{
"source": name,
"source": service_name,
"target": "/var/lib/mysql",
"type": "volume",
}
@ -75,7 +75,7 @@ def helper(state: State) -> State:
volumes.append(
{
"type": "volume",
"source": f"{name}_backup",
"source": f"{service_name}_backup",
"target": "/backup",
}
)

View File

@ -31,12 +31,12 @@ def helper(state: State) -> State:
database = state.module.params["database"]
username = state.module.params["username"]
password = state.module.params["password"]
name = state.module.params["name"]
service_name = state.module.params["name"]
project_name = state.module.params["project_name"]
volumes = [
{
"source": name,
"source": service_name,
"target": "/var/lib/postgresql/data",
"type": "volume",
}
@ -67,7 +67,7 @@ def helper(state: State) -> State:
volumes.append(
{
"type": "volume",
"source": f"{name}_backup",
"source": f"{service_name}_backup",
"target": "/backup",
}
)

View File

@ -4,11 +4,13 @@ line-length = 100
[tool.basedpyright]
typeCheckingMode = "strict"
reportIgnoreCommentWithoutRule = true
reportUnusedCallResult = true
reportMissingTypeStubs = false
# handled by ruff
reportUnusedVariable = false
reportUnusedImport = false
reportUndefinedVariable = false
[tool.ruff.lint]