From 5656149ff1b0026a01c077cf08150dd220b25e95 Mon Sep 17 00:00:00 2001 From: alsadi Date: Mon, 4 Mar 2019 11:30:14 +0200 Subject: [PATCH] initial work --- README.md | 20 +- examples/awx-working/docker-compose.yml | 67 +++++++ examples/awx/docker-compose.yml | 67 +++++++ examples/busybox/docker-compose.yml | 41 ++++ podman-compose.py | 242 ++++++++++++++++++++++++ 5 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 examples/awx-working/docker-compose.yml create mode 100644 examples/awx/docker-compose.yml create mode 100644 examples/busybox/docker-compose.yml create mode 100755 podman-compose.py diff --git a/README.md b/README.md index 5e14668..65f2364 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ -# podman-compose -a script to run docker-compose.yml using podman +# PodMan-Compose + +A script to run `docker-compose.yml` using [podman](https://podman.io/), +doing necessary mapping to make it work rootless. + +## NOTE + +it's still underdevelopment and does not work yet. + +## Mappings + +* `1podfw` - create all containers in one pod (inter-container communication is done via `localhost`), doing port mapping in that pod +* `1pod` - create all containers in one pod, doing port mapping in each container +* `identity` - no mapping +* `host` - use host network, and inter-container communication is done via host gateway and published ports + +## Examples + diff --git a/examples/awx-working/docker-compose.yml b/examples/awx-working/docker-compose.yml new file mode 100644 index 0000000..3c2297b --- /dev/null +++ b/examples/awx-working/docker-compose.yml @@ -0,0 +1,67 @@ +version: '3' +services: + postgres: + image: "postgres:9.6" + environment: + POSTGRES_USER: awx + POSTGRES_PASSWORD: awxpass + POSTGRES_DB: awx + + rabbitmq: + image: "rabbitmq:3" + environment: + RABBITMQ_DEFAULT_VHOST: awx + + memcached: + image: "memcached:alpine" + + awx_web: + # image: "geerlingguy/awx_web:latest" + image: "ansible/awx_web:latest" + links: + - rabbitmq + - memcached + - postgres + ports: + - "8080:8052" + hostname: awxweb + user: root + environment: + SECRET_KEY: aabbcc + DATABASE_USER: awx + DATABASE_PASSWORD: awxpass + DATABASE_NAME: awx + DATABASE_PORT: 5432 + DATABASE_HOST: localhost + RABBITMQ_USER: guest + RABBITMQ_PASSWORD: guest + RABBITMQ_HOST: localhost + RABBITMQ_PORT: 5672 + RABBITMQ_VHOST: awx + MEMCACHED_HOST: localhost + MEMCACHED_PORT: 11211 + + awx_task: + # image: "geerlingguy/awx_task:latest" + image: "ansible/awx_task:latest" + links: + - rabbitmq + - memcached + - awx_web:awxweb + - postgres + hostname: awx + user: root + environment: + SECRET_KEY: aabbcc + DATABASE_USER: awx + DATABASE_PASSWORD: awxpass + DATABASE_NAME: awx + DATABASE_PORT: 5432 + DATABASE_HOST: localhost + RABBITMQ_USER: guest + RABBITMQ_PASSWORD: guest + RABBITMQ_HOST: localhost + RABBITMQ_PORT: 5672 + RABBITMQ_VHOST: awx + MEMCACHED_HOST: localhost + MEMCACHED_PORT: 11211 diff --git a/examples/awx/docker-compose.yml b/examples/awx/docker-compose.yml new file mode 100644 index 0000000..673667c --- /dev/null +++ b/examples/awx/docker-compose.yml @@ -0,0 +1,67 @@ +version: '3' +services: + postgres: + image: "postgres:9.6" + environment: + POSTGRES_USER: awx + POSTGRES_PASSWORD: awxpass + POSTGRES_DB: awx + + rabbitmq: + image: "rabbitmq:3" + environment: + RABBITMQ_DEFAULT_VHOST: awx + + memcached: + image: "memcached:alpine" + + awx_web: + # image: "geerlingguy/awx_web:latest" + image: "ansible/awx_web:latest" + links: + - rabbitmq + - memcached + - postgres + ports: + - "8080:8052" + hostname: awxweb + user: root + environment: + SECRET_KEY: aabbcc + DATABASE_USER: awx + DATABASE_PASSWORD: awxpass + DATABASE_NAME: awx + DATABASE_PORT: 5432 + DATABASE_HOST: postgres + RABBITMQ_USER: guest + RABBITMQ_PASSWORD: guest + RABBITMQ_HOST: rabbitmq + RABBITMQ_PORT: 5672 + RABBITMQ_VHOST: awx + MEMCACHED_HOST: memcached + MEMCACHED_PORT: 11211 + + awx_task: + # image: "geerlingguy/awx_task:latest" + image: "ansible/awx_task:latest" + links: + - rabbitmq + - memcached + - awx_web:awxweb + - postgres + hostname: awx + user: root + environment: + SECRET_KEY: aabbcc + DATABASE_USER: awx + DATABASE_PASSWORD: awxpass + DATABASE_NAME: awx + DATABASE_PORT: 5432 + DATABASE_HOST: postgres + RABBITMQ_USER: guest + RABBITMQ_PASSWORD: guest + RABBITMQ_HOST: rabbitmq + RABBITMQ_PORT: 5672 + RABBITMQ_VHOST: awx + MEMCACHED_HOST: memcached + MEMCACHED_PORT: 11211 diff --git a/examples/busybox/docker-compose.yml b/examples/busybox/docker-compose.yml new file mode 100644 index 0000000..f62e72a --- /dev/null +++ b/examples/busybox/docker-compose.yml @@ -0,0 +1,41 @@ +version: "2" +services: + + redis: + image: redis:alpine + ports: + - "6379" + environment: + - SECRET_KEY=aabbcc + - ENV_IS_SET + + frontend: + image: busybox + #entrypoint: [] + command: ["/bin/busybox", "httpd", "-f", "-p", "8080"] + working_dir: / + environment: + SECRET_KEY2: aabbcc + ENV_IS_SET2: + ports: + - "8080" + links: + - redis:myredis + labels: + my.label: my_value + +#tmpfs: /run +#tmpfs: +# - /run +# - /tmp +#user: postgresql +#working_dir: /code +#domainname: foo.com +#hostname: foo +#ipc: host +#mac_address: 02:42:ac:11:65:43 +#privileged: true +#read_only: true +#shm_size: 64M +#stdin_open: true +#tty: true diff --git a/podman-compose.py b/podman-compose.py new file mode 100755 index 0000000..ac2abc5 --- /dev/null +++ b/podman-compose.py @@ -0,0 +1,242 @@ +#! /usr/bin/env python + +from __future__ import print_function + + +import os +import argparse +import subprocess +import time + +import json +import yaml + +def try_int(i, fallback=None): + try: return int(i) + except ValueError: pass + except TypeError: pass + return fallback + + +# https://docs.docker.com/compose/compose-file/#service-configuration-reference +# https://docs.docker.com/samples/ +# https://docs.docker.com/compose/gettingstarted/ +# https://docs.docker.com/compose/django/ +# https://docs.docker.com/compose/wordpress/ + +def tr_identity(project_name, services, given_containers): + containers=[] + for cnt in given_containers: + containers.append() + return [], containers + +def tr_1pod(project_name, services, given_containers): + """ + project_name: + services: {service_name: ["container_name1", "..."]}, currently only one is supported + given_containers: [{}, ...] + """ + pod=dict(name=project_name) + containers=[] + common_extra_hosts = [] + for srv, cnts in services.items(): + common_extra_hosts.append("{}:127.0.0.1".format(srv)) + for cnt in cnts: + common_extra_hosts.append("{}:127.0.0.1".format(cnt)) + for cnt0 in given_containers: + cnt = dict(cnt0, pod=project_name) + # services can be accessed as localhost because they are on one pod + extra_hosts = list(cnt.get("extra_hosts", [])) + extra_hosts.extend(common_extra_hosts) + # link aliases + for link in cnt.get("links", []): + a = link.strip().split(':', 1) + if len(a)==2: + alias=a[1].strip() + extra_hosts.append("{}:127.0.0.1".format(alias)) + cnt["extra_hosts"] = extra_hosts + containers.append(cnt) + return [pod], containers + +def tr_1podfw(project_name, services, given_containers): + pods, containers = tr_1pod(project_name, services, given_containers) + pod=pods[0] + ports=[] + for cnt in containers: + cnt_ports = cnt.get('ports') + if cnt_ports: + ports.extend(cnt_ports) + del cnt['ports'] + if ports: pod["ports"]=ports + return pods, containers + +def down(project_name, dirname, pods, containers): + for cnt in containers: + print("""podman stop -t=1 '{name}'""".format(**cnt)) + for cnt in containers: + print("""podman rm '{name}'""".format(**cnt)) + for pod in pods: + print("""podman pod rm '{name}'""".format(**pod)) + +def container_to_args(cnt, dirname): + pod=cnt.get('pod') or '' + args=[ + 'podman', 'run', '--pod={}'.format(pod), + '--name={}'.format(cnt.get('name')), + ] + args.extend(['-d']) + if cnt.get('read_only'): + args.append('--read-only') + for i in cnt.get('labels', []): + args.extend(['--label', i]) + env=cnt.get('environment', {}) + if isinstance(env, dict): + env=[("{}={}".format(k, v) if v else k) for k,v in env.items()] + for e in env: + args.extend(['-e', e]) + for i in cnt.get('env_file', []): + i=os.path.realpath(os.path.join(dirname, i)) + args.extend(['--env-file', i]) + for i in cnt.get('tmpfs', []): + args.extend(['--tmpfs', i]) + for i in cnt.get('volumes', []): + # TODO: make it absolute using os.path.realpath(i) + args.extend(['-v', i]) + for i in cnt.get('extra_hosts', []): + args.extend(['--add-host', i]) + for i in cnt.get('expose', []): + args.extend(['--expose', i]) + for i in cnt.get('ports', []): + args.extend(['-p', i]) + user=cnt.get('user') + if user is not None: + args.extend(['-u', user]) + if cnt.get('working_dir') is not None: + args.extend(['-w', cnt.get('working_dir')]) + if cnt.get('hostname'): + args.extend(['--hostname', cnt.get('hostname')]) + if cnt.get('shm_size'): + args.extend(['--shm_size', '{}'.format(cnt.get('shm_size'))]) + if cnt.get('stdin_open'): + args.append('-i') + if cnt.get('tty'): + args.append('--tty') + # currently podman shipped by fedora does not package this + #if cnt.get('init'): + # args.append('--init') + entrypoint = cnt.get('entrypoint') + if entrypoint is not None: + if isinstance(entrypoint, list): + args.extend(['--entrypoint', json.dumps(entrypoint)]) + else: + args.extend(['--entrypoint', entrypoint]) + args.append(cnt.get('image')) # command, ..etc. + command=cnt.get('command') + if command is not None: + # TODO: handle if command is string + args.extend(command) + return args + +def up(project_name, dirname, pods, containers): + os.chdir(dirname) + # no need remove them if they have same hash label + down(project_name, dirname, pods, containers) + for pod in pods: + args=[ + "podman", "pod", "create", + "--name={}".format(pod["name"]), + "--share", "cgroup,ipc", + ] + ports = pod.get("ports") or [] + for i in ports: + args.extend(['-p', i]) + print(" ".join(args)) + p=subprocess.Popen(args) + print(p.wait()) + #print(opts) + for cnt in containers: + # TODO: -e , --add-host, -v, --read-only + args = container_to_args(cnt, dirname) + print(" ".join(args)) + p=subprocess.Popen(args) + print(p.wait()) + #print("""podman run -d --pod='{pod}' --name='{name}' '{image}'""".format(**cnt)) + # subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0) + time.sleep(3600) + +def main(filename, project_name, no_ansi, transform_policy): + filename= os.path.realpath(filename) + dirname = os.path.dirname(filename) + dir_basename = os.path.basename(dirname) + if not project_name: project_name = dir_basename + with open(filename, 'r') as f: + compose=yaml.safe_load(f) + print(json.dumps(compose, indent=2)) + ver=compose.get('version') + services=compose.get('services') + podman_compose_labels=[ + "io.podman.compose.config-hash=123", + "io.podman.compose.project="+project_name, + "io.podman.compose.version=0.0.1", + ] + # other top-levels: + # volumes: {...} + # networks: {driver: ...} + # configs: {...} + # secrets: {...} + given_containers = [] + container_names_by_service = {} + for service_name, service_desc in services.items(): + replicas = try_int(service_desc.get('deploy', {}).get('replicas', '1')) + container_names_by_service[service_name]=[] + for num in range(1, replicas+1): + name = "{project_name}_{service_name}_{num}".format( + project_name=project_name, + service_name=service_name, + num=num, + ) + container_names_by_service[service_name].append(name) + #print(service_name,service_desc) + cnt=dict(name=name, num=num, service_name=service_name, **service_desc) + labels = cnt.get('labels') or [] + if isinstance(labels, dict): + labels=[("{}={}".format(k, v) if v else k) for k,v in labels.items()] + labels.extend(podman_compose_labels) + labels.extend([ + "com.docker.compose.container-number={}".format(num), + "com.docker.compose.service="+service_name, + ]) + cnt['labels'] = labels + given_containers.append(cnt) + #pods, containers = tr_1pod(project_name, container_names_by_service, given_containers) + pods, containers = tr_1podfw(project_name, container_names_by_service, given_containers) + #pods, containers = tr_identity(project_name, container_names_by_service, given_containers) + up(project_name, dirname, pods, containers) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('command', metavar='command', + help='command to run', + choices=['up', 'down'], nargs=1, default="up") + + parser.add_argument("-f", "--file", + help="Specify an alternate compose file (default: docker-compose.yml)", + type=str, default='docker-compose.yml') + parser.add_argument("-p", "--project-name", + help="Specify an alternate project name (default: directory name)", + type=str, default=None) + parser.add_argument("--no-ansi", + help="Do not print ANSI control characters", action='store_true') + + parser.add_argument("-t", "--transform_policy", + help="how to translate docker compose to podman [1pod|hostnet|accurate]", + choices=['1pod', '1podfw', 'hostnet', 'identity'], default='1podfw') + + args = parser.parse_args() + + main( + filename=args.file, + project_name=args.project_name, + no_ansi=args.no_ansi, + transform_policy=args.transform_policy)