forked from extern/podman-compose
helps to add more commands and parse their arguments
This commit is contained in:
parent
6cbcd4d242
commit
eb72a71e9e
74
CONTRIBUTING.md
Normal file
74
CONTRIBUTING.md
Normal 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
|
@ -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 it’s a string, it’s equivalent to specifying CMD-SHELL
|
# If it’s a string, it’s 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 it’s a list, first item is either NONE, CMD or CMD-SHELL.
|
# If it’s 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()
|
||||||
|
Loading…
Reference in New Issue
Block a user