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 typing import Any
import yaml
from pydantic import BaseModel, ConfigDict
import copy
from dataclasses import asdict, dataclass, field, replace
from typing import Any, Callable
import yaml
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
diff: dict[str, Any] = {}
diff: dict[str, Any] = field(default_factory=dict)
class Settings(BaseModel):
model_config = ConfigDict(frozen=True)
@dataclass(frozen=True)
class Settings:
projects_dir: str = "/usr/local/share/ez_compose/"
class State(BaseModel):
model_config = ConfigDict(frozen=True)
module: AnsibleModule
@dataclass(frozen=True)
class State:
module: Any # Replace Any with the actual type of AnsibleModule if available
result: Result
settings: Settings
compose_filename: str
before: dict[str, Any] = {}
after: dict[str, Any] = {}
compose_filepath: str
before: dict[str, Any]
after: dict[str, Any] = field(default_factory=dict)
def new_state(module: AnsibleModule) -> State:
settings = Settings(**module.params["settings"])
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] = {}
return State(
module=module,
result=Result(),
settings=settings,
compose_filename=(
f"{settings.projects_dir}/"
+ f"{module.params['project_name']}/"
+ f"{module.params['name']}.yml"
),
compose_filepath=compose_filepath,
before=before,
)
def get_compose(state: State) -> State:
file = state.compose_filename
def apply_service_base(state: State) -> State:
params = state.module.params
with open(file, "r") as stream:
compose = yaml.safe_load(stream)
compose = copy.deepcopy(state.before)
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:
file = state.compose_filename
file = state.compose_filepath
with open(file, mode="w") as stream:
yaml.dump(state.after, stream)
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>
# MIT License (see LICENSE)
from __future__ import annotations
# TODO: write ansible sections
import copy
DOCUMENTATION = r"""
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
State,
get_compose,
new_state,
write_compose,
)
EXAMPLES = r"""
"""
RETURN = r"""
"""
def generate(state: State) -> State:
params = state.module.params
compose = copy.deepcopy(state.before)
from __future__ import annotations # pyright: ignore[reportGeneralTypeIssues]
services = compose.get("services", {})
container = services.get(params["name"], {})
container.update(
{
"image": params["image"],
}
)
services.update({params["name"]: container})
return state.model_copy(update={"after": compose})
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import run_service
def main():
module = AnsibleModule(
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]
run_service()
if __name__ == "__main__":

View File

@ -1,5 +1,5 @@
[tool.black]
line-length=100
line-length = 100
[tool.basedpyright]
typeCheckingMode = "strict"
@ -9,3 +9,8 @@ reportMissingTypeStubs = false
# handled by ruff
reportUnusedVariable = false
reportUnusedImport = false
[tool.ruff.lint]
# Irrelevant for ansible
ignore = ["E402", "F404"]