commit dd302c83a3d0c4b0d318c3b3efaf93f3df89aa29 Author: Faerbit Date: Sun Nov 3 22:53:20 2024 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16b93ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,288 @@ +out +sample + +# Created by https://www.toptal.com/developers/gitignore/api/python,vim,pycharm+all +# Edit at https://www.toptal.com/developers/gitignore?templates=python,vim,pycharm+all + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/python,vim,pycharm+all diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ab425e5 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyyaml = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..88f7049 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,81 @@ +{ + "_meta": { + "hash": { + "sha256": "27a55b76dc774eff1af052a9de8b0c6b059a44578b80abeb18dbe190986011e0" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "pyyaml": { + "hashes": [ + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + } + }, + "develop": {} +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..61816f8 --- /dev/null +++ b/Readme.md @@ -0,0 +1 @@ +# dcqc - docker compose quadlet converter diff --git a/main.py b/main.py new file mode 100644 index 0000000..3b1b99b --- /dev/null +++ b/main.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +import argparse +import sys +from pathlib import Path + +import yaml + + +class IniFile: + PREAMBLE = "### Generated by dcqc - do not edit ###" + def __init__(self): + self._data = {} + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, 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): + print(msg, file=sys.stderr) + sys.exit(1) + +def enforce_list(key, dict_): + if not isinstance(dict_[key], list): + abort(f"{key} is not a list. Offending dict: {dict_}") + + +def write_build_unit(args, yaml_dict, service_name): + 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"] + + with open(out_file, "w", encoding="utf-8") as f: + unit_file.write(f) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("compose_file") + parser.add_argument("-o", "--output-dir", default="/etc/containers/systemd", type=Path) + parser.add_argument("-u", "--user") + 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) + + for network_name in yaml_dict.get("networks", []): + out_file = args.output_dir / f"{network_name}.network" + print(f'Generating network "{network_name}" ({out_file})') + network = yaml_dict["networks"].get(network_name) + if network is None: + network = {} + + unit_file = IniFile() + unit_file["Unit"] = {} + unit_file["Unit"]["Description"] = f"{network_name.capitalize()} network" + + unit_file["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["Service"] = {} + unit_file["Service"]["Restart"] = "on-failure" + + unit_file["Install"] = {} + unit_file["Install"]["WantedBy"] = "default.target" + + with open(out_file, "w", encoding="utf-8") as f: + unit_file.write(f) + + 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" + 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 = 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 "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 "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"] + + + unit_file["Service"] = {} + unit_file["Service"]["Restart"] = "on-failure" + + unit_file["Install"] = {} + unit_file["Install"]["WantedBy"] = "default.target" + + with open(out_file, "w", encoding="utf-8") as f: + unit_file.write(f) + + print('\n==> Remember to run "systemctl daemon-reload"') + + + +if __name__ == '__main__': + main()