131 lines
3.9 KiB
Python
Executable file
131 lines
3.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from typing import Self
|
|
|
|
from python_on_whales import docker
|
|
from python_on_whales import DockerClient
|
|
|
|
PROJECT_ROOT_DIRECTORY = Path(__file__).parent.parent.resolve()
|
|
SERVICES_DIRECTORY = PROJECT_ROOT_DIRECTORY / "services"
|
|
AVAILABLE_SERVICES = list(
|
|
service.name for service in SERVICES_DIRECTORY.iterdir()
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Server:
|
|
server_name: str
|
|
services: list[str]
|
|
context_name: str | None = None
|
|
|
|
@property
|
|
def context(self) -> str:
|
|
return self.context_name if self.context_name else self.server_name
|
|
|
|
@classmethod
|
|
def parse_dict(cls, data: dict[str, Any]) -> Self:
|
|
if not (server_name := data.get("server_name")):
|
|
raise ValueError("The service is missing server_name property.")
|
|
|
|
if not (services := data.get("services")):
|
|
raise ValueError("The config is missing services property.")
|
|
|
|
if not isinstance(services, list):
|
|
raise ValueError("The services property should be an array.")
|
|
|
|
return cls(server_name=server_name, services=services)
|
|
|
|
|
|
@dataclass
|
|
class ServicesConfig:
|
|
servers: list[Server]
|
|
|
|
def search_service(self, service_name: str) -> Server:
|
|
servers_matched = []
|
|
for server in self.servers:
|
|
if service_name in server.services:
|
|
servers_matched.append(server)
|
|
|
|
if not servers_matched:
|
|
raise ValueError(f"Service `{service_name}` not found.")
|
|
|
|
if len(servers_matched) > 1:
|
|
raise ValueError(
|
|
f"Two services with the same name (`{service_name}`) found.",
|
|
)
|
|
|
|
return servers_matched.pop()
|
|
|
|
@classmethod
|
|
def parse_dict(cls, data: dict[str, Any]) -> Self:
|
|
if not (all_servers := data.get("servers")):
|
|
raise ValueError("The config is missing servers property.")
|
|
if not isinstance(all_servers, list):
|
|
raise ValueError("The servers property should be an array.")
|
|
servers = []
|
|
for server in all_servers:
|
|
if not isinstance(server, dict):
|
|
raise ValueError("The server should be an object.")
|
|
servers.append(Server.parse_dict(server))
|
|
|
|
return cls(servers=servers)
|
|
|
|
|
|
def _get_service_context(service_name: str, config: ServicesConfig) -> str:
|
|
context_names = [ctx.name for ctx in docker.context.list()]
|
|
server_config = config.search_service(service_name)
|
|
|
|
if server_config.context not in context_names:
|
|
raise ValueError("Context not found on docker host.")
|
|
|
|
return server_config.context
|
|
|
|
|
|
def deploy_service(service_name: str, config: ServicesConfig):
|
|
context = _get_service_context(service_name, config)
|
|
service_dir = SERVICES_DIRECTORY / service_name
|
|
|
|
docker = DockerClient(context=context)
|
|
|
|
compose_file = service_dir / "docker-compose.yml"
|
|
env_file = service_dir / ".env"
|
|
|
|
if not compose_file.exists():
|
|
raise ValueError(
|
|
f"Service {service_name} is missing docker-compose.yml",
|
|
)
|
|
|
|
print(f"INFO: deploying service {service_name} with context {context}")
|
|
docker.stack.deploy(
|
|
name=service_name,
|
|
compose_files=[compose_file],
|
|
env_files=[env_file] if env_file else [],
|
|
)
|
|
|
|
|
|
def load_config(config_file: Path | str) -> ServicesConfig:
|
|
config_file = Path(config_file)
|
|
unknown_data = json.loads(config_file.read_text())
|
|
return ServicesConfig.parse_dict(unknown_data)
|
|
|
|
|
|
def _add_args(parser: argparse.ArgumentParser):
|
|
parser.add_argument("services_file")
|
|
parser.add_argument("service_name", choices=AVAILABLE_SERVICES)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser("deployservice")
|
|
_add_args(parser)
|
|
parsed_args = parser.parse_args()
|
|
config = load_config(parsed_args.services_file)
|
|
deploy_service(parsed_args.service_name, config)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|