Compare commits
4 Commits
f4adf7023c
...
0cb921ab26
Author | SHA1 | Date |
---|---|---|
Luca Bilke | 0cb921ab26 | |
Luca Bilke | 9b8d6771bd | |
Luca Bilke | 074ffbd734 | |
Luca Bilke | ad618a0442 |
|
@ -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
|
||||
compose_filepath: 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] = {}
|
||||
after: dict[str, Any] = {}
|
||||
|
||||
|
||||
def new_state(module: AnsibleModule) -> State:
|
||||
settings = Settings(**module.params["settings"])
|
||||
|
||||
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]
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
#!/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:
|
||||
archive = state.module.params.get("archive")
|
||||
backup_volumes = state.module.params.get("backup_volumes", [])
|
||||
name = state.module.params["name"]
|
||||
project_name = state.module.params["project_name"]
|
||||
|
||||
volumes = [
|
||||
{
|
||||
"type": "volume",
|
||||
"source": volume,
|
||||
"target": f"/backup/{volume}",
|
||||
"read_only": True,
|
||||
}
|
||||
for volume in backup_volumes
|
||||
]
|
||||
|
||||
environment = {
|
||||
"BACKUP_FILENAME": f"{project_name}-%Y-%m-%dT%H-%M-%S.{'{{ .Extension }}'}",
|
||||
"BACKUP_LATEST_SYMLINK": f"{project_name}-latest",
|
||||
"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",
|
||||
}
|
||||
|
||||
if archive:
|
||||
volumes.append(
|
||||
{
|
||||
"type": "bind",
|
||||
"source": f"{archive}/{project_name}",
|
||||
"target": "/archive",
|
||||
"bind": {"create_host_path": True},
|
||||
}
|
||||
)
|
||||
|
||||
update = {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
}
|
||||
|
||||
return update_service(state, update)
|
||||
|
||||
|
||||
def main():
|
||||
extra_args = {
|
||||
"archive": {"type": "str"},
|
||||
"backup_volumes": {"type": "list"},
|
||||
}
|
||||
run_service(extra_args, helper)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,104 @@
|
|||
#!/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 shlex
|
||||
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
|
||||
State,
|
||||
run_service,
|
||||
update_service,
|
||||
)
|
||||
|
||||
|
||||
def helper(state: State) -> State:
|
||||
backup = state.module.params.get("backup", True)
|
||||
database = state.module.params["database"]
|
||||
username = state.module.params["username"]
|
||||
password = state.module.params["password"]
|
||||
root_password = state.module.params["root_password"]
|
||||
name = state.module.params["name"]
|
||||
project_name = state.module.params["project_name"]
|
||||
|
||||
volumes = [
|
||||
{
|
||||
"source": name,
|
||||
"target": "/var/lib/mysql",
|
||||
"type": "volume",
|
||||
}
|
||||
]
|
||||
|
||||
environment = {
|
||||
"MARIADB_DATABASE": database,
|
||||
"MARIADB_USER": username,
|
||||
"MARIADB_PASSWORD": password,
|
||||
}
|
||||
|
||||
labels: dict[str, str] = {}
|
||||
|
||||
if root_password:
|
||||
environment.update(
|
||||
{
|
||||
"MARIADB_ROOT_PASSWORD": root_password,
|
||||
}
|
||||
)
|
||||
|
||||
if backup:
|
||||
labels.update(
|
||||
{
|
||||
"docker-volume-backup.archive-pre": (
|
||||
"/bin/sh -c '"
|
||||
"mysqldump --all-databases "
|
||||
f"-u {shlex.quote(username)} "
|
||||
f"-p {shlex.quote(password)} "
|
||||
f">/backup/{shlex.quote(project_name)}.sql"
|
||||
"'"
|
||||
)
|
||||
}
|
||||
)
|
||||
# mysqldump -psecret --all-databases > /tmp/dumps/dump.sql
|
||||
volumes.append(
|
||||
{
|
||||
"type": "volume",
|
||||
"source": f"{name}_backup",
|
||||
"target": "/backup",
|
||||
}
|
||||
)
|
||||
|
||||
update = {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return update_service(state, update)
|
||||
|
||||
|
||||
def main():
|
||||
extra_args = {
|
||||
"archive": {"type": "bool"},
|
||||
"database": {"type": "str", "required": True},
|
||||
"username": {"type": "str", "required": True},
|
||||
"password": {"type": "str", "required": True},
|
||||
"root_password": {"type": "str"},
|
||||
}
|
||||
run_service(extra_args, helper)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,95 @@
|
|||
#!/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 shlex
|
||||
|
||||
from ansible_collections.snailed.ez_compose.plugins.module_utils.common import (
|
||||
State,
|
||||
run_service,
|
||||
update_service,
|
||||
)
|
||||
|
||||
|
||||
def helper(state: State) -> State:
|
||||
backup = state.module.params.get("backup", True)
|
||||
database = state.module.params["database"]
|
||||
username = state.module.params["username"]
|
||||
password = state.module.params["password"]
|
||||
name = state.module.params["name"]
|
||||
project_name = state.module.params["project_name"]
|
||||
|
||||
volumes = [
|
||||
{
|
||||
"source": name,
|
||||
"target": "/var/lib/postgresql/data",
|
||||
"type": "volume",
|
||||
}
|
||||
]
|
||||
|
||||
environment = {
|
||||
"POSTGRES_DB": database,
|
||||
"POSTGRES_USER": username,
|
||||
"POSTGRES_PASSWORD": password,
|
||||
}
|
||||
|
||||
labels: dict[str, str] = {}
|
||||
|
||||
if backup:
|
||||
labels.update(
|
||||
{
|
||||
"docker-volume-backup.archive-pre": (
|
||||
"/bin/sh -c '"
|
||||
f"PGPASSWORD={shlex.quote(password)} "
|
||||
"pg_dumpall "
|
||||
f"-U {shlex.quote(username)} "
|
||||
f"-f /backup/{shlex.quote(project_name)}.sql"
|
||||
"'"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
volumes.append(
|
||||
{
|
||||
"type": "volume",
|
||||
"source": f"{name}_backup",
|
||||
"target": "/backup",
|
||||
}
|
||||
)
|
||||
|
||||
update = {
|
||||
"environment": environment,
|
||||
"volumes": volumes,
|
||||
"labels": labels,
|
||||
}
|
||||
|
||||
return update_service(state, update)
|
||||
|
||||
|
||||
def main():
|
||||
extra_args = {
|
||||
"archive": {"type": "bool"},
|
||||
"database": {"type": "str", "required": True},
|
||||
"username": {"type": "str", "required": True},
|
||||
"password": {"type": "str", "required": True},
|
||||
}
|
||||
run_service(extra_args, helper)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -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__":
|
||||
|
|
|
@ -9,3 +9,8 @@ reportMissingTypeStubs = false
|
|||
# handled by ruff
|
||||
reportUnusedVariable = false
|
||||
reportUnusedImport = false
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Irrelevant for ansible
|
||||
ignore = ["E402", "F404"]
|
||||
|
|
Loading…
Reference in New Issue