refactor common.py

This commit is contained in:
Luca Bilke 2024-10-22 22:23:44 +02:00
parent f4adf7023c
commit ad618a0442
No known key found for this signature in database
GPG Key ID: C9E851809C1A5BDE
3 changed files with 159 additions and 94 deletions

View File

@ -3,65 +3,174 @@
from __future__ import annotations from __future__ import annotations
from typing import Any import copy
import yaml from dataclasses import asdict, dataclass, field, replace
from pydantic import BaseModel, ConfigDict from typing import Any, Callable
import yaml
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
PROJECTS_DIR = "/var/lib/ez_compose"
BASE_SERVICE_ARGS = {
"project_name": {
"type": "str",
"required": True,
},
"name": {
"type": "str",
"required": True,
},
"image": {
"type": "str",
},
"state": {
"type": "str",
"default": "present",
"choices": ["present", "absent"],
},
"defaults": {
"type": "dict",
},
}
class Result(BaseModel):
model_config = ConfigDict(frozen=True)
@dataclass(frozen=True)
class Result:
changed: bool = False changed: bool = False
diff: dict[str, Any] = {} diff: dict[str, Any] = field(default_factory=dict)
class Settings(BaseModel): @dataclass(frozen=True)
model_config = ConfigDict(frozen=True) class Settings:
projects_dir: str = "/usr/local/share/ez_compose/" projects_dir: str = "/usr/local/share/ez_compose/"
class State(BaseModel): @dataclass(frozen=True)
model_config = ConfigDict(frozen=True) class State:
module: Any # Replace Any with the actual type of AnsibleModule if available
module: AnsibleModule
result: Result result: Result
settings: Settings compose_filepath: str
compose_filename: str before: dict[str, Any]
after: dict[str, Any] = field(default_factory=dict)
def _recursive_update(default: dict[Any, Any], update: dict[Any, Any]) -> dict[Any, Any]:
for key in update:
if isinstance(update[key], dict) and isinstance(default.get(key), dict):
default[key] = _recursive_update(default[key], update[key])
elif isinstance(update[key], list) and isinstance(default.get(key), list):
default_set = set(default[key])
custom_set = set(update[key])
default[key] = list(default_set.union(custom_set))
else:
default[key] = update[key]
return default
def get_state(module: AnsibleModule) -> State:
compose_filepath = f"{PROJECTS_DIR}/{module.params['project_name']}/docker-compose.yml"
try:
with open(compose_filepath, "r") as fp:
before = yaml.safe_load(fp)
except FileNotFoundError:
before: dict[str, Any] = {} before: dict[str, Any] = {}
after: dict[str, Any] = {}
def new_state(module: AnsibleModule) -> State:
settings = Settings(**module.params["settings"])
return State( return State(
module=module, module=module,
result=Result(), result=Result(),
settings=settings, compose_filepath=compose_filepath,
compose_filename=( before=before,
f"{settings.projects_dir}/"
+ f"{module.params['project_name']}/"
+ f"{module.params['name']}.yml"
),
) )
def get_compose(state: State) -> State: def apply_service_base(state: State) -> State:
file = state.compose_filename params = state.module.params
with open(file, "r") as stream: compose = copy.deepcopy(state.before)
compose = yaml.safe_load(stream) services = compose.get("services", {})
return state.model_copy(update={"before": compose}) 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)
def set_defaults(state: State) -> State:
params = state.module.params
compose = copy.deepcopy(state.before)
services = compose["services"]
service = services[params["name"]]
_recursive_update(service, params["defaults"])
services.update({params["name"]: service})
return replace(state, after=compose)
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)
def write_compose(state: State) -> State: def write_compose(state: State) -> State:
file = state.compose_filename file = state.compose_filepath
with open(file, mode="w") as stream: with open(file, mode="w") as stream:
yaml.dump(state.after, stream) yaml.dump(state.after, stream)
return state return state
def run_service(
extra_args: dict[str, Any] = {},
helper: Callable[[State], State] = lambda x: x,
) -> None:
module = AnsibleModule(
argument_spec=BASE_SERVICE_ARGS.update(extra_args),
supports_check_mode=True,
)
state = get_state(module)
for f in [apply_service_base, set_defaults, helper]:
state = f(state)
if module.check_mode:
module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]
write_compose(state)
module.exit_json(**asdict(state.result)) # type: ignore[reportUnkownMemberType]

View File

@ -1,76 +1,27 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2024, Luca Bilke <luca@bil.ke> # Copyright: (c) 2024, Luca Bilke <luca@bil.ke>
# MIT License (see LICENSE) # MIT License (see LICENSE)
from __future__ import annotations # TODO: write ansible sections
import copy DOCUMENTATION = r"""
"""
from ansible.module_utils.basic import AnsibleModule EXAMPLES = r"""
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import ( """
State,
get_compose,
new_state,
write_compose,
)
RETURN = r"""
"""
def generate(state: State) -> State: from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
params = state.module.params
compose = copy.deepcopy(state.before)
services = compose.get("services", {}) from ansible_collections.snailed.ez_compose.plugins.module_utils.common import run_service
container = services.get(params["name"], {})
container.update(
{
"image": params["image"],
}
)
services.update({params["name"]: container})
return state.model_copy(update={"after": compose})
def main(): def main():
module = AnsibleModule( run_service()
argument_spec={
"state": {
"type": "str",
"default": "present",
"choices": ["present", "absent"],
},
"name": {
"type": "str",
"required": True,
},
"project_name": {
"type": "str",
"required": True,
},
"image": {
"type": "str",
},
"settings": {
"type": "dict",
"required": True,
},
},
supports_check_mode=True,
)
state = new_state(module)
get_compose(state)
generate(state)
if module.check_mode:
module.exit_json(**state.result) # type: ignore[reportUnkownMemberType]
write_compose(state)
module.exit_json(**state.result) # type: ignore[reportUnkownMemberType]
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -9,3 +9,8 @@ reportMissingTypeStubs = false
# handled by ruff # handled by ruff
reportUnusedVariable = false reportUnusedVariable = false
reportUnusedImport = false reportUnusedImport = false
[tool.ruff.lint]
# Irrelevant for ansible
ignore = ["E402", "F404"]