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 return pods, containers
def run_podman(dry_run, podman_path, podman_args, wait=True, sleep=1): def mount_dict_vol_to_bind(compose, mount_dict):
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):
""" """
inspect volume to get directory inspect volume to get directory
create volume if needed create volume if needed
and return mount_dict as bind of that directory 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 if mount_dict["type"]!="volume": return mount_dict
vol_name = mount_dict["source"] vol_name = mount_dict["source"]
print("podman volume inspect {vol_name} || podman volume create {vol_name}".format(vol_name=vol_name)) 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' # 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: except subprocess.CalledProcessError:
subprocess.check_output([podman_path, "volume", "create", "-l", "io.podman.compose.project={}".format(proj_name), vol_name]) compose.podman.output(["volume", "create", "-l", "io.podman.compose.project={}".format(proj_name), vol_name])
out = subprocess.check_output([podman_path, "volume", "inspect", vol_name]) out = compose.podman.output(["volume", "inspect", vol_name])
src = json.loads(out)[0]["mountPoint"] src = json.loads(out)[0]["mountPoint"]
ret=dict(mount_dict, type="bind", source=src, _vol=vol_name) ret=dict(mount_dict, type="bind", source=src, _vol=vol_name)
bind_prop=ret.get("bind", {}).get("propagation") 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 except KeyError: pass
return ret 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) 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") mount_type = mount_desc.get("type")
source = mount_desc.get("source") source = mount_desc.get("source")
target = mount_desc["target"] target = mount_desc["target"]
@ -400,108 +392,74 @@ def mount_desc_to_args(mount_desc, podman_path, basedir, proj_name, srv_name, cn
else: else:
raise ValueError("unknown mount type:"+mount_type) 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): def container_to_args(compose, cnt):
transfer_service_status(services, container_names_by_service, "start", dry_run, podman_path) dirname = compose.dirname
shared_vols = compose.shared_vols
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):
pod = cnt.get('pod') or '' pod = cnt.get('pod') or ''
args = [ podman_args = [
'run', 'run',
'--name={}'.format(cnt.get('name')), '--name={}'.format(cnt.get('name')),
'-d' '-d'
] ]
if pod: if pod:
args.append('--pod={}'.format(pod)) podman_args.append('--pod={}'.format(pod))
sec = norm_as_list(cnt.get("security_opt")) sec = norm_as_list(cnt.get("security_opt"))
for s in sec: for s in sec:
args.extend(['--security-opt', s]) podman_args.extend(['--security-opt', s])
if cnt.get('read_only'): if cnt.get('read_only'):
args.append('--read-only') podman_args.append('--read-only')
for i in cnt.get('labels', []): for i in cnt.get('labels', []):
args.extend(['-l', i]) podman_args.extend(['-l', i])
net = cnt.get("network_mode") net = cnt.get("network_mode")
if net: if net:
args.extend(['--network', net]) podman_args.extend(['--network', net])
env = norm_as_list(cnt.get('environment', {})) env = norm_as_list(cnt.get('environment', {}))
for e in env: for e in env:
args.extend(['-e', e]) podman_args.extend(['-e', e])
for i in cnt.get('env_file', []): for i in cnt.get('env_file', []):
i = os.path.realpath(os.path.join(dirname, i)) 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', []) tmpfs_ls = cnt.get('tmpfs', [])
if is_str(tmpfs_ls): tmpfs_ls=[tmpfs_ls] if is_str(tmpfs_ls): tmpfs_ls=[tmpfs_ls]
for i in tmpfs_ls: for i in tmpfs_ls:
args.extend(['--tmpfs', i]) podman_args.extend(['--tmpfs', i])
for i in cnt.get('volumes', []): for volume in cnt.get('volumes', []):
# TODO: should we make it os.path.realpath(os.path.join(, i))? # TODO: should we make it os.path.realpath(os.path.join(, i))?
mount_args = mount_desc_to_args( mount_args = mount_desc_to_args(compose, volume, cnt['_service'], cnt['name'])
i, podman_path, dirname, podman_args.extend(['--mount', mount_args])
cnt['_project'], cnt['_service'], cnt['name'],
shared_vols
)
args.extend(['--mount', mount_args])
for i in cnt.get('extra_hosts', []): for i in cnt.get('extra_hosts', []):
args.extend(['--add-host', i]) podman_args.extend(['--add-host', i])
for i in cnt.get('expose', []): for i in cnt.get('expose', []):
args.extend(['--expose', i]) podman_args.extend(['--expose', i])
if cnt.get('publishall'): if cnt.get('publishall'):
args.append('-P') podman_args.append('-P')
for i in cnt.get('ports', []): for i in cnt.get('ports', []):
args.extend(['-p', i]) podman_args.extend(['-p', i])
user = cnt.get('user') user = cnt.get('user')
if user is not None: if user is not None:
args.extend(['-u', user]) podman_args.extend(['-u', user])
if cnt.get('working_dir') is not None: 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'): if cnt.get('hostname'):
args.extend(['--hostname', cnt.get('hostname')]) podman_args.extend(['--hostname', cnt.get('hostname')])
if cnt.get('shm_size'): 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'): if cnt.get('stdin_open'):
args.append('-i') podman_args.append('-i')
if cnt.get('tty'): if cnt.get('tty'):
args.append('--tty') podman_args.append('--tty')
# currently podman shipped by fedora does not package this # currently podman shipped by fedora does not package this
# if cnt.get('init'): # if cnt.get('init'):
# args.append('--init') # args.append('--init')
entrypoint = cnt.get('entrypoint') entrypoint = cnt.get('entrypoint')
if entrypoint is not None: if entrypoint is not None:
if is_str(entrypoint): if is_str(entrypoint):
args.extend(['--entrypoint', entrypoint]) podman_args.extend(['--entrypoint', entrypoint])
else: else:
args.extend(['--entrypoint', json.dumps(entrypoint)]) podman_args.extend(['--entrypoint', json.dumps(entrypoint)])
# WIP: healthchecks are still work in progress # WIP: healthchecks are still work in progress
healthcheck = cnt.get('healthcheck', None) or {} 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 its a string, its equivalent to specifying CMD-SHELL
if is_str(healthcheck_test): if is_str(healthcheck_test):
# podman does not add shell to handle command with whitespace # 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): elif is_list(healthcheck_test):
# If its a list, first item is either NONE, CMD or CMD-SHELL. # If its a list, first item is either NONE, CMD or CMD-SHELL.
healthcheck_type = healthcheck_test.pop(0) healthcheck_type = healthcheck_test.pop(0)
if healthcheck_type == 'NONE': if healthcheck_type == 'NONE':
args.append("--no-healthcheck") podman_args.append("--no-healthcheck")
elif healthcheck_type == 'CMD': 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]) "' '".join([cmd_quote(i) for i in healthcheck_test])
)]) )])
elif healthcheck_type == 'CMD-SHELL': elif healthcheck_type == 'CMD-SHELL':
if len(healthcheck_test)!=1: if len(healthcheck_test)!=1:
raise ValueError("'CMD_SHELL' takes a single string after it") 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: else:
raise ValueError( raise ValueError(
"unknown healthcheck test type [{}],\ "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. # interval, timeout and start_period are specified as durations.
if 'interval' in healthcheck: if 'interval' in healthcheck:
args.extend(['--healthcheck-interval', healthcheck['interval']]) podman_args.extend(['--healthcheck-interval', healthcheck['interval']])
if 'timeout' in healthcheck: if 'timeout' in healthcheck:
args.extend(['--healthcheck-timeout', healthcheck['timeout']]) podman_args.extend(['--healthcheck-timeout', healthcheck['timeout']])
if 'start_period' in healthcheck: 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 # convert other parameters to string
if 'retries' in healthcheck: 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') command = cnt.get('command')
if command is not None: if command is not None:
if is_str(command): if is_str(command):
args.extend([command]) podman_args.extend([command])
else: else:
args.extend(command) podman_args.extend(command)
return args return podman_args
def rec_deps(services, container_by_name, cnt, init_service): def rec_deps(services, container_by_name, cnt, init_service):
@ -583,82 +541,72 @@ def flat_deps(services, container_by_name):
for name, cnt in container_by_name.items(): for name, cnt in container_by_name.items():
rec_deps(services, container_by_name, cnt, cnt.get('_service')) 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): # podman and compose classes
for cnt in containers: ###################
if cnt.get('build'): continue
run_podman(dry_run, podman_path, ["pull", cnt["image"]], sleep=0)
def push(project_name, dirname, pods, containers, dry_run, podman_path, cmd_args): class Podman:
parser = argparse.ArgumentParser() def __init__(self, compose, podman_path='podman', dry_run=False):
parser.prog+=' push' self.compose = compose
parser.add_argument("--ignore-push-failures", action='store_true', self.podman_path = podman_path
help="Push what it can and ignores images with push failures. (not implemented)") self.dry_run = dry_run
parser.add_argument('services', metavar='services', nargs='*',
help='services to push')
args = parser.parse_args(cmd_args)
services = set(args.services)
for cnt in 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)
# pylint: disable=unused-argument def output(self, podman_args):
def build(project_name, dirname, pods, containers, dry_run, podman_path, podman_args=[]): cmd = [self.podman_path]+podman_args
for cnt in containers: return subprocess.check_output(cmd)
if 'build' not in cnt: continue
build_desc = cnt['build']
if not hasattr(build_desc, 'items'):
build_desc = dict(context=build_desc)
ctx = build_desc.get('context', '.')
dockerfile = os.path.join(ctx, build_desc.get("dockerfile", "Dockerfile"))
if not os.path.exists(dockerfile):
dockerfile = os.path.join(ctx, build_desc.get("dockerfile", "dockerfile"))
if not os.path.exists(dockerfile):
raise OSError("Dockerfile not found in "+ctx)
build_args = [
"build", "-t", cnt["image"],
"-f", dockerfile
]
build_args.extend(podman_args)
args_list = norm_as_list(build_desc.get('args', {}))
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): def run(self, podman_args, wait=True, sleep=1):
os.chdir(dirname) 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
# NOTE: podman does not cache, so don't always build class PodmanCompose:
# TODO: if build and the following command fails "podman inspect -t image <image_name>" then run build 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
# no need remove them if they have same hash label def run(self):
if no_cleanup == False: args = self._parse_args()
down(project_name, dirname, pods, containers, dry_run, podman_path) 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))
for pod in pods: self.podman = Podman(self, podman_path, global_args.dry_run)
args = [ cmd_name = global_args.command
"pod", "create", cmd = self.commands[cmd_name]
"--name={}".format(pod["name"]), cmd(self, args)
"--share", "net",
]
ports = pod.get("ports") or []
for i in ports:
args.extend(['-p', i])
run_podman(dry_run, podman_path, args)
for cnt in containers: def _parse_compose_file(self):
# TODO: -e , --add-host, -v, --read-only args = self.global_args
args = container_to_args(cnt, dirname, podman_path, shared_vols) cmd = args.command
run_podman(dry_run, podman_path, args) filename = args.file
project_name = args.project_name
no_ansi = args.no_ansi
def run_compose( no_cleanup = args.no_cleanup
cmd, cmd_args, filename, project_name, dry_run = args.dry_run
no_ansi, no_cleanup, dry_run, transform_policy = args.transform_policy
transform_policy, podman_path, host_env=None, host_env = None
):
if not os.path.exists(filename): if not os.path.exists(filename):
alt_path = filename.replace('.yml', '.yaml') alt_path = filename.replace('.yml', '.yaml')
if os.path.exists(alt_path): if os.path.exists(alt_path):
@ -669,18 +617,13 @@ def run_compose(
filename = os.path.realpath(filename) filename = os.path.realpath(filename)
dirname = os.path.dirname(filename) dirname = os.path.dirname(filename)
dir_basename = os.path.basename(dirname) dir_basename = os.path.basename(dirname)
self.dirname = dirname
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: if not project_name:
project_name = dir_basename project_name = dir_basename
self.project_name = project_name
dotenv_path = os.path.join(dirname, ".env") dotenv_path = os.path.join(dirname, ".env")
if os.path.exists(dotenv_path): if os.path.exists(dotenv_path):
@ -703,6 +646,7 @@ def run_compose(
shared_vols = compose.get('volumes', {}) shared_vols = compose.get('volumes', {})
# shared_vols = list(shared_vols.keys()) # shared_vols = list(shared_vols.keys())
shared_vols = set(shared_vols.keys()) shared_vols = set(shared_vols.keys())
self.shared_vols = shared_vols
podman_compose_labels = [ podman_compose_labels = [
"io.podman.compose.config-hash=123", "io.podman.compose.config-hash=123",
"io.podman.compose.project=" + project_name, "io.podman.compose.project=" + project_name,
@ -746,6 +690,7 @@ def run_compose(
cnt['_service'] = service_name cnt['_service'] = service_name
cnt['_project'] = project_name cnt['_project'] = project_name
given_containers.append(cnt) 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]) container_by_name = dict([(c["name"], c) for c in given_containers])
flat_deps(container_names_by_service, container_by_name) flat_deps(container_names_by_service, container_by_name)
#print("deps:", [(c["name"], c["_deps"]) for c in given_containers]) #print("deps:", [(c["name"], c["_deps"]) for c in given_containers])
@ -755,44 +700,32 @@ def run_compose(
tr = transformations[transform_policy] tr = transformations[transform_policy]
pods, containers = tr( pods, containers = tr(
project_name, container_names_by_service, given_containers) project_name, container_names_by_service, given_containers)
if cmd not in ["build", "push", "start", "stop", "restart"] and cmd_args: self.pods = pods
raise ValueError("'{}' does not accept any argument".format(cmd)) self.containers = containers
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))
def main(): def _parse_args(self):
parser = argparse.ArgumentParser() 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', parser.add_argument('command', metavar='command',
help='command to run', help='command to run, on of {}'.format(cmds),
choices=['up', 'down', 'start', 'stop', 'restart', 'build', 'pull', 'push'], nargs=None, default="up") choices=cmds, nargs=None, default="up")
parser.add_argument('args', nargs=argparse.REMAINDER) parser.add_argument('args', nargs=argparse.REMAINDER)
parser.add_argument("-f", "--file", parser.add_argument("-f", "--file",
help="Specify an alternate compose file (default: docker-compose.yml)", help="Specify an alternate compose file (default: docker-compose.yml)",
@ -813,19 +746,182 @@ def main():
help="how to translate docker compose to podman [1pod|hostnet|accurate]", help="how to translate docker compose to podman [1pod|hostnet|accurate]",
choices=['1pod', '1podfw', 'hostnet', 'cntnet', 'publishall', 'identity'], default='1podfw') choices=['1pod', '1podfw', 'hostnet', 'cntnet', 'publishall', 'identity'], default='1podfw')
args = parser.parse_args() podman_compose = PodmanCompose()
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
)
###################
# 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 compose.containers:
if 'build' not in cnt: continue
if services and cnt['_service'] not in services: continue
compose.podman.run(["push", cnt["image"]], sleep=0)
@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'):
build_desc = dict(context=build_desc)
ctx = build_desc.get('context', '.')
dockerfile = os.path.join(ctx, build_desc.get("dockerfile", "Dockerfile"))
if not os.path.exists(dockerfile):
dockerfile = os.path.join(ctx, build_desc.get("dockerfile", "dockerfile"))
if not os.path.exists(dockerfile):
raise OSError("Dockerfile not found in "+ctx)
build_args = [
"build", "-t", cnt["image"],
"-f", dockerfile
]
build_args.extend(podman_args)
args_list = norm_as_list(build_desc.get('args', {}))
for build_arg in args_list:
build_args.extend(("--build-arg", build_arg,))
build_args.append(ctx)
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 compose.global_args.no_cleanup == False:
compose.commands['down'](compose, 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:
podman_args.extend(['-p', i])
compose.podman.run(podman_args)
for cnt in compose.containers:
# TODO: -e , --add-host, -v, --read-only
podman_args = container_to_args(compose, cnt)
compose.podman.run(podman_args)
@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)
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)
@cmd_run(podman_compose, 'start', 'start specific services')
def compose_start(compose, args):
transfer_service_status(compose, args, 'start')
@cmd_run(podman_compose, 'stop', 'stop specific services')
def compose_stop(compose, args):
transfer_service_status(compose, args, 'start')
@cmd_run(podman_compose, 'restart', 'restart specific services')
def compose_restart(compose, args):
transfer_service_status(compose, args, 'restart')
###################
# 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():
podman_compose.run()
if __name__ == "__main__": if __name__ == "__main__":
main() main()