helps to add more commands and parse their arguments

This commit is contained in:
Muayyad Alsadi 2019-08-09 16:31:56 +03:00
parent 6cbcd4d242
commit eb72a71e9e
2 changed files with 456 additions and 286 deletions

74
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,74 @@
# Contributing to podman-compose
## Adding new commands
To add a command you need to add a function that is decorated
with `@cmd_run` passing the compose instance, command name and
description. the wrapped function should accept two arguments
the compose instance and the command-specific arguments (resulted
from python's `argparse` package) inside that command you can
run PodMan like this `compose.podman.run(['inspect', 'something'])`
and inside that function you can access `compose.pods`
and `compose.containers` ...etc.
Here is an example
```
@cmd_run(podman_compose, 'build', 'build images defined in the stack')
def compose_build(compose, args):
compose.podman.run(['build', 'something'])
```
## Command arguments parsing
Add a function that accept `parser` which is an instance from `argparse`.
In side that function you can call `parser.add_argument()`.
The function decorated with `@cmd_parse` accepting the compose instance,
and command names (as a list or as a string).
You can do this multiple times.
Here is an example
```
@cmd_parse(podman_compose, 'build')
def compose_build_parse(parser):
parser.add_argument("--pull",
help="attempt to pull a newer version of the image", action='store_true')
parser.add_argument("--pull-always",
help="attempt to pull a newer version of the image, Raise an error even if the image is present locally.", action='store_true')
```
NOTE: `@cmd_parse` should be after `@cmd_run`
## Calling a command from inside another
If you need to call `podman-compose down` from inside `podman-compose up`
do something like:
```
@cmd_run(podman_compose, 'up', 'up desc')
def compose_up(compose, args):
compose.commands['down'](compose, args)
# or
compose.commands['down'](argparse.Namespace(foo=123))
```
## Missing Commands (help needed)
bundle Generate a Docker bundle from the Compose file
config Validate and view the Compose file
create Create services
events Receive real time events from containers
exec Execute a command in a running container
images List images
kill Kill containers
logs View output from containers
pause Pause services
port Print the public port for a port binding
ps List containers
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
top Display the running processes
unpause Unpause services
version Show the Docker-Compose version information

View File

@ -324,33 +324,22 @@ def tr_1podfw(project_name, services, given_containers):
return pods, containers
def run_podman(dry_run, podman_path, podman_args, wait=True, sleep=1):
print("podman " + " ".join(podman_args))
if dry_run:
return None
cmd = [podman_path]+podman_args
# 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)
p = subprocess.Popen(cmd)
if wait:
print(p.wait())
if sleep:
time.sleep(sleep)
return p
def mount_dict_vol_to_bind(mount_dict, podman_path, proj_name, shared_vols):
def mount_dict_vol_to_bind(compose, mount_dict):
"""
inspect volume to get directory
create volume if needed
and return mount_dict as bind of that directory
"""
proj_name = compose.proj_name
shared_vols = compose.shared_vols
if mount_dict["type"]!="volume": return mount_dict
vol_name = mount_dict["source"]
print("podman volume inspect {vol_name} || podman volume create {vol_name}".format(vol_name=vol_name))
# podman volume list --format '{{.Name}}\t{{.MountPoint}}' -f 'label=io.podman.compose.project=HERE'
try: out = subprocess.check_output([podman_path, "volume", "inspect", vol_name])
try: out = compose.podman.output(["volume", "inspect", vol_name])
except subprocess.CalledProcessError:
subprocess.check_output([podman_path, "volume", "create", "-l", "io.podman.compose.project={}".format(proj_name), vol_name])
out = subprocess.check_output([podman_path, "volume", "inspect", vol_name])
compose.podman.output(["volume", "create", "-l", "io.podman.compose.project={}".format(proj_name), vol_name])
out = compose.podman.output(["volume", "inspect", vol_name])
src = json.loads(out)[0]["mountPoint"]
ret=dict(mount_dict, type="bind", source=src, _vol=vol_name)
bind_prop=ret.get("bind", {}).get("propagation")
@ -366,9 +355,12 @@ def mount_dict_vol_to_bind(mount_dict, podman_path, proj_name, shared_vols):
except KeyError: pass
return ret
def mount_desc_to_args(mount_desc, podman_path, basedir, proj_name, srv_name, cnt_name, shared_vols):
def mount_desc_to_args(compose, mount_desc, srv_name, cnt_name):
basedir = compose.dirname
proj_name = compose.project_name
shared_vols = compose.shared_vols
if is_str(mount_desc): mount_desc=parse_short_mount(mount_desc, basedir)
mount_desc = mount_dict_vol_to_bind(fix_mount_dict(mount_desc, srv_name, cnt_name), podman_path, proj_name, shared_vols)
mount_desc = mount_dict_vol_to_bind(compose, fix_mount_dict(mount_desc, srv_name, cnt_name))
mount_type = mount_desc.get("type")
source = mount_desc.get("source")
target = mount_desc["target"]
@ -400,108 +392,74 @@ def mount_desc_to_args(mount_desc, podman_path, basedir, proj_name, srv_name, cn
else:
raise ValueError("unknown mount type:"+mount_type)
# pylint: disable=unused-argument
def down(project_name, dirname, pods, containers, dry_run, podman_path):
for cnt in containers:
run_podman(dry_run, podman_path, [
"stop", "-t=1", cnt["name"]], sleep=0)
for cnt in containers:
run_podman(dry_run, podman_path, ["rm", cnt["name"]], sleep=0)
for pod in pods:
run_podman(dry_run, podman_path, ["pod", "rm", pod["name"]], sleep=0)
def start(services, container_names_by_service, dry_run, podman_path):
transfer_service_status(services, container_names_by_service, "start", dry_run, podman_path)
def stop(services, container_names_by_service, dry_run, podman_path):
transfer_service_status(services, container_names_by_service, "stop", dry_run, podman_path)
def restart(services, container_names_by_service, dry_run, podman_path):
transfer_service_status(services, container_names_by_service, "restart", dry_run, podman_path)
def transfer_service_status(services, container_names_by_service, action, dry_run, podman_path):
# TODO: handle dependencies, handle creations
targets = []
for service in services:
if service not in container_names_by_service:
raise ValueError("unknown service: " + service)
targets.extend(container_names_by_service[service])
for target in targets:
run_podman(dry_run, podman_path, [action, target], sleep = 0)
def container_to_args(cnt, dirname, podman_path, shared_vols):
def container_to_args(compose, cnt):
dirname = compose.dirname
shared_vols = compose.shared_vols
pod = cnt.get('pod') or ''
args = [
podman_args = [
'run',
'--name={}'.format(cnt.get('name')),
'-d'
]
if pod:
args.append('--pod={}'.format(pod))
podman_args.append('--pod={}'.format(pod))
sec = norm_as_list(cnt.get("security_opt"))
for s in sec:
args.extend(['--security-opt', s])
podman_args.extend(['--security-opt', s])
if cnt.get('read_only'):
args.append('--read-only')
podman_args.append('--read-only')
for i in cnt.get('labels', []):
args.extend(['-l', i])
podman_args.extend(['-l', i])
net = cnt.get("network_mode")
if net:
args.extend(['--network', net])
podman_args.extend(['--network', net])
env = norm_as_list(cnt.get('environment', {}))
for e in env:
args.extend(['-e', e])
podman_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])
podman_args.extend(['--env-file', i])
tmpfs_ls = cnt.get('tmpfs', [])
if is_str(tmpfs_ls): tmpfs_ls=[tmpfs_ls]
for i in tmpfs_ls:
args.extend(['--tmpfs', i])
for i in cnt.get('volumes', []):
podman_args.extend(['--tmpfs', i])
for volume in cnt.get('volumes', []):
# TODO: should we make it os.path.realpath(os.path.join(, i))?
mount_args = mount_desc_to_args(
i, podman_path, dirname,
cnt['_project'], cnt['_service'], cnt['name'],
shared_vols
)
args.extend(['--mount', mount_args])
mount_args = mount_desc_to_args(compose, volume, cnt['_service'], cnt['name'])
podman_args.extend(['--mount', mount_args])
for i in cnt.get('extra_hosts', []):
args.extend(['--add-host', i])
podman_args.extend(['--add-host', i])
for i in cnt.get('expose', []):
args.extend(['--expose', i])
podman_args.extend(['--expose', i])
if cnt.get('publishall'):
args.append('-P')
podman_args.append('-P')
for i in cnt.get('ports', []):
args.extend(['-p', i])
podman_args.extend(['-p', i])
user = cnt.get('user')
if user is not None:
args.extend(['-u', user])
podman_args.extend(['-u', user])
if cnt.get('working_dir') is not None:
args.extend(['-w', cnt.get('working_dir')])
podman_args.extend(['-w', cnt.get('working_dir')])
if cnt.get('hostname'):
args.extend(['--hostname', cnt.get('hostname')])
podman_args.extend(['--hostname', cnt.get('hostname')])
if cnt.get('shm_size'):
args.extend(['--shm_size', '{}'.format(cnt.get('shm_size'))])
podman_args.extend(['--shm_size', '{}'.format(cnt.get('shm_size'))])
if cnt.get('stdin_open'):
args.append('-i')
podman_args.append('-i')
if cnt.get('tty'):
args.append('--tty')
podman_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 is_str(entrypoint):
args.extend(['--entrypoint', entrypoint])
podman_args.extend(['--entrypoint', entrypoint])
else:
args.extend(['--entrypoint', json.dumps(entrypoint)])
podman_args.extend(['--entrypoint', json.dumps(entrypoint)])
# WIP: healthchecks are still work in progress
healthcheck = cnt.get('healthcheck', None) or {}
@ -512,20 +470,20 @@ def container_to_args(cnt, dirname, podman_path, shared_vols):
# If its a string, its equivalent to specifying CMD-SHELL
if is_str(healthcheck_test):
# podman does not add shell to handle command with whitespace
args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(cmd_quote(healthcheck_test))])
podman_args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(cmd_quote(healthcheck_test))])
elif is_list(healthcheck_test):
# If its a list, first item is either NONE, CMD or CMD-SHELL.
healthcheck_type = healthcheck_test.pop(0)
if healthcheck_type == 'NONE':
args.append("--no-healthcheck")
podman_args.append("--no-healthcheck")
elif healthcheck_type == 'CMD':
args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(
podman_args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(
"' '".join([cmd_quote(i) for i in healthcheck_test])
)])
elif healthcheck_type == 'CMD-SHELL':
if len(healthcheck_test)!=1:
raise ValueError("'CMD_SHELL' takes a single string after it")
args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(cmd_quote(healthcheck_test[0]))])
podman_args.extend(['--healthcheck-command', '/bin/sh -c {}'.format(cmd_quote(healthcheck_test[0]))])
else:
raise ValueError(
"unknown healthcheck test type [{}],\
@ -537,24 +495,24 @@ def container_to_args(cnt, dirname, podman_path, shared_vols):
# interval, timeout and start_period are specified as durations.
if 'interval' in healthcheck:
args.extend(['--healthcheck-interval', healthcheck['interval']])
podman_args.extend(['--healthcheck-interval', healthcheck['interval']])
if 'timeout' in healthcheck:
args.extend(['--healthcheck-timeout', healthcheck['timeout']])
podman_args.extend(['--healthcheck-timeout', healthcheck['timeout']])
if 'start_period' in healthcheck:
args.extend(['--healthcheck-start-period', healthcheck['start_period']])
podman_args.extend(['--healthcheck-start-period', healthcheck['start_period']])
# convert other parameters to string
if 'retries' in healthcheck:
args.extend(['--healthcheck-retries', '{}'.format(healthcheck['retries'])])
podman_args.extend(['--healthcheck-retries', '{}'.format(healthcheck['retries'])])
args.append(cnt.get('image')) # command, ..etc.
podman_args.append(cnt.get('image')) # command, ..etc.
command = cnt.get('command')
if command is not None:
if is_str(command):
args.extend([command])
podman_args.extend([command])
else:
args.extend(command)
return args
podman_args.extend(command)
return podman_args
def rec_deps(services, container_by_name, cnt, init_service):
@ -583,29 +541,268 @@ def flat_deps(services, container_by_name):
for name, cnt in container_by_name.items():
rec_deps(services, container_by_name, cnt, cnt.get('_service'))
# pylint: disable=unused-argument
def pull(project_name, dirname, pods, containers, dry_run, podman_path):
for cnt in containers:
if cnt.get('build'): continue
run_podman(dry_run, podman_path, ["pull", cnt["image"]], sleep=0)
###################
# podman and compose classes
###################
def push(project_name, dirname, pods, containers, dry_run, podman_path, cmd_args):
parser = argparse.ArgumentParser()
parser.prog+=' push'
parser.add_argument("--ignore-push-failures", action='store_true',
help="Push what it can and ignores images with push failures. (not implemented)")
parser.add_argument('services', metavar='services', nargs='*',
help='services to push')
args = parser.parse_args(cmd_args)
class Podman:
def __init__(self, compose, podman_path='podman', dry_run=False):
self.compose = compose
self.podman_path = podman_path
self.dry_run = dry_run
def output(self, podman_args):
cmd = [self.podman_path]+podman_args
return subprocess.check_output(cmd)
def run(self, podman_args, wait=True, sleep=1):
print("podman " + " ".join(podman_args))
if self.dry_run:
return None
cmd = [self.podman_path]+podman_args
# 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)
p = subprocess.Popen(cmd)
if wait:
print(p.wait())
if sleep:
time.sleep(sleep)
return p
class PodmanCompose:
def __init__(self):
self.commands = {}
self.global_args = None
self.project_name = None
self.dirname = None
self.pods = None
self.containers = None
self.shared_vols = None
self.container_names_by_service = None
def run(self):
args = self._parse_args()
global_args = self.global_args
podman_path = global_args.podman_path
if podman_path != 'podman':
if os.path.isfile(podman_path) and os.access(podman_path, os.X_OK):
podman_path = os.path.realpath(podman_path)
else:
# this also works if podman hasn't been installed now
if dry_run == False:
raise IOError(
"Binary {} has not been found.".format(podman_path))
self.podman = Podman(self, podman_path, global_args.dry_run)
cmd_name = global_args.command
cmd = self.commands[cmd_name]
cmd(self, args)
def _parse_compose_file(self):
args = self.global_args
cmd = args.command
filename = args.file
project_name = args.project_name
no_ansi = args.no_ansi
no_cleanup = args.no_cleanup
dry_run = args.dry_run
transform_policy = args.transform_policy
host_env = None
if not os.path.exists(filename):
alt_path = filename.replace('.yml', '.yaml')
if os.path.exists(alt_path):
filename = alt_path
else:
print("file [{}] not found".format(filename))
exit(-1)
filename = os.path.realpath(filename)
dirname = os.path.dirname(filename)
dir_basename = os.path.basename(dirname)
self.dirname = dirname
if not project_name:
project_name = dir_basename
self.project_name = project_name
dotenv_path = os.path.join(dirname, ".env")
if os.path.exists(dotenv_path):
with open(dotenv_path, 'r') as f:
dotenv_ls = [l.strip() for l in f if l.strip() and not l.startswith('#')]
dotenv_dict = dict([l.split("=", 1) for l in dotenv_ls if "=" in l])
else:
dotenv_dict = {}
with open(filename, 'r') as f:
compose = rec_subs(yaml.safe_load(f), [os.environ, dotenv_dict])
compose['_dirname']=dirname
# debug mode
#print(json.dumps(compose, indent = 2))
ver = compose.get('version')
services = compose.get('services')
# volumes: [...]
shared_vols = compose.get('volumes', {})
# shared_vols = list(shared_vols.keys())
shared_vols = set(shared_vols.keys())
self.shared_vols = shared_vols
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:
# 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):
name0 = "{project_name}_{service_name}_{num}".format(
project_name=project_name,
service_name=service_name,
num=num,
)
if num == 1:
name = service_desc.get("container_name", name0)
else:
name = name0
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)
if 'image' not in cnt:
cnt['image'] = "{project_name}_{service_name}".format(
project_name=project_name,
service_name=service_name,
)
labels = norm_as_list(cnt.get('labels'))
labels.extend(podman_compose_labels)
labels.extend([
"com.docker.compose.container-number={}".format(num),
"com.docker.compose.service=" + service_name,
])
cnt['labels'] = labels
cnt['_service'] = service_name
cnt['_project'] = project_name
given_containers.append(cnt)
self.container_names_by_service = container_names_by_service
container_by_name = dict([(c["name"], c) for c in given_containers])
flat_deps(container_names_by_service, container_by_name)
#print("deps:", [(c["name"], c["_deps"]) for c in given_containers])
given_containers = list(container_by_name.values())
given_containers.sort(key=lambda c: len(c.get('_deps') or []))
#print("sorted:", [c["name"] for c in given_containers])
tr = transformations[transform_policy]
pods, containers = tr(
project_name, container_names_by_service, given_containers)
self.pods = pods
self.containers = containers
def _parse_args(self):
parser = argparse.ArgumentParser()
self._init_global_parser(parser)
self.global_args = parser.parse_args()
self._parse_compose_file()
self.args = self._init_cmd_parser(self.global_args)
return self.args
def _init_cmd_parser(self, global_args):
cmd_name = global_args.command
parser = argparse.ArgumentParser(description=self.commands[cmd_name]._cmd_desc)
parser.prog+=' '+cmd_name
# self._init_global_parser(parser)
for cmd_parser in self.commands[cmd_name]._parse_args:
cmd_parser(parser)
return parser.parse_args(global_args.args)
def _init_global_parser(self, parser):
cmds = list(self.commands.keys())
parser.add_argument('command', metavar='command',
help='command to run, on of {}'.format(cmds),
choices=cmds, nargs=None, default="up")
parser.add_argument('args', nargs=argparse.REMAINDER)
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("--podman-path",
help="Specify an alternate path to podman (default: use location in $PATH variable)",
type=str, default="podman")
parser.add_argument("--no-ansi",
help="Do not print ANSI control characters", action='store_true')
parser.add_argument("--no-cleanup",
help="Do not stop and remove existing pod & containers", action='store_true')
parser.add_argument("--dry-run",
help="No action; perform a simulation of commands", action='store_true')
parser.add_argument("-t", "--transform_policy",
help="how to translate docker compose to podman [1pod|hostnet|accurate]",
choices=['1pod', '1podfw', 'hostnet', 'cntnet', 'publishall', 'identity'], default='1podfw')
podman_compose = PodmanCompose()
###################
# decorators to add commands and parse options
###################
class cmd_run:
def __init__(self, compose, cmd_name, cmd_desc):
self.compose = compose
self.cmd_name = cmd_name
self.cmd_desc = cmd_desc
def __call__(self, func):
def wrapped(*args, **kw):
return func(*args, **kw)
wrapped._compose = self.compose
wrapped._cmd_name = self.cmd_name
wrapped._cmd_desc = self.cmd_desc
wrapped._parse_args = []
self.compose.commands[self.cmd_name] = wrapped
return wrapped
class cmd_parse:
def __init__(self, compose, cmd_names):
self.compose = compose
self.cmd_names = cmd_names if is_list(cmd_names) else [cmd_names]
def __call__(self, func):
def wrapped(*args, **kw):
return func(*args, **kw)
for cmd_name in self.cmd_names:
self.compose.commands[cmd_name]._parse_args.append(wrapped)
return wrapped
###################
# actual commands
###################
@cmd_run(podman_compose, 'pull', 'pull stack images')
def compose_pull(compose, args):
for cnt in compose.containers:
if cnt.get('build'): continue
compose.podman.run(["pull", cnt["image"]], sleep=0)
@cmd_run(podman_compose, 'push', 'push stack images')
def compose_push(compose, args):
services = set(args.services)
for cnt in containers:
for cnt in compose.containers:
if 'build' not in cnt: continue
if services and cnt['_service'] not in services: continue
run_podman(dry_run, podman_path, ["push", cnt["image"]], sleep=0)
compose.podman.run(["push", cnt["image"]], sleep=0)
# pylint: disable=unused-argument
def build(project_name, dirname, pods, containers, dry_run, podman_path, podman_args=[]):
for cnt in containers:
@cmd_run(podman_compose, 'build', 'build stack images')
def compose_build(compose, args):
podman_args = []
if args.pull_always: podman_args.append("--pull-always")
elif args.pull: podman_args.append("--pull")
for cnt in compose.containers:
if 'build' not in cnt: continue
build_desc = cnt['build']
if not hasattr(build_desc, 'items'):
@ -625,207 +822,106 @@ def build(project_name, dirname, pods, containers, dry_run, podman_path, podman_
for build_arg in args_list:
build_args.extend(("--build-arg", build_arg,))
build_args.append(ctx)
run_podman(dry_run, podman_path, build_args, sleep=0)
def up(project_name, dirname, pods, containers, no_cleanup, dry_run, podman_path, shared_vols):
os.chdir(dirname)
compose.podman.run(build_args, sleep=0)
@cmd_run(podman_compose, 'up', 'Create and start the entire stack or some of its services')
def compose_up(compose, args):
shared_vols = compose.shared_vols
os.chdir(compose.dirname)
# NOTE: podman does not cache, so don't always build
# TODO: if build and the following command fails "podman inspect -t image <image_name>" then run build
# no need remove them if they have same hash label
if no_cleanup == False:
down(project_name, dirname, pods, containers, dry_run, podman_path)
if compose.global_args.no_cleanup == False:
compose.commands['down'](compose, args)
for pod in pods:
args = [
for pod in compose.pods:
podman_args = [
"pod", "create",
"--name={}".format(pod["name"]),
"--share", "net",
]
ports = pod.get("ports") or []
for i in ports:
args.extend(['-p', i])
run_podman(dry_run, podman_path, args)
podman_args.extend(['-p', i])
compose.podman.run(podman_args)
for cnt in containers:
for cnt in compose.containers:
# TODO: -e , --add-host, -v, --read-only
args = container_to_args(cnt, dirname, podman_path, shared_vols)
run_podman(dry_run, podman_path, args)
podman_args = container_to_args(compose, cnt)
compose.podman.run(podman_args)
def run_compose(
cmd, cmd_args, filename, project_name,
no_ansi, no_cleanup, dry_run,
transform_policy, podman_path, host_env=None,
):
if not os.path.exists(filename):
alt_path = filename.replace('.yml', '.yaml')
if os.path.exists(alt_path):
filename = alt_path
else:
print("file [{}] not found".format(filename))
exit(-1)
filename = os.path.realpath(filename)
dirname = os.path.dirname(filename)
dir_basename = os.path.basename(dirname)
@cmd_run(podman_compose, 'down', 'tear down entire stack')
def compose_down(compose, args):
for cnt in compose.containers:
compose.podman.run(["stop", "-t=1", cnt["name"]], sleep=0)
for cnt in compose.containers:
compose.podman.run(["rm", cnt["name"]], sleep=0)
for pod in compose.pods:
compose.podman.run(["pod", "rm", pod["name"]], sleep=0)
if podman_path != 'podman':
if os.path.isfile(podman_path) and os.access(podman_path, os.X_OK):
podman_path = os.path.realpath(podman_path)
else:
# this also works if podman hasn't been installed now
if dry_run == False:
raise IOError(
"Binary {} has not been found.".format(podman_path))
if not project_name:
project_name = dir_basename
def transfer_service_status(compose, args, action):
# TODO: handle dependencies, handle creations
container_names_by_service = compose.container_names_by_service
targets = []
for service in args.services:
if service not in container_names_by_service:
raise ValueError("unknown service: " + service)
targets.extend(container_names_by_service[service])
podman_args=[action]
timeout=getattr(args, 'timeout', None)
if timeout is not None:
podman_args.extend(['-t', "{}".format(timeout)])
for target in targets:
compose.podman.run(podman_args+[target], sleep=0)
dotenv_path = os.path.join(dirname, ".env")
if os.path.exists(dotenv_path):
with open(dotenv_path, 'r') as f:
dotenv_ls = [l.strip() for l in f if l.strip() and not l.startswith('#')]
dotenv_dict = dict([l.split("=", 1) for l in dotenv_ls if "=" in l])
else:
dotenv_dict = {}
@cmd_run(podman_compose, 'start', 'start specific services')
def compose_start(compose, args):
transfer_service_status(compose, args, 'start')
with open(filename, 'r') as f:
compose = rec_subs(yaml.safe_load(f), [os.environ, dotenv_dict])
@cmd_run(podman_compose, 'stop', 'stop specific services')
def compose_stop(compose, args):
transfer_service_status(compose, args, 'start')
compose['_dirname']=dirname
# debug mode
#print(json.dumps(compose, indent = 2))
@cmd_run(podman_compose, 'restart', 'restart specific services')
def compose_restart(compose, args):
transfer_service_status(compose, args, 'restart')
ver = compose.get('version')
services = compose.get('services')
# volumes: [...]
shared_vols = compose.get('volumes', {})
# shared_vols = list(shared_vols.keys())
shared_vols = set(shared_vols.keys())
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:
# 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):
name0 = "{project_name}_{service_name}_{num}".format(
project_name=project_name,
service_name=service_name,
num=num,
)
if num == 1:
name = service_desc.get("container_name", name0)
else:
name = name0
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)
if 'image' not in cnt:
cnt['image'] = "{project_name}_{service_name}".format(
project_name=project_name,
service_name=service_name,
)
labels = norm_as_list(cnt.get('labels'))
labels.extend(podman_compose_labels)
labels.extend([
"com.docker.compose.container-number={}".format(num),
"com.docker.compose.service=" + service_name,
])
cnt['labels'] = labels
cnt['_service'] = service_name
cnt['_project'] = project_name
given_containers.append(cnt)
container_by_name = dict([(c["name"], c) for c in given_containers])
flat_deps(container_names_by_service, container_by_name)
#print("deps:", [(c["name"], c["_deps"]) for c in given_containers])
given_containers = list(container_by_name.values())
given_containers.sort(key=lambda c: len(c.get('_deps') or []))
#print("sorted:", [c["name"] for c in given_containers])
tr = transformations[transform_policy]
pods, containers = tr(
project_name, container_names_by_service, given_containers)
if cmd not in ["build", "push", "start", "stop", "restart"] and cmd_args:
raise ValueError("'{}' does not accept any argument".format(cmd))
if cmd == "pull":
pull(project_name, dirname, pods, containers, dry_run, podman_path)
elif cmd == "push":
push(project_name, dirname, pods, containers, dry_run, podman_path, cmd_args)
elif cmd == "build":
parser = argparse.ArgumentParser()
parser.prog+=' build'
parser.add_argument("--pull",
help="attempt to pull a newer version of the image", action='store_true')
parser.add_argument("--pull-always",
help="attempt to pull a newer version of the image, Raise an error even if the image is present locally.", action='store_true')
args = parser.parse_args(cmd_args)
podman_args = []
if args.pull_always: podman_args.append("--pull-always")
elif args.pull: podman_args.append("--pull")
build(project_name, dirname, pods, containers, dry_run, podman_path, podman_args)
elif cmd == "up":
up(project_name, dirname, pods, containers,
no_cleanup, dry_run, podman_path, shared_vols)
elif cmd == "down":
down(project_name, dirname, pods, containers, dry_run, podman_path)
elif cmd == "start":
start(cmd_args, container_names_by_service, dry_run, podman_path)
elif cmd == "stop":
stop(cmd_args, container_names_by_service, dry_run, podman_path)
elif cmd == "restart":
restart(cmd_args, container_names_by_service, dry_run, podman_path)
else:
raise NotImplementedError("command {} is not implemented".format(cmd))
###################
# command arguments parsing
###################
@cmd_parse(podman_compose, ['stop', 'restart'])
def compose_stop_restart_parse(parser):
parser.add_argument("-t", "--timeout",
help="Specify a shutdown timeout in seconds. ",
type=float, default=10)
@cmd_parse(podman_compose, ['start', 'stop', 'restart'])
def compose_statu_parse(parser):
parser.add_argument('services', metavar='services', nargs='+',
help='affected services')
@cmd_parse(podman_compose, 'push')
def compose_push_parse(parser):
parser.add_argument("--ignore-push-failures", action='store_true',
help="Push what it can and ignores images with push failures. (not implemented)")
parser.add_argument('services', metavar='services', nargs='*',
help='services to push')
@cmd_parse(podman_compose, 'build')
def compose_build_parse(parser):
parser.add_argument("--pull",
help="attempt to pull a newer version of the image", action='store_true')
parser.add_argument("--pull-always",
help="attempt to pull a newer version of the image, Raise an error even if the image is present locally.", action='store_true')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('command', metavar='command',
help='command to run',
choices=['up', 'down', 'start', 'stop', 'restart', 'build', 'pull', 'push'], nargs=None, default="up")
parser.add_argument('args', nargs=argparse.REMAINDER)
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("--podman-path",
help="Specify an alternate path to podman (default: use location in $PATH variable)",
type=str, default="podman")
parser.add_argument("--no-ansi",
help="Do not print ANSI control characters", action='store_true')
parser.add_argument("--no-cleanup",
help="Do not stop and remove existing pod & containers", action='store_true')
parser.add_argument("--dry-run",
help="No action; perform a simulation of commands", action='store_true')
parser.add_argument("-t", "--transform_policy",
help="how to translate docker compose to podman [1pod|hostnet|accurate]",
choices=['1pod', '1podfw', 'hostnet', 'cntnet', 'publishall', 'identity'], default='1podfw')
args = parser.parse_args()
run_compose(
cmd=args.command,
cmd_args=args.args,
filename=args.file,
project_name=args.project_name,
no_ansi=args.no_ansi,
no_cleanup=args.no_cleanup,
dry_run=args.dry_run,
transform_policy=args.transform_policy,
podman_path=args.podman_path
)
podman_compose.run()
if __name__ == "__main__":
main()