diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ca8fb1..2ffaafd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,8 +61,8 @@ repos: hooks: - id: generate-compose-config name: regenerate docker compose configuration - files: ^(compose\.yml(?:\.tpl)?|images/[^/]+/(?:docker-)?compose\.ya?ml|tools/generate_compose_config\.py)$ + files: ^(compose\.yml(?:\.tpl)?|images/[^/]+/((?:docker-)?compose\.ya?ml|specs\.json)|tools/generate_compose_config\.py)$ pass_filenames: false language: python - entry: tools/generate_compose_config.py --output-file compose.yml + entry: tools/generate_compose_config.py additional_dependencies: ['PyYAML==6.0', 'jinja2==3.1.2'] diff --git a/compose.yml b/compose.yml index f0cc087..ea21233 100644 --- a/compose.yml +++ b/compose.yml @@ -138,6 +138,98 @@ services: service: asyncssh-server-2.13.1 ports: - '22022:22' + asyncssh-client-2.0.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.0.0 + asyncssh-client-2.0.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.0.1 + asyncssh-client-2.1.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.1.0 + asyncssh-client-2.2.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.2.0 + asyncssh-client-2.2.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.2.1 + asyncssh-client-2.3.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.3.0 + asyncssh-client-2.4.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.4.0 + asyncssh-client-2.4.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.4.1 + asyncssh-client-2.4.2: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.4.2 + asyncssh-client-2.5.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.5.0 + asyncssh-client-2.6.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.6.0 + asyncssh-client-2.7.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.7.0 + asyncssh-client-2.7.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.7.1 + asyncssh-client-2.7.2: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.7.2 + asyncssh-client-2.8.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.8.0 + asyncssh-client-2.8.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.8.1 + asyncssh-client-2.9.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.9.0 + asyncssh-client-2.10.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.10.0 + asyncssh-client-2.10.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.10.1 + asyncssh-client-2.11.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.11.0 + asyncssh-client-2.12.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.12.0 + asyncssh-client-2.13.0: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.13.0 + asyncssh-client-2.13.1: + extends: + file: images/asyncssh/compose.yml + service: asyncssh-client-2.13.1 bitvise-server-8.15: extends: diff --git a/images/asyncssh/Dockerfile b/images/asyncssh/Dockerfile index f96b1e2..c51852d 100644 --- a/images/asyncssh/Dockerfile +++ b/images/asyncssh/Dockerfile @@ -1,26 +1,42 @@ -FROM python:3 +# hadolint ignore=DL3049 +FROM python:3 AS asyncssh-base -WORKDIR /app -COPY requirements.txt simple_server.py ./ ARG VERSION + +WORKDIR /app +COPY files/requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt asyncssh==$VERSION +# Server image +FROM asyncssh-base AS asyncssh-server + ARG USERNAME=sshattacker ARG PASSWORD=secret ARG AUTHORIZED_KEYS_FILE=authorized_keys -# Copy RSA server key -COPY ssh_host_key* ./ +# Copy server script and RSA server key +COPY files/simple_server.py files/ssh_host_key* ./ # Copy authorized pubkeys -COPY "${AUTHORIZED_KEYS_FILE}" "authorized_keys" +COPY "files/${AUTHORIZED_KEYS_FILE}" "authorized_keys" LABEL ssh.implementation.name="asyncssh" \ ssh.implementation.version="${VERSION}" \ ssh.implementation.type="server" -ENV USERNAME="${USERNAME}" PASSWORD="${PASSWORD}" -# Although the JSON notation for CMD is preferable, there is no straightforward -# way to pass env vars, so we just disable the warning here. -# hadolint ignore=DL3025 -CMD python simple_server.py "${USERNAME}" --password "${PASSWORD}" --authorized-keys "authorized_keys" +# Environment variables are picked up by the simple_server.py script +ENV SSH_USERNAME="${USERNAME}" SSH_PASSWORD="${PASSWORD}" + +ENTRYPOINT [ "python", "/app/simple_server.py", "-f", "authorized_keys" ] EXPOSE 22 + +# Client image +FROM asyncssh-base AS asyncssh-client + +# Copy client script +COPY files/simple_client.py ./ + +LABEL ssh.implementation.name="asyncssh" \ + ssh.implementation.version="${VERSION}" \ + ssh.implementation.type="client" + +ENTRYPOINT [ "python", "/app/simple_client.py" ] diff --git a/images/asyncssh/compose.yml b/images/asyncssh/compose.yml index 71d42fb..c549ce2 100644 --- a/images/asyncssh/compose.yml +++ b/images/asyncssh/compose.yml @@ -4,137 +4,344 @@ services: image: rub-nds/asyncssh-server:2.0.0 build: context: . + target: asyncssh-server args: VERSION: 2.0.0 asyncssh-server-2.0.1: image: rub-nds/asyncssh-server:2.0.1 build: context: . + target: asyncssh-server args: VERSION: 2.0.1 asyncssh-server-2.1.0: image: rub-nds/asyncssh-server:2.1.0 build: context: . + target: asyncssh-server args: VERSION: 2.1.0 asyncssh-server-2.2.0: image: rub-nds/asyncssh-server:2.2.0 build: context: . + target: asyncssh-server args: VERSION: 2.2.0 asyncssh-server-2.2.1: image: rub-nds/asyncssh-server:2.2.1 build: context: . + target: asyncssh-server args: VERSION: 2.2.1 asyncssh-server-2.3.0: image: rub-nds/asyncssh-server:2.3.0 build: context: . + target: asyncssh-server args: VERSION: 2.3.0 asyncssh-server-2.4.0: image: rub-nds/asyncssh-server:2.4.0 build: context: . + target: asyncssh-server args: VERSION: 2.4.0 asyncssh-server-2.4.1: image: rub-nds/asyncssh-server:2.4.1 build: context: . + target: asyncssh-server args: VERSION: 2.4.1 asyncssh-server-2.4.2: image: rub-nds/asyncssh-server:2.4.2 build: context: . + target: asyncssh-server args: VERSION: 2.4.2 asyncssh-server-2.5.0: image: rub-nds/asyncssh-server:2.5.0 build: context: . + target: asyncssh-server args: VERSION: 2.5.0 asyncssh-server-2.6.0: image: rub-nds/asyncssh-server:2.6.0 build: context: . + target: asyncssh-server args: VERSION: 2.6.0 asyncssh-server-2.7.0: image: rub-nds/asyncssh-server:2.7.0 build: context: . + target: asyncssh-server args: VERSION: 2.7.0 asyncssh-server-2.7.1: image: rub-nds/asyncssh-server:2.7.1 build: context: . + target: asyncssh-server args: VERSION: 2.7.1 asyncssh-server-2.7.2: image: rub-nds/asyncssh-server:2.7.2 build: context: . + target: asyncssh-server args: VERSION: 2.7.2 asyncssh-server-2.8.0: image: rub-nds/asyncssh-server:2.8.0 build: context: . + target: asyncssh-server args: VERSION: 2.8.0 asyncssh-server-2.8.1: image: rub-nds/asyncssh-server:2.8.1 build: context: . + target: asyncssh-server args: VERSION: 2.8.1 asyncssh-server-2.9.0: image: rub-nds/asyncssh-server:2.9.0 build: context: . + target: asyncssh-server args: VERSION: 2.9.0 asyncssh-server-2.10.0: image: rub-nds/asyncssh-server:2.10.0 build: context: . + target: asyncssh-server args: VERSION: 2.10.0 asyncssh-server-2.10.1: image: rub-nds/asyncssh-server:2.10.1 build: context: . + target: asyncssh-server args: VERSION: 2.10.1 asyncssh-server-2.11.0: image: rub-nds/asyncssh-server:2.11.0 build: context: . + target: asyncssh-server args: VERSION: 2.11.0 asyncssh-server-2.12.0: image: rub-nds/asyncssh-server:2.12.0 build: context: . + target: asyncssh-server args: VERSION: 2.12.0 asyncssh-server-2.13.0: image: rub-nds/asyncssh-server:2.13.0 build: context: . + target: asyncssh-server args: VERSION: 2.13.0 asyncssh-server-2.13.1: image: rub-nds/asyncssh-server:2.13.1 build: context: . + target: asyncssh-server args: VERSION: 2.13.1 + asyncssh-client-2.0.0: + image: rub-nds/asyncssh-client:2.0.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.0.0 + profiles: [client] + asyncssh-client-2.0.1: + image: rub-nds/asyncssh-client:2.0.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.0.1 + profiles: [client] + asyncssh-client-2.1.0: + image: rub-nds/asyncssh-client:2.1.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.1.0 + profiles: [client] + asyncssh-client-2.2.0: + image: rub-nds/asyncssh-client:2.2.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.2.0 + profiles: [client] + asyncssh-client-2.2.1: + image: rub-nds/asyncssh-client:2.2.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.2.1 + profiles: [client] + asyncssh-client-2.3.0: + image: rub-nds/asyncssh-client:2.3.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.3.0 + profiles: [client] + asyncssh-client-2.4.0: + image: rub-nds/asyncssh-client:2.4.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.4.0 + profiles: [client] + asyncssh-client-2.4.1: + image: rub-nds/asyncssh-client:2.4.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.4.1 + profiles: [client] + asyncssh-client-2.4.2: + image: rub-nds/asyncssh-client:2.4.2 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.4.2 + profiles: [client] + asyncssh-client-2.5.0: + image: rub-nds/asyncssh-client:2.5.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.5.0 + profiles: [client] + asyncssh-client-2.6.0: + image: rub-nds/asyncssh-client:2.6.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.6.0 + profiles: [client] + asyncssh-client-2.7.0: + image: rub-nds/asyncssh-client:2.7.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.7.0 + profiles: [client] + asyncssh-client-2.7.1: + image: rub-nds/asyncssh-client:2.7.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.7.1 + profiles: [client] + asyncssh-client-2.7.2: + image: rub-nds/asyncssh-client:2.7.2 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.7.2 + profiles: [client] + asyncssh-client-2.8.0: + image: rub-nds/asyncssh-client:2.8.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.8.0 + profiles: [client] + asyncssh-client-2.8.1: + image: rub-nds/asyncssh-client:2.8.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.8.1 + profiles: [client] + asyncssh-client-2.9.0: + image: rub-nds/asyncssh-client:2.9.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.9.0 + profiles: [client] + asyncssh-client-2.10.0: + image: rub-nds/asyncssh-client:2.10.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.10.0 + profiles: [client] + asyncssh-client-2.10.1: + image: rub-nds/asyncssh-client:2.10.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.10.1 + profiles: [client] + asyncssh-client-2.11.0: + image: rub-nds/asyncssh-client:2.11.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.11.0 + profiles: [client] + asyncssh-client-2.12.0: + image: rub-nds/asyncssh-client:2.12.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.12.0 + profiles: [client] + asyncssh-client-2.13.0: + image: rub-nds/asyncssh-client:2.13.0 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.13.0 + profiles: [client] + asyncssh-client-2.13.1: + image: rub-nds/asyncssh-client:2.13.1 + build: + context: . + target: asyncssh-client + args: + VERSION: 2.13.1 + profiles: [client] diff --git a/images/asyncssh/authorized_keys b/images/asyncssh/files/authorized_keys similarity index 100% rename from images/asyncssh/authorized_keys rename to images/asyncssh/files/authorized_keys diff --git a/images/asyncssh/requirements.txt b/images/asyncssh/files/requirements.txt similarity index 70% rename from images/asyncssh/requirements.txt rename to images/asyncssh/files/requirements.txt index 419af10..724d1ff 100644 --- a/images/asyncssh/requirements.txt +++ b/images/asyncssh/files/requirements.txt @@ -1,2 +1,3 @@ asyncio~=3.4.3 asyncssh>=2.0.0 +click~=8.1.3 diff --git a/images/asyncssh/files/simple_client.py b/images/asyncssh/files/simple_client.py new file mode 100644 index 0000000..8858af7 --- /dev/null +++ b/images/asyncssh/files/simple_client.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +import click +import asyncio +import asyncssh +import sys + + +@click.command() +@click.option("-H", "--host", help="hostname or ip", default='172.17.0.1"') +@click.option("-p", "--port", help="prot", default=22, type=int) +@click.option("-u", "--username", help="username", default="sshattacker") +@click.option("-P", "--password", help="password", default="secret") +@click.option("-c", "--command", help="command", default="pwd") +@click.option( + "-o", + "--output", + is_flag=True, + show_default=True, + default=False, + help="print output", +) +@click.option( + "-e", "--error", is_flag=True, show_default=True, default=False, help="print error" +) +def client_start(host, port, username, password, command, output, error): + print(f"Connecting to {host}:{port} as {username}") + + try: + asyncio.get_event_loop().run_until_complete( + run_client(host, port, username, password, command, output, error) + ) + except (OSError, asyncssh.Error) as exc: + sys.exit("SSH connection failed: " + str(exc)) + + +async def run_client(host, port, username, password, command, output, error): + async with asyncssh.connect( + host=host, port=port, username=username, password=password, known_hosts=None + ) as conn: + result = await conn.run(command, check=True) + print(result.stdout, end="") + + +if __name__ == "__main__": + client_start() # pyright: reportGeneralTypeIssues=false diff --git a/images/asyncssh/simple_server.py b/images/asyncssh/files/simple_server.py old mode 100755 new mode 100644 similarity index 68% rename from images/asyncssh/simple_server.py rename to images/asyncssh/files/simple_server.py index d20c009..723c6d7 --- a/images/asyncssh/simple_server.py +++ b/images/asyncssh/files/simple_server.py @@ -2,8 +2,10 @@ """ Server code from example of AsyncSSH, see: https://asyncssh.readthedocs.io/en/stable/#server-examples + +Modified to accept parameters from environment variables for easier integration with docker """ -import argparse +import click import pathlib import asyncio import asyncssh @@ -103,71 +105,62 @@ async def start_server(port, host_keys): ) -def validate_username(text): - stripped = text.strip() +def validate_username(ctx, param, value): + stripped = value.strip() if not stripped: raise ValueError("Username must not be empty!") return stripped -def main(argv=None): - parser = argparse.ArgumentParser() - parser.add_argument("username", help="SSH username", type=validate_username) - parser.add_argument( - "-P", - "--password", - action="store", - help="SSH password (password auth will be disabled if not specified)", - ) - parser.add_argument( - "-f", - "--authorized-keys-file", - type=pathlib.Path, - action="store", - help="SSH authorized_keys file (pubkey auth won't not work if not specified)", - ) - parser.add_argument( - "-p", - "--port", - action="store", - type=int, - default=22, - help="SSH port to listen on", - ) - parser.add_argument( - "--host-key", - action="store", - type=pathlib.Path, - default=pathlib.Path(__file__).parent.joinpath("ssh_host_key"), - help="SSH host key", - ) - args = parser.parse_args(argv) - +@click.command() +@click.option( + "-u", "--username", help="SSH username", type=str, callback=validate_username +) +@click.option( + "-P", + "--password", + help="SSH password (password authentication will be disabled if not specified)", + type=str, +) +@click.option( + "-f", + "--authorized-keys-file", + help="SSH authorized_keys file (publickey authentication won't work if not specified)", + type=pathlib.Path, +) +@click.option("-p", "--port", help="SSH port to listen on", default=22, type=int) +@click.option( + "--host-key", + help="SSH host key", + default=pathlib.Path(__file__).parent.joinpath("ssh_host_key"), + type=pathlib.Path, +) +def main(username, password, authorized_keys_file, port, host_key): logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) - logger.info("Username: %r", args.username) - if args.password is not None: - logger.info("Password: %r", args.password) - PASSWORDS[args.username] = args.password + logger.info("Username: %r", username) + if password is not None: + logger.info("Password: %r", password) + PASSWORDS[username] = password else: logger.warning("No password given, password auth will be disabled.") - if args.authorized_keys_file is not None: - logger.info("authorized_keys file: %s", args.authorized_keys_file) - AUTHORIZED_KEYS_FILES[args.username] = args.authorized_keys_file + if authorized_keys_file is not None: + logger.info("authorized_keys file: %s", authorized_keys_file) + AUTHORIZED_KEYS_FILES[username] = authorized_keys_file else: - logger.warning("No authorized_keys file given, pubkey auth will not work.") + logger.warning("No authorized_keys file given, publickey auth will not work.") - if args.password is None and args.authorized_keys_file is None: + if password is None and authorized_keys_file is None: logger.warning( "Neither password nor authorized_keys file specified, you won't be able to log in!" ) - logger.info("Server starting up on %d...", args.port) + logger.info("Server starting up on %d...", port) loop = asyncio.get_event_loop() try: - loop.run_until_complete(start_server(port=args.port, host_keys=[args.host_key])) + loop.run_until_complete(start_server(port=port, host_keys=[host_key])) except (OSError, asyncssh.Error) as exc: sys.exit("Error starting server: " + str(exc)) loop.run_forever() @@ -175,4 +168,4 @@ def main(argv=None): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main(auto_envvar_prefix="SSH")) # pyright: reportGeneralTypeIssues=false diff --git a/images/asyncssh/ssh_host_key b/images/asyncssh/files/ssh_host_key similarity index 100% rename from images/asyncssh/ssh_host_key rename to images/asyncssh/files/ssh_host_key diff --git a/images/asyncssh/ssh_host_key.pub b/images/asyncssh/files/ssh_host_key.pub similarity index 100% rename from images/asyncssh/ssh_host_key.pub rename to images/asyncssh/files/ssh_host_key.pub diff --git a/images/asyncssh/specs.json b/images/asyncssh/specs.json new file mode 100644 index 0000000..e59fae0 --- /dev/null +++ b/images/asyncssh/specs.json @@ -0,0 +1,59 @@ +{ + "name": "asyncssh", + "url": "https://asyncssh.readthedocs.io/en/latest/", + "capabilities": { + "server": true, + "client": true + }, + "multistage": true, + "serverVersions": [ + "2.0.0", + "2.0.1", + "2.1.0", + "2.2.0", + "2.2.1", + "2.3.0", + "2.4.0", + "2.4.1", + "2.4.2", + "2.5.0", + "2.6.0", + "2.7.0", + "2.7.1", + "2.7.2", + "2.8.0", + "2.8.1", + "2.9.0", + "2.10.0", + "2.10.1", + "2.11.0", + "2.12.0", + "2.13.0", + "2.13.1" + ], + "clientVersions": [ + "2.0.0", + "2.0.1", + "2.1.0", + "2.2.0", + "2.2.1", + "2.3.0", + "2.4.0", + "2.4.1", + "2.4.2", + "2.5.0", + "2.6.0", + "2.7.0", + "2.7.1", + "2.7.2", + "2.8.0", + "2.8.1", + "2.9.0", + "2.10.0", + "2.10.1", + "2.11.0", + "2.12.0", + "2.13.0", + "2.13.1" + ] +} diff --git a/compose.yml.tpl b/templates/combined.yml.tpl similarity index 100% rename from compose.yml.tpl rename to templates/combined.yml.tpl diff --git a/templates/implementation.yml.tpl b/templates/implementation.yml.tpl new file mode 100644 index 0000000..3429b63 --- /dev/null +++ b/templates/implementation.yml.tpl @@ -0,0 +1,29 @@ +version: '3.2' +services: +{%- if specs.capabilities.server -%} + {%- for version in specs.serverVersions %} + {{ specs.name }}-server-{{ version }}: + image: rub-nds/{{ specs.name }}-server:{{ version }} + build: + context: . + {%- if specs.multistage %} + target: {{ specs.name }}-server + {%- endif %} + args: + VERSION: {{ version }} + {%- endfor -%} +{%- endif -%} +{%- if specs.capabilities.client -%} + {%- for version in specs.clientVersions %} + {{ specs.name }}-client-{{ version }}: + image: rub-nds/{{ specs.name }}-client:{{ version }} + build: + context: . + {%- if specs.multistage %} + target: {{ specs.name }}-client + {%- endif %} + args: + VERSION: {{ version }} + profiles: [client] + {%- endfor %} +{% endif %} diff --git a/tools/generate_compose_config.py b/tools/generate_compose_config.py index 809207b..eb8c6b9 100755 --- a/tools/generate_compose_config.py +++ b/tools/generate_compose_config.py @@ -1,68 +1,61 @@ #!/usr/bin/env python3 -import argparse import logging import sys import pathlib import yaml +import json import jinja2 +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +repository_root_path = pathlib.Path(__file__).parent.parent +jinja_env = jinja2.Environment( + loader=jinja2.FileSystemLoader( + searchpath=[repository_root_path.joinpath("templates")], + followlinks=True, + ), + autoescape=False, +) -def main(argv=None): - repository_root_path = pathlib.Path(__file__).parent.parent - logging.basicConfig(level=logging.DEBUG) - logger = logging.getLogger(__name__) +def generate_impl_compose(specs_file): + with specs_file.open("r") as fp: + specs = json.load(fp) + implementation_template = jinja_env.get_template("implementation.yml.tpl") + with specs_file.parent.joinpath("compose.yml").open("w") as output_file: + output_file.write(implementation_template.render(specs=specs)) - parser = argparse.ArgumentParser() - parser.add_argument( - "file", - type=pathlib.Path, - nargs="*", - default=sorted( - repository_root_path.glob("images/*/compose.yml"), - # Ensure that image names without a `-` are always sorted *after* - # those that include a `-` (e.g. `openssh-7.x` comes before - # `openssh`). - key=lambda p: tuple(x or "~" for x in p.parent.stem.partition("-")), - ), - ) - parser.add_argument( - "-t", - "--template-file", - type=str, - default="compose.yml.tpl", - ) - parser.add_argument( - "-o", "--output-file", type=argparse.FileType("w"), default=sys.stdout - ) - args = parser.parse_args(argv) +def generate_combined_compose(): files = {} - for path in args.file: - logger.debug("Parsed file: %s", path) - with path.open("r") as fp: + for compose_file in sorted( + repository_root_path.glob("images/*/compose.yml"), + # Ensure that image names without a `-` are always sorted *after* + # those that include a `-` (e.g. `openssh-7.x` comes before + # `openssh`). + key=lambda p: tuple(x or "~" for x in p.parent.stem.partition("-")), + ): + logger.debug("Parsed file: %s", compose_file) + with compose_file.open("r") as fp: files[ - str(path.relative_to(repository_root_path)).replace("\\", "/") + str(compose_file.relative_to(repository_root_path)).replace("\\", "/") ] = yaml.safe_load(fp) - - env = jinja2.Environment( - loader=jinja2.FileSystemLoader( - searchpath=[repository_root_path], - followlinks=True, - ), - autoescape=False, - ) - logger.debug("Template file: %s", args.template_file) - template = env.get_template(str(args.template_file)) - args.output_file.write( - template.render( - files=files, - # Pass some builtin functions to make template development easier. - enumerate=enumerate, - sorted=sorted, - ), - ) - + combined_template = jinja_env.get_template("combined.yml.tpl") + with repository_root_path.joinpath("compose.yml").open("w") as output_file: + output_file.write( + combined_template.render( + files=files, + # Pass some builtin functions to make template development easier. + enumerate=enumerate, + sorted=sorted, + ), + ) + + +def main(): + for specs_file in repository_root_path.glob("images/*/specs.json"): + generate_impl_compose(specs_file) + generate_combined_compose() return 0