251 lines
9.3 KiB
Python
Executable File
251 lines
9.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
|
|
TIME_REGEX = re.compile(r'((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\d+?)s)?')
|
|
|
|
def parse_time(time_str):
|
|
parts = TIME_REGEX.match(time_str)
|
|
if not parts or parts.span() == (0, 0):
|
|
return
|
|
parts = parts.groupdict()
|
|
time_params = {}
|
|
for name, param in parts.items():
|
|
if param:
|
|
time_params[name] = int(param)
|
|
return timedelta(**time_params)
|
|
|
|
class IniFile:
|
|
PREAMBLE = "### Generated by dcqc - do not edit ###"
|
|
def __init__(self):
|
|
self._data = {}
|
|
|
|
def __getitem__(self, key: str):
|
|
return self._data[key]
|
|
|
|
def __setitem__(self, key: str, value):
|
|
self._data[key] = value
|
|
|
|
def write(self, f):
|
|
f.write(self.PREAMBLE)
|
|
f.write("\n\n")
|
|
for section, section_data in self._data.items():
|
|
f.write(f"[{section}]\n")
|
|
for key, value in section_data.items():
|
|
if isinstance(value, list):
|
|
for item in value:
|
|
f.write(f"{key}={item}\n")
|
|
else:
|
|
f.write(f"{key}={value}\n")
|
|
f.write("\n")
|
|
|
|
def abort(msg: str):
|
|
print(msg, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def enforce_list(key: str, dict_: dict):
|
|
if not isinstance(dict_[key], list):
|
|
abort(f"{key} is not a list. Offending dict: {dict_}")
|
|
|
|
|
|
def add_part_of(args, unit_file: IniFile):
|
|
unit_file["Unit"]["PartOf"] = args.target
|
|
|
|
def write_file(ini_file: IniFile, path: Path):
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
ini_file.write(f)
|
|
|
|
def write_build_unit(args, yaml_dict: dict, service_name: str):
|
|
out_file = args.output_dir / f"{service_name}.build"
|
|
print(f'Generating build "{service_name}" ({out_file})')
|
|
build = yaml_dict["services"][service_name]["build"]
|
|
|
|
unit_file = IniFile()
|
|
unit_file["Unit"] = {}
|
|
unit_file["Unit"]["Description"] = f"{service_name.capitalize()} build"
|
|
|
|
unit_file["Build"] = {}
|
|
unit_file["Build"]["ImageTag"] = f"localhost/{service_name}"
|
|
if isinstance(build, str):
|
|
unit_file["Build"]["SetWorkingDirectory"] = build
|
|
elif isinstance(build, dict):
|
|
if "context" in build:
|
|
unit_file["Build"]["SetWorkingDirectory"] = build["context"]
|
|
if "dockerfile" in build:
|
|
unit_file["Build"]["File"] = build["dockerfile"]
|
|
|
|
write_file(unit_file, out_file)
|
|
|
|
|
|
def write_network_units(args, yaml_dict):
|
|
networks = yaml_dict.get("networks", {})
|
|
if "default" not in networks:
|
|
networks["default"] = None
|
|
for network_name in networks:
|
|
out_file = args.output_dir / f"{network_name}.network"
|
|
print(f'Generating network "{network_name}" ({out_file})')
|
|
network = networks.get(network_name)
|
|
if network is None:
|
|
network = {}
|
|
|
|
unit_file = IniFile()
|
|
unit_file["Unit"] = {}
|
|
unit_file["Unit"]["Description"] = f"{network_name.capitalize()} network"
|
|
add_part_of(args, unit_file)
|
|
|
|
unit_file["Network"] = {}
|
|
if "name" in network:
|
|
unit_file["Network"]["NetworkName"] = network_name
|
|
unit_file["Network"]["Internal"] = str(network.get("internal", True)).lower()
|
|
if "enable_ipv6" in network:
|
|
unit_file["Network"]["IPv6"] = str(network["enable_ipv6"]).lower()
|
|
unit_file["Network"]["DisableDNS"] = "false"
|
|
unit_file["Network"]["Options"] = "isolate=true"
|
|
|
|
unit_file["Service"] = {}
|
|
unit_file["Service"]["Restart"] = "on-failure"
|
|
|
|
unit_file["Install"] = {}
|
|
unit_file["Install"]["WantedBy"] = "default.target"
|
|
|
|
write_file(unit_file, out_file)
|
|
|
|
|
|
def write_service_units(args, yaml_dict):
|
|
for service_name in yaml_dict["services"]:
|
|
out_file = args.output_dir / f"{service_name}.container"
|
|
print(f'Generating container "{service_name}" ({out_file})')
|
|
service = yaml_dict["services"][service_name]
|
|
|
|
unit_file = IniFile()
|
|
unit_file["Unit"] = {}
|
|
unit_file["Unit"]["Description"] = f"{service_name.capitalize()} container"
|
|
add_part_of(args, unit_file)
|
|
# currently decreases reliability instead of increasing it
|
|
# if "depends_on" in service:
|
|
# enforce_list("depends_on", service)
|
|
# for dependency in service["depends_on"]:
|
|
# unit_file["Unit"].setdefault("Requires", []).append(f"{dependency}.service")
|
|
# unit_file["Unit"].setdefault("After", []).append(f"{dependency}.service")
|
|
|
|
unit_file["Container"] = {}
|
|
if "container_name" in service:
|
|
unit_file["Container"]["ContainerName"] = service["container_name"]
|
|
if "image" in service:
|
|
unit_file["Container"]["Image"] = service["image"]
|
|
unit_file["Container"]["AutoUpdate"] = "registry"
|
|
elif "build" in service:
|
|
write_build_unit(args, yaml_dict, service_name)
|
|
unit_file["Container"]["Image"] = f"{service_name}.build"
|
|
unit_file["Container"]["AutoUpdate"] = "local"
|
|
if "ports" in service:
|
|
enforce_list("ports", service)
|
|
unit_file["Container"]["PublishPort"] = service["ports"]
|
|
if "command" in service:
|
|
enforce_list("command", service)
|
|
command = service["command"]
|
|
cmd = f'"{command[0]}"'
|
|
if len(command) > 1:
|
|
cmd += " \\\n"
|
|
for i, cmd_part in enumerate(command[1:]):
|
|
cmd += f' "{cmd_part}"'
|
|
if i < len(command) - 2:
|
|
cmd += " \\\n"
|
|
unit_file["Container"]["Exec"] = cmd
|
|
if "entrypoint" in service:
|
|
if not isinstance(service["entrypoint"], str):
|
|
abort(f"Only supporting str for entrypoint for now. Got {type(service['entrypoint'])}")
|
|
unit_file["Container"]["Entrypoint"] = service["entrypoint"]
|
|
if "labels" in service:
|
|
enforce_list("labels", service)
|
|
unit_file["Container"]["Label"] = service["labels"]
|
|
if "environment" in service:
|
|
if isinstance(service["environment"], dict):
|
|
env_vars = []
|
|
for key, value in service["environment"].items():
|
|
env_vars.append(f"{key}={value}")
|
|
service["environment"] = env_vars
|
|
enforce_list("environment", service)
|
|
unit_file["Container"]["Environment"] = service["environment"]
|
|
if "env_file" in service:
|
|
unit_file["Container"]["EnvironmentFile"] = service["env_file"]
|
|
if "volumes" in service:
|
|
enforce_list("volumes", service)
|
|
unit_file["Container"]["Volume"] = service["volumes"]
|
|
if "networks" in service:
|
|
enforce_list("networks", service)
|
|
unit_file["Container"]["Network"] = list(map(lambda x: f"{x}.network", service["networks"]))
|
|
else:
|
|
unit_file["Container"]["Network"] = ["default.network"]
|
|
if "devices" in service:
|
|
unit_file["Container"]["AddDevice"] = service["devices"]
|
|
if "userns_mode" in service:
|
|
unit_file["Container"]["UserNS"] = service["userns_mode"]
|
|
if "group_add" in service:
|
|
enforce_list("group_add", service)
|
|
unit_file["Container"]["GroupAdd"] = service["group_add"]
|
|
if "security_opt" in service:
|
|
enforce_list("security_opt", service)
|
|
unmask = []
|
|
mask = []
|
|
for line in service["security_opt"]:
|
|
parts = line.split(":")
|
|
if len(parts) != 2:
|
|
abort(f'Can only parts security_opts of form "type:path". Got: {line}')
|
|
type_ = parts[0]
|
|
match type_:
|
|
case "mask":
|
|
mask.append(parts[1])
|
|
case "unmask":
|
|
unmask.append(parts[1])
|
|
case _:
|
|
abort(f'security_opt type "{type_}" is not supported"')
|
|
if mask:
|
|
unit_file["Container"]["Mask"] = mask
|
|
if unmask:
|
|
unit_file["Container"]["Unmask"] = unmask
|
|
|
|
if "stop_grace_period" in service:
|
|
unit_file["Container"]["StopTimeout"] = int(parse_time(service["stop_grace_period"]).total_seconds())
|
|
|
|
|
|
unit_file["Service"] = {}
|
|
unit_file["Service"]["Restart"] = "on-failure"
|
|
|
|
unit_file["Install"] = {}
|
|
unit_file["Install"]["WantedBy"] = "default.target"
|
|
|
|
write_file(unit_file, out_file)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
parser.add_argument("compose_file")
|
|
parser.add_argument("-o", "--output-dir", help="output directory for unit files",
|
|
default="/etc/containers/systemd", type=Path)
|
|
parser.add_argument("-t", "--target",
|
|
help="Add PartOf dependency to all unit files", default="containers.target")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
with open(args.compose_file, encoding="utf-8") as f:
|
|
yaml_dict = yaml.safe_load(f)
|
|
|
|
write_network_units(args, yaml_dict)
|
|
write_service_units(args, yaml_dict)
|
|
|
|
print('\n==> Remember to run "systemctl daemon-reload"')
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|