From 762318093c367644c90ad0360345e6125352a4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Husiaty=C5=84ski?= Date: Tue, 1 Mar 2022 08:53:24 +0100 Subject: [PATCH] Force black formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Black removes the burden of manual code formatting and is by now considered the standard Python formatting tool. https://black.readthedocs.io/en/stable/ Format all Python code with black. GitHub linting action is updated to ensure all files are formatted with Black. Signed-off-by: Piotr HusiatyƄski --- .github/workflows/pylint.yml | 2 +- podman_compose.py | 1778 ++++++++++++++++++++++------------ pytests/test_volumes.py | 1 + setup.py | 30 +- test-requirements.txt | 1 + test_volumes.py | 1 + tests/test_podman_compose.py | 9 +- 7 files changed, 1180 insertions(+), 642 deletions(-) diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 45bfd48..d4c2ab7 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -12,6 +12,7 @@ jobs: python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 + - uses: psf/black@stable - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -26,4 +27,3 @@ jobs: python -m compileall podman_compose.py pylint podman_compose.py # pylint $(git ls-files '*.py') - diff --git a/podman_compose.py b/podman_compose.py index 677e9b3..bdca7fe 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -32,15 +32,16 @@ except ImportError: import yaml from dotenv import dotenv_values -__version__ = '1.0.4' +__version__ = "1.0.4" # helper functions -is_str = lambda s: isinstance(s, str) +is_str = lambda s: isinstance(s, str) is_dict = lambda d: isinstance(d, dict) is_list = lambda l: not is_str(l) and not is_dict(l) and hasattr(l, "__iter__") # identity filter filteri = lambda a: filter(lambda i: i, a) + def try_int(i, fallback=None): try: return int(i) @@ -50,6 +51,7 @@ def try_int(i, fallback=None): pass return fallback + def try_float(i, fallback=None): try: return float(i) @@ -59,31 +61,45 @@ def try_float(i, fallback=None): pass return fallback + def log(*msgs, sep=" ", end="\n"): - line = (sep.join([str(msg) for msg in msgs]))+end + line = (sep.join([str(msg) for msg in msgs])) + end sys.stderr.write(line) sys.stderr.flush() + dir_re = re.compile(r"^[~/\.]") -propagation_re = re.compile("^(?:z|Z|O|U|r?shared|r?slave|r?private|r?unbindable|r?bind|(?:no)?(?:exec|dev|suid))$") -norm_re = re.compile('[^-_a-z0-9]') -num_split_re = re.compile(r'(\d+|\D+)') +propagation_re = re.compile( + "^(?:z|Z|O|U|r?shared|r?slave|r?private|r?unbindable|r?bind|(?:no)?(?:exec|dev|suid))$" +) +norm_re = re.compile("[^-_a-z0-9]") +num_split_re = re.compile(r"(\d+|\D+)") PODMAN_CMDS = ( - "pull", "push", "build", "inspect", - "run", "start", "stop", "rm", "volume", + "pull", + "push", + "build", + "inspect", + "run", + "start", + "stop", + "rm", + "volume", ) + def ver_as_list(a): return [try_int(i, i) for i in num_split_re.findall(a)] + def strverscmp_lt(a, b): - a_ls = ver_as_list(a or '') - b_ls = ver_as_list(b or '') + a_ls = ver_as_list(a or "") + b_ls = ver_as_list(b or "") return a_ls < b_ls + def parse_short_mount(mount_str, basedir): - mount_a = mount_str.split(':') + mount_a = mount_str.split(":") mount_opt_dict = {} mount_opt = None if len(mount_a) == 1: @@ -94,13 +110,13 @@ def parse_short_mount(mount_str, basedir): mount_src, mount_dst = mount_a # dest must start with / like /foo:/var/lib/mysql # otherwise it's option like /var/lib/mysql:rw - if not mount_dst.startswith('/'): + if not mount_dst.startswith("/"): mount_dst, mount_opt = mount_a mount_src = None elif len(mount_a) == 3: mount_src, mount_dst, mount_opt = mount_a else: - raise ValueError("could not parse mount "+mount_str) + raise ValueError("could not parse mount " + mount_str) if mount_src and dir_re.match(mount_src): # Specify an absolute path mapping # - /opt/data:/var/lib/mysql @@ -109,31 +125,37 @@ def parse_short_mount(mount_str, basedir): # User-relative path # - ~/configs:/etc/configs/:ro mount_type = "bind" - mount_src = os.path.realpath(os.path.join(basedir, os.path.expanduser(mount_src))) + mount_src = os.path.realpath( + os.path.join(basedir, os.path.expanduser(mount_src)) + ) else: # Named volume # - datavolume:/var/lib/mysql mount_type = "volume" - mount_opts = filteri((mount_opt or '').split(',')) + mount_opts = filteri((mount_opt or "").split(",")) propagation_opts = [] for opt in mount_opts: - if opt == 'ro': mount_opt_dict["read_only"] = True - elif opt == 'rw': mount_opt_dict["read_only"] = False - elif opt in ('consistent', 'delegated', 'cached'): + if opt == "ro": + mount_opt_dict["read_only"] = True + elif opt == "rw": + mount_opt_dict["read_only"] = False + elif opt in ("consistent", "delegated", "cached"): mount_opt_dict["consistency"] = opt elif propagation_re.match(opt): propagation_opts.append(opt) else: # TODO: ignore - raise ValueError("unknown mount option "+opt) - mount_opt_dict["bind"] = dict(propagation=','.join(propagation_opts)) + raise ValueError("unknown mount option " + opt) + mount_opt_dict["bind"] = dict(propagation=",".join(propagation_opts)) return dict(type=mount_type, source=mount_src, target=mount_dst, **mount_opt_dict) + # NOTE: if a named volume is used but not defined it # gives ERROR: Named volume "abc" is used in service "xyz" # but no declaration was found in the volumes section. # unless it's anonymous-volume + def fix_mount_dict(compose, mount_dict, proj_name, srv_name): """ in-place fix mount dictionary to: @@ -142,26 +164,33 @@ def fix_mount_dict(compose, mount_dict, proj_name, srv_name): - if no source it would be generated """ # if already applied nothing todo - if "_vol" in mount_dict: return mount_dict + if "_vol" in mount_dict: + return mount_dict if mount_dict["type"] == "volume": vols = compose.vols source = mount_dict.get("source", None) vol = (vols.get(source, None) or {}) if source else {} - name = vol.get('name', None) + name = vol.get("name", None) mount_dict["_vol"] = vol # handle anonymouse or implied volume if not source: # missing source - vol["name"] = "_".join([ - proj_name, srv_name, - hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(), - ]) + vol["name"] = "_".join( + [ + proj_name, + srv_name, + hashlib.sha256(mount_dict["target"].encode("utf-8")).hexdigest(), + ] + ) elif not name: external = vol.get("external", None) - ext_name = external.get("name", None) if isinstance(external, dict) else None - vol["name"] = ext_name if ext_name else f"{proj_name}_{source}" + ext_name = ( + external.get("name", None) if isinstance(external, dict) else None + ) + vol["name"] = ext_name if ext_name else f"{proj_name}_{source}" return mount_dict + # docker and docker-compose support subset of bash variable substitution # https://docs.docker.com/compose/compose-file/#variable-substitution # https://docs.docker.com/compose/env-file/ @@ -174,7 +203,8 @@ def fix_mount_dict(compose, mount_dict, proj_name, srv_name): # ${VARIABLE?err} raise error if not set # $$ means $ -var_re = re.compile(r""" +var_re = re.compile( + r""" \$(?: (?P\$) | (?P[_a-zA-Z][_a-zA-Z0-9]*) | @@ -186,7 +216,10 @@ var_re = re.compile(r""" ))? }) ) -""", re.VERBOSE) +""", + re.VERBOSE, +) + def rec_subs(value, subs_dict): """ @@ -195,23 +228,26 @@ def rec_subs(value, subs_dict): if is_dict(value): value = {k: rec_subs(v, subs_dict) for k, v in value.items()} elif is_str(value): + def convert(m): if m.group("escaped") is not None: return "$" name = m.group("named") or m.group("braced") value = subs_dict.get(name) - if value == "" and m.group('empty'): + if value == "" and m.group("empty"): value = None if value is not None: return str(value) if m.group("err") is not None: raise RuntimeError(m.group("err")) return m.group("default") or "" + value = var_re.sub(convert, value) elif hasattr(value, "__iter__"): value = [rec_subs(i, subs_dict) for i in value] return value + def norm_as_list(src): """ given a dictionary {key1:value1, key2: None} or list @@ -248,6 +284,7 @@ def norm_as_dict(src): raise ValueError("dictionary or iterable is expected") return dst + def norm_ulimit(inner_value): if is_dict(inner_value): if not inner_value.keys() & {"soft", "hard"}: @@ -255,11 +292,13 @@ def norm_ulimit(inner_value): soft = inner_value.get("soft", inner_value.get("hard", None)) hard = inner_value.get("hard", inner_value.get("soft", None)) return f"{soft}:{hard}" - if is_list(inner_value): return norm_ulimit(norm_as_dict(inner_value)) + if is_list(inner_value): + return norm_ulimit(norm_as_dict(inner_value)) # if int or string return as is return inner_value -#def tr_identity(project_name, given_containers): + +# def tr_identity(project_name, given_containers): # pod_name = f'pod_{project_name}' # pod = dict(name=pod_name) # containers = [] @@ -267,6 +306,7 @@ def norm_ulimit(inner_value): # containers.append(dict(cnt, pod=pod_name)) # return [pod], containers + def tr_identity(project_name, given_containers): pod_name = f"pod_{project_name}" pod = dict(name=pod_name) @@ -285,26 +325,37 @@ def assert_volume(compose, mount_dict): if mount_dict["type"] == "bind": basedir = os.path.realpath(compose.dirname) mount_src = mount_dict["source"] - mount_src = os.path.realpath(os.path.join(basedir, os.path.expanduser(mount_src))) + mount_src = os.path.realpath( + os.path.join(basedir, os.path.expanduser(mount_src)) + ) if not os.path.exists(mount_src): try: os.makedirs(mount_src, exist_ok=True) except OSError: pass return - if mount_dict["type"] != "volume" or not vol or vol.get("external", None) or not vol.get("name", None): return + if ( + mount_dict["type"] != "volume" + or not vol + or vol.get("external", None) + or not vol.get("name", None) + ): + return proj_name = compose.project_name vol_name = vol["name"] log(f"podman volume inspect {vol_name} || podman volume create {vol_name}") # TODO: might move to using "volume list" # podman volume list --format '{{.Name}}\t{{.MountPoint}}' -f 'label=io.podman.compose.project=HERE' - try: _ = compose.podman.output([], "volume", ["inspect", vol_name]).decode('utf-8') + try: + _ = compose.podman.output([], "volume", ["inspect", vol_name]).decode("utf-8") except subprocess.CalledProcessError: labels = vol.get("labels", None) or [] args = [ "create", - "--label", f"io.podman.compose.project={proj_name}", - "--label", f"com.docker.compose.project={proj_name}", + "--label", + f"io.podman.compose.project={proj_name}", + "--label", + f"com.docker.compose.project={proj_name}", ] for item in norm_as_list(labels): args.extend(["--label", item]) @@ -316,20 +367,25 @@ def assert_volume(compose, mount_dict): args.extend(["--opt", f"{opt}={value}"]) args.append(vol_name) compose.podman.output([], "volume", args) - _ = compose.podman.output([], "volume", ["inspect", vol_name]).decode('utf-8') + _ = compose.podman.output([], "volume", ["inspect", vol_name]).decode("utf-8") -def mount_desc_to_mount_args(compose, mount_desc, srv_name, cnt_name): # pylint: disable=unused-argument + +def mount_desc_to_mount_args( + compose, mount_desc, srv_name, cnt_name +): # pylint: disable=unused-argument mount_type = mount_desc.get("type", None) - vol = mount_desc.get("_vol", None) if mount_type=="volume" else None + vol = mount_desc.get("_vol", None) if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source", None) target = mount_desc["target"] opts = [] if mount_desc.get(mount_type, None): # TODO: we might need to add mount_dict[mount_type]["propagation"] = "z" mount_prop = mount_desc.get(mount_type, {}).get("propagation", None) - if mount_prop: opts.append(f"{mount_type}-propagation={mount_prop}") - if mount_desc.get("read_only", False): opts.append("ro") - if mount_type == 'tmpfs': + if mount_prop: + opts.append(f"{mount_type}-propagation={mount_prop}") + if mount_desc.get("read_only", False): + opts.append("ro") + if mount_type == "tmpfs": tmpfs_opts = mount_desc.get("tmpfs", {}) tmpfs_size = tmpfs_opts.get("size", None) if tmpfs_size: @@ -338,41 +394,52 @@ def mount_desc_to_mount_args(compose, mount_desc, srv_name, cnt_name): # pylint: if tmpfs_mode: opts.append(f"tmpfs-mode={tmpfs_mode}") opts = ",".join(opts) - if mount_type == 'bind': + if mount_type == "bind": return f"type=bind,source={source},destination={target},{opts}".rstrip(",") - if mount_type == 'volume': + if mount_type == "volume": return f"type=volume,source={source},destination={target},{opts}".rstrip(",") - if mount_type == 'tmpfs': + if mount_type == "tmpfs": return f"type=tmpfs,destination={target},{opts}".rstrip(",") - raise ValueError("unknown mount type:"+mount_type) + raise ValueError("unknown mount type:" + mount_type) + def container_to_ulimit_args(cnt, podman_args): - ulimit = cnt.get('ulimits', []) + ulimit = cnt.get("ulimits", []) if ulimit is not None: # ulimit can be a single value, i.e. ulimit: host if is_str(ulimit): - podman_args.extend(['--ulimit', ulimit]) + podman_args.extend(["--ulimit", ulimit]) # or a dictionary or list: else: ulimit = norm_as_dict(ulimit) - ulimit = [ "{}={}".format(ulimit_key, norm_ulimit(inner_value)) for ulimit_key, inner_value in ulimit.items()] + ulimit = [ + "{}={}".format(ulimit_key, norm_ulimit(inner_value)) + for ulimit_key, inner_value in ulimit.items() + ] for i in ulimit: - podman_args.extend(['--ulimit', i]) + podman_args.extend(["--ulimit", i]) -def mount_desc_to_volume_args(compose, mount_desc, srv_name, cnt_name): # pylint: disable=unused-argument + +def mount_desc_to_volume_args( + compose, mount_desc, srv_name, cnt_name +): # pylint: disable=unused-argument mount_type = mount_desc["type"] - if mount_type not in ('bind', 'volume'): - raise ValueError("unknown mount type:"+mount_type) - vol = mount_desc.get("_vol", None) if mount_type=="volume" else None + if mount_type not in ("bind", "volume"): + raise ValueError("unknown mount type:" + mount_type) + vol = mount_desc.get("_vol", None) if mount_type == "volume" else None source = vol["name"] if vol else mount_desc.get("source", None) if not source: raise ValueError(f"missing mount source for {mount_type} on {srv_name}") target = mount_desc["target"] opts = [] - propagations = set(filteri(mount_desc.get(mount_type, {}).get("propagation", "").split(','))) - if mount_type != 'bind': - propagations.update(filteri(mount_desc.get('bind', {}).get("propagation", "").split(','))) + propagations = set( + filteri(mount_desc.get(mount_type, {}).get("propagation", "").split(",")) + ) + if mount_type != "bind": + propagations.update( + filteri(mount_desc.get("bind", {}).get("propagation", "").split(",")) + ) opts.extend(propagations) # --volume, -v[=[[SOURCE-VOLUME|HOST-DIR:]CONTAINER-DIR[:OPTIONS]]] # [rw|ro] @@ -386,78 +453,84 @@ def mount_desc_to_volume_args(compose, mount_desc, srv_name, cnt_name): # pylint # [U] read_only = mount_desc.get("read_only", None) if read_only is not None: - opts.append('ro' if read_only else 'rw') - args = f'{source}:{target}' - if opts: args += ':' + ','.join(opts) + opts.append("ro" if read_only else "rw") + args = f"{source}:{target}" + if opts: + args += ":" + ",".join(opts) return args + def get_mnt_dict(compose, cnt, volume): proj_name = compose.project_name - srv_name = cnt['_service'] + srv_name = cnt["_service"] basedir = compose.dirname if is_str(volume): volume = parse_short_mount(volume, basedir) return fix_mount_dict(compose, volume, proj_name, srv_name) + def get_mount_args(compose, cnt, volume): volume = get_mnt_dict(compose, cnt, volume) # proj_name = compose.project_name - srv_name = cnt['_service'] + srv_name = cnt["_service"] mount_type = volume["type"] assert_volume(compose, volume) if compose.prefer_volume_over_mount: - if mount_type == 'tmpfs': + if mount_type == "tmpfs": # TODO: --tmpfs /tmp:rw,size=787448k,mode=1777 - args = volume['target'] + args = volume["target"] tmpfs_opts = volume.get("tmpfs", {}) opts = [] size = tmpfs_opts.get("size", None) - if size: opts.append(f'size={size}') + if size: + opts.append(f"size={size}") mode = tmpfs_opts.get("mode", None) - if mode: opts.append(f'mode={mode}') - if opts: args += ':' + ','.join(opts) - return ['--tmpfs', args] - args = mount_desc_to_volume_args(compose, volume, srv_name, cnt['name']) - return ['-v', args] - args = mount_desc_to_mount_args(compose, volume, srv_name, cnt['name']) - return ['--mount', args] + if mode: + opts.append(f"mode={mode}") + if opts: + args += ":" + ",".join(opts) + return ["--tmpfs", args] + args = mount_desc_to_volume_args(compose, volume, srv_name, cnt["name"]) + return ["-v", args] + args = mount_desc_to_mount_args(compose, volume, srv_name, cnt["name"]) + return ["--mount", args] def get_secret_args(compose, cnt, secret): - secret_name = secret if is_str(secret) else secret.get('source', None) + secret_name = secret if is_str(secret) else secret.get("source", None) if not secret_name or secret_name not in compose.declared_secrets.keys(): raise ValueError( f'ERROR: undeclared secret: "{secret}", service: {cnt["_service"]}' ) declared_secret = compose.declared_secrets[secret_name] - source_file = declared_secret.get('file', None) - dest_file = '' - secret_opts = '' + source_file = declared_secret.get("file", None) + dest_file = "" + secret_opts = "" - target = None if is_str(secret) else secret.get('target', None) - uid = None if is_str(secret) else secret.get('uid', None) - gid = None if is_str(secret) else secret.get('gid', None) - mode = None if is_str(secret) else secret.get('mode', None) + target = None if is_str(secret) else secret.get("target", None) + uid = None if is_str(secret) else secret.get("uid", None) + gid = None if is_str(secret) else secret.get("gid", None) + mode = None if is_str(secret) else secret.get("mode", None) if source_file: if not target: - dest_file = f'/run/secrets/{secret_name}' + dest_file = f"/run/secrets/{secret_name}" elif not target.startswith("/"): sec = target if target else secret_name - dest_file = f'/run/secrets/{sec}' + dest_file = f"/run/secrets/{sec}" else: dest_file = target basedir = compose.dirname - source_file = os.path.realpath(os.path.join(basedir, os.path.expanduser(source_file))) - volume_ref = [ - '--volume', f'{source_file}:{dest_file}:ro,rprivate,rbind' - ] + source_file = os.path.realpath( + os.path.join(basedir, os.path.expanduser(source_file)) + ) + volume_ref = ["--volume", f"{source_file}:{dest_file}:ro,rprivate,rbind"] if uid or gid or mode: sec = target if target else secret_name log( f'WARNING: Service {cnt["_service"]} uses secret "{sec}" with uid, gid, or mode.' - + ' These fields are not supported by this implementation of the Compose file' + + " These fields are not supported by this implementation of the Compose file" ) return volume_ref # v3.5 and up added external flag, earlier the spec @@ -467,61 +540,88 @@ def get_secret_args(compose, cnt, secret): # since these commands are directly translated to # podman-create commands, albiet we can only support a 1:1 mapping # at the moment - if declared_secret.get('external', False) or declared_secret.get('name', None): - secret_opts += f',uid={uid}' if uid else '' - secret_opts += f',gid={gid}' if gid else '' - secret_opts += f',mode={mode}' if mode else '' + if declared_secret.get("external", False) or declared_secret.get("name", None): + secret_opts += f",uid={uid}" if uid else "" + secret_opts += f",gid={gid}" if gid else "" + secret_opts += f",mode={mode}" if mode else "" # The target option is only valid for type=env, # which in an ideal world would work # for type=mount as well. # having a custom name for the external secret # has the same problem as well - ext_name = declared_secret.get('name', None) + ext_name = declared_secret.get("name", None) err_str = 'ERROR: Custom name/target reference "{}" for mounted external secret "{}" is not supported' if ext_name and ext_name != secret_name: raise ValueError(err_str.format(secret_name, ext_name)) if target and target != secret_name: raise ValueError(err_str.format(target, secret_name)) if target: - log('WARNING: Service "{}" uses target: "{}" for secret: "{}".' - .format(cnt['_service'], target, secret_name) - + ' That is un-supported and a no-op and is ignored.') - return [ '--secret', '{}{}'.format(secret_name, secret_opts) ] + log( + 'WARNING: Service "{}" uses target: "{}" for secret: "{}".'.format( + cnt["_service"], target, secret_name + ) + + " That is un-supported and a no-op and is ignored." + ) + return ["--secret", "{}{}".format(secret_name, secret_opts)] - raise ValueError('ERROR: unparseable secret: "{}", service: "{}"' - .format(secret_name, cnt['_service'])) + raise ValueError( + 'ERROR: unparseable secret: "{}", service: "{}"'.format( + secret_name, cnt["_service"] + ) + ) def container_to_res_args(cnt, podman_args): # v2: https://docs.docker.com/compose/compose-file/compose-file-v2/#cpu-and-other-resources # cpus, cpu_shares, mem_limit, mem_reservation - cpus_limit_v2 = try_float(cnt.get('cpus', None), None) - cpu_shares_v2 = try_int(cnt.get('cpu_shares', None), None) - mem_limit_v2 = cnt.get('mem_limit', None) - mem_res_v2 = cnt.get('mem_reservation', None) + cpus_limit_v2 = try_float(cnt.get("cpus", None), None) + cpu_shares_v2 = try_int(cnt.get("cpu_shares", None), None) + mem_limit_v2 = cnt.get("mem_limit", None) + mem_res_v2 = cnt.get("mem_reservation", None) # v3: https://docs.docker.com/compose/compose-file/compose-file-v3/#resources # spec: https://github.com/compose-spec/compose-spec/blob/master/deploy.md#resources # deploy.resources.{limits,reservations}.{cpus, memory} - deploy = cnt.get('deploy', None) or {} - res = deploy.get('resources', None) or {} - limits = res.get('limits', None) or {} - cpus_limit_v3 = try_float(limits.get('cpus', None), None) - mem_limit_v3 = limits.get('memory', None) - reservations = res.get('reservations', None) or {} - #cpus_res_v3 = try_float(reservations.get('cpus', None), None) - mem_res_v3 = reservations.get('memory', None) + deploy = cnt.get("deploy", None) or {} + res = deploy.get("resources", None) or {} + limits = res.get("limits", None) or {} + cpus_limit_v3 = try_float(limits.get("cpus", None), None) + mem_limit_v3 = limits.get("memory", None) + reservations = res.get("reservations", None) or {} + # cpus_res_v3 = try_float(reservations.get('cpus', None), None) + mem_res_v3 = reservations.get("memory", None) # add args cpus = cpus_limit_v3 or cpus_limit_v2 if cpus: - podman_args.extend(('--cpus', str(cpus),)) + podman_args.extend( + ( + "--cpus", + str(cpus), + ) + ) if cpu_shares_v2: - podman_args.extend(('--cpu-shares', str(cpu_shares_v2),)) + podman_args.extend( + ( + "--cpu-shares", + str(cpu_shares_v2), + ) + ) mem = mem_limit_v3 or mem_limit_v2 if mem: - podman_args.extend(('-m', str(mem).lower(),)) + podman_args.extend( + ( + "-m", + str(mem).lower(), + ) + ) mem_res = mem_res_v3 or mem_res_v2 if mem_res: - podman_args.extend(('--memory-reservation', str(mem_res).lower(),)) + podman_args.extend( + ( + "--memory-reservation", + str(mem_res).lower(), + ) + ) + def port_dict_to_str(port_desc): # NOTE: `mode: host|ingress` is ignored @@ -535,10 +635,11 @@ def port_dict_to_str(port_desc): ret = f"{host_ip}:{published}:{cnt_port}" else: ret = f"{published}:{cnt_port}" if published else f"{cnt_port}" - if protocol!="tcp": - ret+= f"/{protocol}" + if protocol != "tcp": + ret += f"/{protocol}" return ret + def norm_ports(ports_in): if not ports_in: ports_in = [] @@ -553,6 +654,7 @@ def norm_ports(ports_in): ports_out.append(port) return ports_out + def assert_cnt_nets(compose, cnt): """ create missing networks @@ -572,15 +674,22 @@ def assert_cnt_nets(compose, cnt): is_ext = net_desc.get("external", None) ext_desc = is_ext if is_dict(is_ext) else {} default_net_name = net if is_ext else f"{proj_name}_{net}" - net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name - try: compose.podman.output([], "network", ["exists", net_name]) + net_name = ( + ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name + ) + try: + compose.podman.output([], "network", ["exists", net_name]) except subprocess.CalledProcessError as e: if is_ext: - raise RuntimeError(f"External network [{net_name}] does not exists") from e + raise RuntimeError( + f"External network [{net_name}] does not exists" + ) from e args = [ "create", - "--label", f"io.podman.compose.project={proj_name}", - "--label", f"com.docker.compose.project={proj_name}", + "--label", + f"io.podman.compose.project={proj_name}", + "--label", + f"com.docker.compose.project={proj_name}", ] # TODO: add more options here, like driver, internal, ..etc labels = net_desc.get("labels", None) or [] @@ -591,35 +700,41 @@ def assert_cnt_nets(compose, cnt): driver = net_desc.get("driver", None) if driver: args.extend(("--driver", driver)) - ipam_config_ls = (net_desc.get("ipam", None) or {}).get("config", None) or [] + ipam_config_ls = (net_desc.get("ipam", None) or {}).get( + "config", None + ) or [] if is_dict(ipam_config_ls): - ipam_config_ls=[ipam_config_ls] + ipam_config_ls = [ipam_config_ls] for ipam in ipam_config_ls: subnet = ipam.get("subnet", None) ip_range = ipam.get("ip_range", None) gateway = ipam.get("gateway", None) - if subnet: args.extend(("--subnet", subnet)) - if ip_range: args.extend(("--ip-range", ip_range)) - if gateway: args.extend(("--gateway", gateway)) + if subnet: + args.extend(("--subnet", subnet)) + if ip_range: + args.extend(("--ip-range", ip_range)) + if gateway: + args.extend(("--gateway", gateway)) args.append(net_name) compose.podman.output([], "network", args) compose.podman.output([], "network", ["exists", net_name]) + def get_net_args(compose, cnt): service_name = cnt["service_name"] net = cnt.get("network_mode", None) if net: - if net=="host": - return ['--network', net] + if net == "host": + return ["--network", net] if net.startswith("slirp4netns:"): - return ['--network', net] + return ["--network", net] if net.startswith("service:"): other_srv = net.split(":", 1)[1].strip() other_cnt = compose.container_names_by_service[other_srv][0] - return ['--network', f"container:{other_cnt}"] + return ["--network", f"container:{other_cnt}"] if net.startswith("container:"): - other_cnt = net.split(":",1)[1].strip() - return ['--network', f"container:{other_cnt}"] + other_cnt = net.split(":", 1)[1].strip() + return ["--network", f"container:{other_cnt}"] proj_name = compose.project_name default_net = compose.default_net nets = compose.networks @@ -632,7 +747,8 @@ def get_net_args(compose, cnt): # cnt_nets is {net_key: net_value, ...} for net_value in cnt_nets.values(): aliases.extend(norm_as_list(net_value.get("aliases", None))) - if ip: continue + if ip: + continue ip = net_value.get("ipv4_address", None) cnt_nets = list(cnt_nets.keys()) cnt_nets = norm_as_list(cnt_nets or default_net) @@ -642,7 +758,9 @@ def get_net_args(compose, cnt): is_ext = net_desc.get("external", None) ext_desc = is_ext if is_dict(is_ext) else {} default_net_name = net if is_ext else f"{proj_name}_{net}" - net_name = ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name + net_name = ( + ext_desc.get("name", None) or net_desc.get("name", None) or default_net_name + ) net_names.add(net_name) net_names_str = ",".join(net_names) net_args = ["--net", net_names_str, "--network-alias", ",".join(aliases)] @@ -654,75 +772,77 @@ def get_net_args(compose, cnt): def container_to_args(compose, cnt, detached=True): # TODO: double check -e , --add-host, -v, --read-only dirname = compose.dirname - pod = cnt.get('pod', None) or '' - name = cnt['name'] - podman_args = [f'--name={name}'] + pod = cnt.get("pod", None) or "" + name = cnt["name"] + podman_args = [f"--name={name}"] if detached: podman_args.append("-d") if pod: - podman_args.append(f'--pod={pod}') + podman_args.append(f"--pod={pod}") deps = [] - for dep_srv in (cnt.get("_deps", None) or []): + for dep_srv in cnt.get("_deps", None) or []: deps.extend(compose.container_names_by_service.get(dep_srv, None) or []) if deps: deps_csv = ",".join(deps) - podman_args.append(f'--requires={deps_csv}') + podman_args.append(f"--requires={deps_csv}") sec = norm_as_list(cnt.get("security_opt", None)) for sec_item in sec: - podman_args.extend(['--security-opt', sec_item]) + podman_args.extend(["--security-opt", sec_item]) ann = norm_as_list(cnt.get("annotations", None)) for a in ann: - podman_args.extend(['--annotation', a]) - if cnt.get('read_only', None): - podman_args.append('--read-only') - for i in cnt.get('labels', []): - podman_args.extend(['--label', i]) - for c in cnt.get('cap_add', []): - podman_args.extend(['--cap-add', c]) - for c in cnt.get('cap_drop', []): - podman_args.extend(['--cap-drop', c]) - for item in cnt.get('devices', []): - podman_args.extend(['--device', item]) - for item in norm_as_list(cnt.get('dns', None)): - podman_args.extend(['--dns', item]) - for item in norm_as_list(cnt.get('dns_opt', None)): - podman_args.extend(['--dns-opt', item]) - for item in norm_as_list(cnt.get('dns_search', None)): - podman_args.extend(['--dns-search', item]) - env_file = cnt.get('env_file', []) - if is_str(env_file): env_file = [env_file] + podman_args.extend(["--annotation", a]) + if cnt.get("read_only", None): + podman_args.append("--read-only") + for i in cnt.get("labels", []): + podman_args.extend(["--label", i]) + for c in cnt.get("cap_add", []): + podman_args.extend(["--cap-add", c]) + for c in cnt.get("cap_drop", []): + podman_args.extend(["--cap-drop", c]) + for item in cnt.get("devices", []): + podman_args.extend(["--device", item]) + for item in norm_as_list(cnt.get("dns", None)): + podman_args.extend(["--dns", item]) + for item in norm_as_list(cnt.get("dns_opt", None)): + podman_args.extend(["--dns-opt", item]) + for item in norm_as_list(cnt.get("dns_search", None)): + podman_args.extend(["--dns-search", item]) + env_file = cnt.get("env_file", []) + if is_str(env_file): + env_file = [env_file] for i in env_file: i = os.path.realpath(os.path.join(dirname, i)) - podman_args.extend(['--env-file', i]) - env = norm_as_list(cnt.get('environment', {})) + podman_args.extend(["--env-file", i]) + env = norm_as_list(cnt.get("environment", {})) for e in env: - podman_args.extend(['-e', e]) - tmpfs_ls = cnt.get('tmpfs', []) - if is_str(tmpfs_ls): tmpfs_ls = [tmpfs_ls] + podman_args.extend(["-e", e]) + tmpfs_ls = cnt.get("tmpfs", []) + if is_str(tmpfs_ls): + tmpfs_ls = [tmpfs_ls] for i in tmpfs_ls: - podman_args.extend(['--tmpfs', i]) - for volume in cnt.get('volumes', []): + podman_args.extend(["--tmpfs", i]) + for volume in cnt.get("volumes", []): podman_args.extend(get_mount_args(compose, cnt, volume)) assert_cnt_nets(compose, cnt) podman_args.extend(get_net_args(compose, cnt)) - logging = cnt.get('logging', None) + logging = cnt.get("logging", None) if logging is not None: podman_args.append(f'--log-driver={logging.get("driver", "k8s-file")}') - log_opts = logging.get('options') or {} - podman_args += [f'--log-opt={name}={value}' for name, value in log_opts.items()] - for secret in cnt.get('secrets', []): + log_opts = logging.get("options") or {} + podman_args += [f"--log-opt={name}={value}" for name, value in log_opts.items()] + for secret in cnt.get("secrets", []): podman_args.extend(get_secret_args(compose, cnt, secret)) - for i in cnt.get('extra_hosts', []): - podman_args.extend(['--add-host', i]) - for i in cnt.get('expose', []): - podman_args.extend(['--expose', i]) - if cnt.get('publishall', None): - podman_args.append('-P') - ports = cnt.get('ports', None) or [] + for i in cnt.get("extra_hosts", []): + podman_args.extend(["--add-host", i]) + for i in cnt.get("expose", []): + podman_args.extend(["--expose", i]) + if cnt.get("publishall", None): + podman_args.append("-P") + ports = cnt.get("ports", None) or [] if isinstance(ports, str): ports = [ports] for port in ports: @@ -730,69 +850,71 @@ def container_to_args(compose, cnt, detached=True): port = port_dict_to_str(port) elif not isinstance(port, str): raise TypeError("port should be either string or dict") - podman_args.extend(['-p', port]) + podman_args.extend(["-p", port]) - user = cnt.get('user', None) + user = cnt.get("user", None) if user is not None: - podman_args.extend(['-u', user]) - if cnt.get('working_dir', None) is not None: - podman_args.extend(['-w', cnt['working_dir']]) - if cnt.get('hostname', None): - podman_args.extend(['--hostname', cnt['hostname']]) - if cnt.get('shm_size', None): - podman_args.extend(['--shm-size', str(cnt['shm_size'])]) - if cnt.get('stdin_open', None): - podman_args.append('-i') - if cnt.get('stop_signal', None): - podman_args.extend(['--stop-signal', cnt['stop_signal']]) - for i in cnt.get('sysctls', []): - podman_args.extend(['--sysctl', i]) - if cnt.get('tty', None): - podman_args.append('--tty') - if cnt.get('privileged', None): - podman_args.append('--privileged') - pull_policy = cnt.get('pull_policy', None) - if pull_policy is not None and pull_policy!='build': - podman_args.extend(['--pull', pull_policy]) - if cnt.get('restart', None) is not None: - podman_args.extend(['--restart', cnt['restart']]) + podman_args.extend(["-u", user]) + if cnt.get("working_dir", None) is not None: + podman_args.extend(["-w", cnt["working_dir"]]) + if cnt.get("hostname", None): + podman_args.extend(["--hostname", cnt["hostname"]]) + if cnt.get("shm_size", None): + podman_args.extend(["--shm-size", str(cnt["shm_size"])]) + if cnt.get("stdin_open", None): + podman_args.append("-i") + if cnt.get("stop_signal", None): + podman_args.extend(["--stop-signal", cnt["stop_signal"]]) + for i in cnt.get("sysctls", []): + podman_args.extend(["--sysctl", i]) + if cnt.get("tty", None): + podman_args.append("--tty") + if cnt.get("privileged", None): + podman_args.append("--privileged") + pull_policy = cnt.get("pull_policy", None) + if pull_policy is not None and pull_policy != "build": + podman_args.extend(["--pull", pull_policy]) + if cnt.get("restart", None) is not None: + podman_args.extend(["--restart", cnt["restart"]]) container_to_ulimit_args(cnt, podman_args) container_to_res_args(cnt, podman_args) # currently podman shipped by fedora does not package this - if cnt.get('init', None): - podman_args.append('--init') - if cnt.get('init-path', None): - podman_args.extend(['--init-path', cnt['init-path']]) - entrypoint = cnt.get('entrypoint', None) + if cnt.get("init", None): + podman_args.append("--init") + if cnt.get("init-path", None): + podman_args.extend(["--init-path", cnt["init-path"]]) + entrypoint = cnt.get("entrypoint", None) if entrypoint is not None: if is_str(entrypoint): entrypoint = shlex.split(entrypoint) - podman_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 {} + healthcheck = cnt.get("healthcheck", None) or {} if not is_dict(healthcheck): raise ValueError("'healthcheck' must be an key-value mapping") - healthcheck_test = healthcheck.get('test', None) + healthcheck_test = healthcheck.get("test", None) if healthcheck_test: # If it's a string, it's equivalent to specifying CMD-SHELL if is_str(healthcheck_test): # podman does not add shell to handle command with whitespace - podman_args.extend(['--healthcheck-command', '/bin/sh -c '+cmd_quote(healthcheck_test)]) + podman_args.extend( + ["--healthcheck-command", "/bin/sh -c " + cmd_quote(healthcheck_test)] + ) elif is_list(healthcheck_test): healthcheck_test = healthcheck_test.copy() # If it's a list, first item is either NONE, CMD or CMD-SHELL. healthcheck_type = healthcheck_test.pop(0) - if healthcheck_type == 'NONE': + if healthcheck_type == "NONE": podman_args.append("--no-healthcheck") - elif healthcheck_type == 'CMD': - cmd_q = "' '".join([cmd_quote(i) for i in healthcheck_test]) - podman_args.extend(['--healthcheck-command', '/bin/sh -c '+cmd_q]) - elif healthcheck_type == 'CMD-SHELL': - if len(healthcheck_test)!=1: + elif healthcheck_type == "CMD": + cmd_q = "' '".join([cmd_quote(i) for i in healthcheck_test]) + podman_args.extend(["--healthcheck-command", "/bin/sh -c " + cmd_q]) + elif healthcheck_type == "CMD-SHELL": + if len(healthcheck_test) != 1: raise ValueError("'CMD_SHELL' takes a single string after it") cmd_q = cmd_quote(healthcheck_test[0]) - podman_args.extend(['--healthcheck-command', '/bin/sh -c '+cmd_q]) + podman_args.extend(["--healthcheck-command", "/bin/sh -c " + cmd_q]) else: raise ValueError( f"unknown healthcheck test type [{healthcheck_type}],\ @@ -802,19 +924,19 @@ def container_to_args(compose, cnt, detached=True): raise ValueError("'healthcheck.test' either a string or a list") # interval, timeout and start_period are specified as durations. - if 'interval' in healthcheck: - podman_args.extend(['--healthcheck-interval', healthcheck['interval']]) - if 'timeout' in healthcheck: - podman_args.extend(['--healthcheck-timeout', healthcheck['timeout']]) - if 'start_period' in healthcheck: - podman_args.extend(['--healthcheck-start-period', healthcheck['start_period']]) + if "interval" in healthcheck: + podman_args.extend(["--healthcheck-interval", healthcheck["interval"]]) + if "timeout" in healthcheck: + podman_args.extend(["--healthcheck-timeout", healthcheck["timeout"]]) + if "start_period" in healthcheck: + podman_args.extend(["--healthcheck-start-period", healthcheck["start_period"]]) # convert other parameters to string - if 'retries' in healthcheck: - podman_args.extend(['--healthcheck-retries', str(healthcheck['retries'])]) + if "retries" in healthcheck: + podman_args.extend(["--healthcheck-retries", str(healthcheck["retries"])]) - podman_args.append(cnt['image']) # command, ..etc. - command = cnt.get('command', None) + podman_args.append(cnt["image"]) # command, ..etc. + command = cnt.get("command", None) if command is not None: if is_str(command): podman_args.extend(shlex.split(command)) @@ -822,6 +944,7 @@ def container_to_args(compose, cnt, detached=True): podman_args.extend([str(i) for i in command]) return podman_args + def rec_deps(services, service_name, start_point=None): """ return all dependencies of service_name recursively @@ -843,6 +966,7 @@ def rec_deps(services, service_name, start_point=None): deps.update(new_deps) return deps + def flat_deps(services, with_extends=False): """ create dependencies "_deps" or update it recursively for all services @@ -853,38 +977,52 @@ def flat_deps(services, with_extends=False): if with_extends: ext = srv.get("extends", {}).get("service", None) if ext: - if ext != name: deps.add(ext) + if ext != name: + deps.add(ext) continue deps_ls = srv.get("depends_on", None) or [] - if is_str(deps_ls): deps_ls=[deps_ls] - elif is_dict(deps_ls): deps_ls=list(deps_ls.keys()) + if is_str(deps_ls): + deps_ls = [deps_ls] + elif is_dict(deps_ls): + deps_ls = list(deps_ls.keys()) deps.update(deps_ls) # parse link to get service name and remove alias links_ls = srv.get("links", None) or [] - if not is_list(links_ls): links_ls=[links_ls] - deps.update([(c.split(":")[0] if ":" in c else c) - for c in links_ls]) + if not is_list(links_ls): + links_ls = [links_ls] + deps.update([(c.split(":")[0] if ":" in c else c) for c in links_ls]) for name, srv in services.items(): rec_deps(services, name) + ################### # podman and compose classes ################### + class Podman: - def __init__(self, compose, podman_path='podman', dry_run=False): + 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='', cmd_args=None): + def output(self, podman_args, cmd="", cmd_args=None): cmd_args = cmd_args or [] xargs = self.compose.get_podman_args(cmd) if cmd else [] cmd_ls = [self.podman_path, *podman_args, cmd] + xargs + cmd_args log(cmd_ls) return subprocess.check_output(cmd_ls) - def run(self, podman_args, cmd='', cmd_args=None, wait=True, sleep=1, obj=None, log_formatter=None): + def run( + self, + podman_args, + cmd="", + cmd_args=None, + wait=True, + sleep=1, + obj=None, + log_formatter=None, + ): if obj is not None: obj.exit_code = None cmd_args = list(map(str, cmd_args or [])) @@ -896,11 +1034,15 @@ class Podman: # 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) if log_formatter is not None: # Pipe podman process output through log_formatter (which can add colored prefix) - p = subprocess.Popen(cmd_ls, stdout=subprocess.PIPE) # pylint: disable=consider-using-with - _ = subprocess.Popen(log_formatter, stdin=p.stdout) # pylint: disable=consider-using-with - p.stdout.close() # Allow p_process to receive a SIGPIPE if logging process exits. + p = subprocess.Popen( + cmd_ls, stdout=subprocess.PIPE + ) # pylint: disable=consider-using-with + _ = subprocess.Popen( + log_formatter, stdin=p.stdout + ) # pylint: disable=consider-using-with + p.stdout.close() # Allow p_process to receive a SIGPIPE if logging process exits. else: - p = subprocess.Popen(cmd_ls) # pylint: disable=consider-using-with + p = subprocess.Popen(cmd_ls) # pylint: disable=consider-using-with if wait: exit_code = p.wait() @@ -915,42 +1057,54 @@ class Podman: def volume_ls(self, proj=None): if not proj: proj = self.compose.project_name - output = self.output([], "volume", [ - "ls", "--noheading", "--filter", f"label=io.podman.compose.project={proj}", - "--format", "{{.Name}}", - ]).decode('utf-8') + output = self.output( + [], + "volume", + [ + "ls", + "--noheading", + "--filter", + f"label=io.podman.compose.project={proj}", + "--format", + "{{.Name}}", + ], + ).decode("utf-8") volumes = output.splitlines() return volumes -def normalize_service(service, sub_dir=''): + +def normalize_service(service, sub_dir=""): # make `build.context` relative to sub_dir # TODO: should we make volume and secret relative too? - if sub_dir and 'build' in service: - build = service['build'] - context = build if is_str(build) else build.get('context', None) - context = context or '' + if sub_dir and "build" in service: + build = service["build"] + context = build if is_str(build) else build.get("context", None) + context = context or "" if context or sub_dir: - if context.startswith('./'): + if context.startswith("./"): context = context[2:] if sub_dir: context = os.path.join(sub_dir, context) - context = context.rstrip('/') + context = context.rstrip("/") if not context: - context = '.' + context = "." if is_str(build): - service['build'] = context + service["build"] = context else: - service['build']['context'] = context + service["build"]["context"] = context for key in ("env_file", "security_opt", "volumes"): - if key not in service: continue - if is_str(service[key]): service[key]=[service[key]] + if key not in service: + continue + if is_str(service[key]): + service[key] = [service[key]] if "security_opt" in service: sec_ls = service["security_opt"] for ix, item in enumerate(sec_ls): - if item in ('seccomp:unconfined', 'apparmor:unconfined'): + if item in ("seccomp:unconfined", "apparmor:unconfined"): sec_ls[ix] = item.replace(":", "=") for key in ("environment", "labels"): - if key not in service: continue + if key not in service: + continue service[key] = norm_as_dict(service[key]) if "extends" in service: extends = service["extends"] @@ -959,6 +1113,7 @@ def normalize_service(service, sub_dir=''): service["extends"] = extends return service + def normalize(compose): """ convert compose dict of some keys from string or dicts into arrays @@ -968,28 +1123,38 @@ def normalize(compose): normalize_service(service) return compose + def rec_merge_one(target, source): """ update target from source recursively """ done = set() for key, value in source.items(): - if key in target: continue - target[key]=value + if key in target: + continue + target[key] = value done.add(key) for key, value in target.items(): - if key in done: continue - if key not in source: continue + if key in done: + continue + if key not in source: + continue value2 = source[key] if isinstance(value2, type(value)): value_type = type(value) value2_type = type(value2) - raise ValueError(f"can't merge value of {key} of type {value_type} and {value2_type}") + raise ValueError( + f"can't merge value of {key} of type {value_type} and {value2_type}" + ) if is_list(value2): - if key == 'volumes': + if key == "volumes": # clean duplicate mount targets - pts = { v.split(':', 1)[1] for v in value2 if ":" in v } - del_ls = [ ix for (ix, v) in enumerate(value) if ":" in v and v.split(':', 1)[1] in pts ] + pts = {v.split(":", 1)[1] for v in value2 if ":" in v} + del_ls = [ + ix + for (ix, v) in enumerate(value) + if ":" in v and v.split(":", 1)[1] in pts + ] for ix in reversed(del_ls): del value[ix] value.extend(value2) @@ -998,9 +1163,10 @@ def rec_merge_one(target, source): elif is_dict(value2): rec_merge_one(value, value2) else: - target[key]=value2 + target[key] = value2 return target + def rec_merge(target, *sources): """ update target recursively from sources @@ -1009,18 +1175,21 @@ def rec_merge(target, *sources): ret = rec_merge_one(target, source) return ret + def resolve_extends(services, service_names, environ): for name in service_names: service = services[name] ext = service.get("extends", {}) - if is_str(ext): ext = {"service": ext} + if is_str(ext): + ext = {"service": ext} from_service_name = ext.get("service", None) - if not from_service_name: continue + if not from_service_name: + continue filename = ext.get("file", None) if filename: - if filename.startswith('./'): + if filename.startswith("./"): filename = filename[2:] - with open(filename, 'r', encoding="utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: content = yaml.safe_load(f) or {} if "services" in content: content = content["services"] @@ -1038,11 +1207,13 @@ def resolve_extends(services, service_names, environ): new_service = rec_merge({}, from_service, service) services[name] = new_service + def dotenv_to_dict(dotenv_path): if not os.path.isfile(dotenv_path): return {} return dotenv_values(dotenv_path) + COMPOSE_DEFAULT_LS = [ "compose.yaml", "compose.yml", @@ -1060,6 +1231,7 @@ COMPOSE_DEFAULT_LS = [ "container-compose.override.yaml", ] + class PodmanCompose: def __init__(self): self.podman = None @@ -1082,8 +1254,14 @@ class PodmanCompose: self.all_services = set() self.prefer_volume_over_mount = True self.merged_yaml = None - self.yaml_hash = '' - self.console_colors = ["\x1B[1;32m", "\x1B[1;33m", "\x1B[1;34m", "\x1B[1;35m", "\x1B[1;36m"] + self.yaml_hash = "" + self.console_colors = [ + "\x1B[1;32m", + "\x1B[1;33m", + "\x1B[1;34m", + "\x1B[1;35m", + "\x1B[1;36m", + ] def assert_services(self, services): if is_str(services): @@ -1099,17 +1277,17 @@ class PodmanCompose: xargs = [] for args in self.global_args.podman_args: xargs.extend(shlex.split(args)) - cmd_norm = cmd if cmd != 'create' else 'run' + cmd_norm = cmd if cmd != "create" else "run" cmd_args = self.global_args.__dict__.get(f"podman_{cmd_norm}_args", None) or [] for args in cmd_args: xargs.extend(shlex.split(args)) return xargs def run(self): - log("podman-compose version: "+__version__) + log("podman-compose version: " + __version__) args = self._parse_args() podman_path = args.podman_path - if podman_path != 'podman': + 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: @@ -1121,14 +1299,17 @@ class PodmanCompose: if not args.dry_run: # just to make sure podman is running try: - self.podman_version = self.podman.output(["--version"], '', []).decode('utf-8').strip() or "" + self.podman_version = ( + self.podman.output(["--version"], "", []).decode("utf-8").strip() + or "" + ) self.podman_version = (self.podman_version.split() or [""])[-1] except subprocess.CalledProcessError: self.podman_version = None if not self.podman_version: log("it seems that you do not have `podman` installed") sys.exit(1) - log("using podman version: "+self.podman_version) + log("using podman version: " + self.podman_version) cmd_name = args.command if cmd_name != "version": self._parse_compose_file() @@ -1148,10 +1329,12 @@ class PodmanCompose: args.file = list(filter(os.path.exists, default_ls)) files = args.file if not files: - log("no compose.yaml, docker-compose.yml or container-compose.yml file found, pass files with -f") + log( + "no compose.yaml, docker-compose.yml or container-compose.yml file found, pass files with -f" + ) sys.exit(-1) ex = map(os.path.exists, files) - missing = [ fn0 for ex0, fn0 in zip(ex, files) if not ex0 ] + missing = [fn0 for ex0, fn0 in zip(ex, files) if not ex0] if missing: log("missing files: ", missing) sys.exit(1) @@ -1172,64 +1355,80 @@ class PodmanCompose: if not project_name: # More strict then actually needed for simplicity: podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]* - project_name = os.environ.get("COMPOSE_PROJECT_NAME", None) or dir_basename.lower() - project_name = norm_re.sub('', project_name) + project_name = ( + os.environ.get("COMPOSE_PROJECT_NAME", None) or dir_basename.lower() + ) + project_name = norm_re.sub("", project_name) if not project_name: raise RuntimeError(f"Project name [{dir_basename}] normalized to empty") self.project_name = project_name - dotenv_path = os.path.join(dirname, ".env") self.environ = dict(os.environ) dotenv_dict = dotenv_to_dict(dotenv_path) self.environ.update(dotenv_dict) - os.environ.update({ key: value for key, value in dotenv_dict.items() if key.startswith('PODMAN_')}) + os.environ.update( + { + key: value + for key, value in dotenv_dict.items() + if key.startswith("PODMAN_") + } + ) # see: https://docs.docker.com/compose/reference/envvars/ # see: https://docs.docker.com/compose/env-file/ - self.environ.update({ - "COMPOSE_FILE": os.path.basename(filename), - "COMPOSE_PROJECT_NAME": self.project_name, - "COMPOSE_PATH_SEPARATOR": pathsep, - }) + self.environ.update( + { + "COMPOSE_FILE": os.path.basename(filename), + "COMPOSE_PROJECT_NAME": self.project_name, + "COMPOSE_PATH_SEPARATOR": pathsep, + } + ) compose = {} for filename in files: - with open(filename, 'r', encoding="utf-8") as f: + with open(filename, "r", encoding="utf-8") as f: content = yaml.safe_load(f) - #log(filename, json.dumps(content, indent = 2)) + # log(filename, json.dumps(content, indent = 2)) if not isinstance(content, dict): - sys.stderr.write("Compose file does not contain a top level object: %s\n"%filename) + sys.stderr.write( + "Compose file does not contain a top level object: %s\n" + % filename + ) sys.exit(1) content = normalize(content) - #log(filename, json.dumps(content, indent = 2)) + # log(filename, json.dumps(content, indent = 2)) content = rec_subs(content, self.environ) rec_merge(compose, content) self.merged_yaml = yaml.safe_dump(compose) - merged_json_b = json.dumps(compose, separators=(',',':')).encode('utf-8') + merged_json_b = json.dumps(compose, separators=(",", ":")).encode("utf-8") self.yaml_hash = hashlib.sha256(merged_json_b).hexdigest() - compose['_dirname'] = dirname + compose["_dirname"] = dirname # debug mode - if len(files)>1: - log(" ** merged:\n", json.dumps(compose, indent = 2)) + if len(files) > 1: + log(" ** merged:\n", json.dumps(compose, indent=2)) # ver = compose.get('version', None) - services = compose.get('services', None) + services = compose.get("services", None) if services is None: services = {} log("WARNING: No services defined") # NOTE: maybe add "extends.service" to _deps at this stage flat_deps(services, with_extends=True) - service_names = sorted([ (len(srv["_deps"]), name) for name, srv in services.items() ]) - service_names = [ name for _, name in service_names] + service_names = sorted( + [(len(srv["_deps"]), name) for name, srv in services.items()] + ) + service_names = [name for _, name in service_names] resolve_extends(services, service_names, self.environ) flat_deps(services) - service_names = sorted([ (len(srv["_deps"]), name) for name, srv in services.items() ]) - service_names = [ name for _, name in service_names] + service_names = sorted( + [(len(srv["_deps"]), name) for name, srv in services.items()] + ) + service_names = [name for _, name in service_names] nets = compose.get("networks", None) or {} if not nets: nets["default"] = None self.networks = nets - if len(self.networks)==1: + if len(self.networks) == 1: self.default_net = list(nets.keys())[0] elif "default" in nets: self.default_net = "default" @@ -1239,34 +1438,36 @@ class PodmanCompose: allnets = set() for name, srv in services.items(): srv_nets = srv.get("networks", None) or default_net - srv_nets = list(srv_nets.keys()) if is_dict(srv_nets) else norm_as_list(srv_nets) + srv_nets = ( + list(srv_nets.keys()) if is_dict(srv_nets) else norm_as_list(srv_nets) + ) allnets.update(srv_nets) given_nets = set(nets.keys()) missing_nets = given_nets - allnets if len(missing_nets): - missing_nets_str= ",".join(missing_nets) + missing_nets_str = ",".join(missing_nets) raise RuntimeError(f"missing networks: {missing_nets_str}") # volumes: [...] - self.vols = compose.get('volumes', {}) + self.vols = compose.get("volumes", {}) podman_compose_labels = [ - "io.podman.compose.config-hash="+ self.yaml_hash, + "io.podman.compose.config-hash=" + self.yaml_hash, "io.podman.compose.project=" + project_name, "io.podman.compose.version=0.0.1", "com.docker.compose.project=" + project_name, "com.docker.compose.project.working_dir=" + dirname, - "com.docker.compose.project.config_files=" + ','.join(relative_files), + "com.docker.compose.project.config_files=" + ",".join(relative_files), ] # other top-levels: # networks: {driver: ...} # configs: {...} - self.declared_secrets = compose.get('secrets', {}) + self.declared_secrets = compose.get("secrets", {}) given_containers = [] container_names_by_service = {} self.services = services for service_name, service_desc in services.items(): - replicas = try_int(service_desc.get('deploy', {}).get('replicas', '1')) + replicas = try_int(service_desc.get("deploy", {}).get("replicas", "1")) container_names_by_service[service_name] = [] - for num in range(1, replicas+1): + for num in range(1, replicas + 1): name0 = f"{project_name}_{service_name}_{num}" if num == 1: name = service_desc.get("container_name", name0) @@ -1274,84 +1475,122 @@ class PodmanCompose: name = name0 container_names_by_service[service_name].append(name) # log(service_name,service_desc) - cnt = dict(name=name, num=num, - service_name=service_name, **service_desc) - if 'image' not in cnt: - cnt['image'] = f"{project_name}_{service_name}" - labels = norm_as_list(cnt.get('labels', None)) + cnt = dict( + name=name, num=num, service_name=service_name, **service_desc + ) + if "image" not in cnt: + cnt["image"] = f"{project_name}_{service_name}" + labels = norm_as_list(cnt.get("labels", None)) cnt["ports"] = norm_ports(cnt.get("ports", None)) labels.extend(podman_compose_labels) - labels.extend([ - f"com.docker.compose.container-number={num}", - "com.docker.compose.service=" + service_name, - ]) - cnt['labels'] = labels - cnt['_service'] = service_name - cnt['_project'] = project_name + labels.extend( + [ + f"com.docker.compose.container-number={num}", + "com.docker.compose.service=" + service_name, + ] + ) + cnt["labels"] = labels + cnt["_service"] = service_name + cnt["_project"] = project_name given_containers.append(cnt) volumes = cnt.get("volumes", None) or [] for volume in volumes: mnt_dict = get_mnt_dict(self, cnt, volume) - if mnt_dict.get("type", None)=="volume" and mnt_dict["source"] and mnt_dict["source"] not in self.vols: + if ( + mnt_dict.get("type", None) == "volume" + and mnt_dict["source"] + and mnt_dict["source"] not in self.vols + ): vol_name = mnt_dict["source"] - raise RuntimeError(f"volume [{vol_name}] not defined in top level") + raise RuntimeError( + f"volume [{vol_name}] not defined in top level" + ) self.container_names_by_service = container_names_by_service self.all_services = set(container_names_by_service.keys()) container_by_name = {c["name"]: c for c in given_containers} - #log("deps:", [(c["name"], c["_deps"]) for c in given_containers]) + # log("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', None) or [])) - #log("sorted:", [c["name"] for c in given_containers]) + given_containers.sort(key=lambda c: len(c.get("_deps", None) or [])) + # log("sorted:", [c["name"] for c in given_containers]) pods, containers = tr_identity(project_name, given_containers) self.pods = pods self.containers = containers self.container_by_name = {c["name"]: c for c in containers} def _parse_args(self): - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter - ) + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) self._init_global_parser(parser) - subparsers = parser.add_subparsers(title='command', dest='command') - subparser = subparsers.add_parser('help', help='show help') + subparsers = parser.add_subparsers(title="command", dest="command") + subparser = subparsers.add_parser("help", help="show help") for cmd_name, cmd in self.commands.items(): - subparser = subparsers.add_parser(cmd_name, help=cmd._cmd_desc) # pylint: disable=protected-access + subparser = subparsers.add_parser( + cmd_name, help=cmd._cmd_desc + ) # pylint: disable=protected-access for cmd_parser in cmd._parse_args: # pylint: disable=protected-access cmd_parser(subparser) self.global_args = parser.parse_args() if self.global_args.version: self.global_args.command = "version" - if not self.global_args.command or self.global_args.command=='help': + if not self.global_args.command or self.global_args.command == "help": parser.print_help() sys.exit(-1) return self.global_args @staticmethod def _init_global_parser(parser): - parser.add_argument("-v", "--version", - help="show version", action='store_true') - parser.add_argument("-f", "--file", - help="Specify an alternate compose file (default: docker-compose.yml)", - metavar='file', action='append', default=[]) - 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("--podman-args", - help="custom global arguments to be passed to `podman`", - metavar='args', action='append', default=[]) + parser.add_argument("-v", "--version", help="show version", action="store_true") + parser.add_argument( + "-f", + "--file", + help="Specify an alternate compose file (default: docker-compose.yml)", + metavar="file", + action="append", + default=[], + ) + 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( + "--podman-args", + help="custom global arguments to be passed to `podman`", + metavar="args", + action="append", + default=[], + ) for podman_cmd in PODMAN_CMDS: - parser.add_argument(f"--podman-{podman_cmd}-args", + parser.add_argument( + f"--podman-{podman_cmd}-args", help=f"custom arguments to be passed to `podman {podman_cmd}`", - metavar='args', action='append', default=[]) - 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') + metavar="args", + action="append", + default=[], + ) + 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", + ) + podman_compose = PodmanCompose() @@ -1359,14 +1598,17 @@ podman_compose = PodmanCompose() # decorators to add commands and parse options ################### -class cmd_run: # pylint: disable=invalid-name,too-few-public-methods + +class cmd_run: # pylint: disable=invalid-name,too-few-public-methods 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 @@ -1374,7 +1616,8 @@ class cmd_run: # pylint: disable=invalid-name,too-few-public-methods self.compose.commands[self.cmd_name] = wrapped return wrapped -class cmd_parse: # pylint: disable=invalid-name,too-few-public-methods + +class cmd_parse: # pylint: disable=invalid-name,too-few-public-methods def __init__(self, compose, cmd_names): self.compose = compose self.cmd_names = cmd_names if is_list(cmd_names) else [cmd_names] @@ -1382,26 +1625,30 @@ class cmd_parse: # pylint: disable=invalid-name,too-few-public-methods 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, 'version', 'show version') + +@cmd_run(podman_compose, "version", "show version") def compose_version(compose, args): - if getattr(args, 'short', False): + if getattr(args, "short", False): print(__version__) return - if getattr(args, 'format', 'pretty') == 'json': + if getattr(args, "format", "pretty") == "json": res = {"version": __version__} print(json.dumps(res)) return print("podman-composer version", __version__) compose.podman.run(["--version"], "", [], sleep=0) + def is_local(container: dict) -> bool: """Test if a container is local, i.e. if it is * prefixed with localhost/ @@ -1413,6 +1660,7 @@ def is_local(container: dict) -> bool: else container["image"].startswith("localhost/") ) + @cmd_run(podman_compose, "pull", "pull stack images") def compose_pull(compose, args): img_containers = [cnt for cnt in compose.containers if "image" in cnt] @@ -1423,52 +1671,75 @@ def compose_pull(compose, args): for image in images: compose.podman.run([], "pull", [image], sleep=0) -@cmd_run(podman_compose, 'push', 'push stack images') + +@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 + if "build" not in cnt: + continue + if services and cnt["_service"] not in services: + continue compose.podman.run([], "push", [cnt["image"]], sleep=0) + def build_one(compose, args, cnt): - if 'build' not in cnt: return - if getattr(args, 'if_not_exists', None): - try: img_id = compose.podman.output([], 'inspect', ['-t', 'image', '-f', '{{.Id}}', cnt["image"]]) - except subprocess.CalledProcessError: img_id = None - if img_id: return - build_desc = cnt['build'] - if not hasattr(build_desc, 'items'): + if "build" not in cnt: + return + if getattr(args, "if_not_exists", None): + try: + img_id = compose.podman.output( + [], "inspect", ["-t", "image", "-f", "{{.Id}}", cnt["image"]] + ) + except subprocess.CalledProcessError: + img_id = None + if img_id: + return + build_desc = cnt["build"] + if not hasattr(build_desc, "items"): build_desc = dict(context=build_desc) - ctx = build_desc.get('context', '.') + ctx = build_desc.get("context", ".") dockerfile = build_desc.get("dockerfile", None) if dockerfile: dockerfile = os.path.join(ctx, dockerfile) else: dockerfile_alts = [ - 'Containerfile', 'ContainerFile', 'containerfile', - 'Dockerfile', 'DockerFile','dockerfile', + "Containerfile", + "ContainerFile", + "containerfile", + "Dockerfile", + "DockerFile", + "dockerfile", ] for dockerfile in dockerfile_alts: dockerfile = os.path.join(ctx, dockerfile) - if os.path.exists(dockerfile): break + if os.path.exists(dockerfile): + break if not os.path.exists(dockerfile): - raise OSError("Dockerfile not found in "+ctx) + raise OSError("Dockerfile not found in " + ctx) build_args = ["-t", cnt["image"], "-f", dockerfile] if "target" in build_desc: build_args.extend(["--target", build_desc["target"]]) container_to_ulimit_args(cnt, build_args) - if getattr(args, 'no_cache', None): + if getattr(args, "no_cache", None): build_args.append("--no-cache") - if getattr(args, 'pull_always', None): build_args.append("--pull-always") - elif getattr(args, 'pull', None): build_args.append("--pull") - args_list = norm_as_list(build_desc.get('args', {})) + if getattr(args, "pull_always", None): + build_args.append("--pull-always") + elif getattr(args, "pull", None): + build_args.append("--pull") + args_list = norm_as_list(build_desc.get("args", {})) for build_arg in args_list + args.build_arg: - build_args.extend(("--build-arg", build_arg,)) + build_args.extend( + ( + "--build-arg", + build_arg, + ) + ) build_args.append(ctx) compose.podman.run([], "build", build_args, sleep=0) -@cmd_run(podman_compose, 'build', 'build stack images') + +@cmd_run(podman_compose, "build", "build stack images") def compose_build(compose, args): if args.services: container_names_by_service = compose.container_names_by_service @@ -1480,89 +1751,102 @@ def compose_build(compose, args): for cnt in compose.containers: build_one(compose, args, cnt) -def create_pods(compose, args): # pylint: disable=unused-argument + +def create_pods(compose, args): # pylint: disable=unused-argument for pod in compose.pods: podman_args = [ "create", - "--name=" + pod['name'], + "--name=" + pod["name"], "--infra=false", "--share=", ] - #if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"): + # if compose.podman_version and not strverscmp_lt(compose.podman_version, "3.4.0"): # podman_args.append("--infra-name={}_infra".format(pod["name"])) ports = pod.get("ports", None) or [] if isinstance(ports, str): ports = [ports] for i in ports: - podman_args.extend(['-p', str(i)]) + podman_args.extend(["-p", str(i)]) compose.podman.run([], "pod", podman_args) + def get_excluded(compose, args): excluded = set() if args.services: excluded = set(compose.services) for service in args.services: - excluded-= compose.services[service]['_deps'] + excluded -= compose.services[service]["_deps"] excluded.discard(service) log("** excluding: ", excluded) return excluded -@cmd_run(podman_compose, 'up', 'Create and start the entire stack or some of its services') + +@cmd_run( + podman_compose, "up", "Create and start the entire stack or some of its services" +) def compose_up(compose, args): excluded = get_excluded(compose, args) if not args.no_build: # `podman build` does not cache, so don't always build - build_args = argparse.Namespace( - if_not_exists=(not args.build), - **args.__dict__) - compose.commands['build'](compose, build_args) + build_args = argparse.Namespace(if_not_exists=(not args.build), **args.__dict__) + compose.commands["build"](compose, build_args) # TODO: implement check hash label for change if args.force_recreate: down_args = argparse.Namespace(**dict(args.__dict__, volumes=False)) - compose.commands['down'](compose, down_args) + compose.commands["down"](compose, down_args) # args.no_recreate disables check for changes (which is not implemented) - podman_command = 'run' if args.detach and not args.no_start else 'create' + podman_command = "run" if args.detach and not args.no_start else "create" create_pods(compose, args) for cnt in compose.containers: if cnt["_service"] in excluded: - log("** skipping: ", cnt['name']) + log("** skipping: ", cnt["name"]) continue podman_args = container_to_args(compose, cnt, detached=args.detach) subproc = compose.podman.run([], podman_command, podman_args) - if podman_command == 'run' and subproc and subproc.returncode: - compose.podman.run([], 'start', [cnt['name']]) + if podman_command == "run" and subproc and subproc.returncode: + compose.podman.run([], "start", [cnt["name"]]) if args.no_start or args.detach or args.dry_run: return # TODO: handle already existing # TODO: if error creating do not enter loop # TODO: colors if sys.stdout.isatty() - exit_code_from = args.__dict__.get('exit_code_from', None) + exit_code_from = args.__dict__.get("exit_code_from", None) if exit_code_from: - args.abort_on_container_exit=True + args.abort_on_container_exit = True threads = [] max_service_length = 0 for cnt in compose.containers: curr_length = len(cnt["_service"]) - max_service_length = curr_length if curr_length > max_service_length else max_service_length + max_service_length = ( + curr_length if curr_length > max_service_length else max_service_length + ) for i, cnt in enumerate(compose.containers): # Add colored service prefix to output by piping output through sed color_idx = i % len(compose.console_colors) color = compose.console_colors[color_idx] - space_suffix=' ' * (max_service_length - len(cnt["_service"]) + 1) - log_formatter = 's/^/{}[{}]{}|\x1B[0m\\ /;'.format(color, cnt["_service"], space_suffix) + space_suffix = " " * (max_service_length - len(cnt["_service"]) + 1) + log_formatter = "s/^/{}[{}]{}|\x1B[0m\\ /;".format( + color, cnt["_service"], space_suffix + ) log_formatter = ["sed", "-e", log_formatter] if cnt["_service"] in excluded: - log("** skipping: ", cnt['name']) + log("** skipping: ", cnt["name"]) continue # TODO: remove sleep from podman.run - obj = compose if exit_code_from == cnt['_service'] else None - thread = Thread(target=compose.podman.run, args=[[], 'start', ['-a', cnt['name']]], kwargs={"obj":obj, "log_formatter": log_formatter}, daemon=True, name=cnt['name']) + obj = compose if exit_code_from == cnt["_service"] else None + thread = Thread( + target=compose.podman.run, + args=[[], "start", ["-a", cnt["name"]]], + kwargs={"obj": obj, "log_formatter": log_formatter}, + daemon=True, + name=cnt["name"], + ) thread.start() threads.append(thread) time.sleep(1) @@ -1574,47 +1858,57 @@ def compose_up(compose, args): threads.remove(thread) if args.abort_on_container_exit: time.sleep(1) - exit_code = compose.exit_code if compose.exit_code is not None else -1 + exit_code = ( + compose.exit_code if compose.exit_code is not None else -1 + ) sys.exit(exit_code) + def get_volume_names(compose, cnt): proj_name = compose.project_name basedir = compose.dirname - srv_name = cnt['_service'] + srv_name = cnt["_service"] ls = [] - for volume in cnt.get('volumes', []): - if is_str(volume): volume = parse_short_mount(volume, basedir) + for volume in cnt.get("volumes", []): + if is_str(volume): + volume = parse_short_mount(volume, basedir) volume = fix_mount_dict(compose, volume, proj_name, srv_name) mount_type = volume["type"] - if mount_type!='volume': continue + if mount_type != "volume": + continue volume_name = (volume.get("_vol", None) or {}).get("name", None) ls.append(volume_name) return ls -@cmd_run(podman_compose, 'down', 'tear down entire stack') + +@cmd_run(podman_compose, "down", "tear down entire stack") def compose_down(compose, args): excluded = get_excluded(compose, args) - podman_args=[] - timeout=getattr(args, 'timeout', None) + podman_args = [] + timeout = getattr(args, "timeout", None) if timeout is None: timeout = 1 - podman_args.extend(['-t', str(timeout)]) + podman_args.extend(["-t", str(timeout)]) containers = list(reversed(compose.containers)) for cnt in containers: - if cnt["_service"] in excluded: continue + if cnt["_service"] in excluded: + continue compose.podman.run([], "stop", [*podman_args, cnt["name"]], sleep=0) for cnt in containers: - if cnt["_service"] in excluded: continue + if cnt["_service"] in excluded: + continue compose.podman.run([], "rm", [cnt["name"]], sleep=0) if args.volumes: vol_names_to_keep = set() for cnt in containers: - if cnt["_service"] not in excluded: continue + if cnt["_service"] not in excluded: + continue vol_names_to_keep.update(get_volume_names(compose, cnt)) log("keep", vol_names_to_keep) for volume_name in compose.podman.volume_ls(): - if volume_name in vol_names_to_keep: continue + if volume_name in vol_names_to_keep: + continue compose.podman.run([], "volume", ["rm", volume_name]) if excluded: @@ -1622,84 +1916,122 @@ def compose_down(compose, args): for pod in compose.pods: compose.podman.run([], "pod", ["rm", pod["name"]], sleep=0) -@cmd_run(podman_compose, 'ps', 'show status of containers') + +@cmd_run(podman_compose, "ps", "show status of containers") def compose_ps(compose, args): proj_name = compose.project_name if args.quiet is True: - compose.podman.run([], "ps", ["-a", "--format", "{{.ID}}", "--filter", f"label=io.podman.compose.project={proj_name}"]) + compose.podman.run( + [], + "ps", + [ + "-a", + "--format", + "{{.ID}}", + "--filter", + f"label=io.podman.compose.project={proj_name}", + ], + ) else: - compose.podman.run([], "ps", ["-a", "--filter", f"label=io.podman.compose.project={proj_name}"]) + compose.podman.run( + [], "ps", ["-a", "--filter", f"label=io.podman.compose.project={proj_name}"] + ) -@cmd_run(podman_compose, 'run', 'create a container similar to a service to run a one-off command') + +@cmd_run( + podman_compose, + "run", + "create a container similar to a service to run a one-off command", +) def compose_run(compose, args): create_pods(compose, args) compose.assert_services(args.service) - container_names=compose.container_names_by_service[args.service] - container_name=container_names[0] + container_names = compose.container_names_by_service[args.service] + container_name = container_names[0] cnt = dict(compose.container_by_name[container_name]) deps = cnt["_deps"] if not args.no_deps: - up_args = argparse.Namespace(**dict(args.__dict__, - detach=True, services=deps, - # defaults - no_build=False, build=None, force_recreate=False, no_start=False, no_cache=False, build_arg=[], - ) + up_args = argparse.Namespace( + **dict( + args.__dict__, + detach=True, + services=deps, + # defaults + no_build=False, + build=None, + force_recreate=False, + no_start=False, + no_cache=False, + build_arg=[], + ) ) - compose.commands['up'](compose, up_args) + compose.commands["up"](compose, up_args) # adjust one-off container options - name0 = "{}_{}_tmp{}".format(compose.project_name, args.service, random.randrange(0, 65536)) + name0 = "{}_{}_tmp{}".format( + compose.project_name, args.service, random.randrange(0, 65536) + ) cnt["name"] = args.name or name0 - if args.entrypoint: cnt["entrypoint"] = args.entrypoint - if args.user: cnt["user"] = args.user - if args.workdir: cnt["working_dir"] = args.workdir - env = dict(cnt.get('environment', {})) + if args.entrypoint: + cnt["entrypoint"] = args.entrypoint + if args.user: + cnt["user"] = args.user + if args.workdir: + cnt["working_dir"] = args.workdir + env = dict(cnt.get("environment", {})) if args.env: - additional_env_vars = dict(map(lambda each: each.split('='), args.env)) + additional_env_vars = dict(map(lambda each: each.split("="), args.env)) env.update(additional_env_vars) - cnt['environment'] = env + cnt["environment"] = env if not args.service_ports: for k in ("expose", "publishall", "ports"): - try: del cnt[k] - except KeyError: pass + try: + del cnt[k] + except KeyError: + pass if args.volume: # TODO: handle volumes pass - cnt['tty'] = not args.T + cnt["tty"] = not args.T if args.cnt_command is not None and len(args.cnt_command) > 0: - cnt['command']=args.cnt_command + cnt["command"] = args.cnt_command # can't restart and --rm - if args.rm and 'restart' in cnt: - del cnt['restart'] + if args.rm and "restart" in cnt: + del cnt["restart"] # run podman podman_args = container_to_args(compose, cnt, args.detach) if not args.detach: - podman_args.insert(1, '-i') + podman_args.insert(1, "-i") if args.rm: - podman_args.insert(1, '--rm') - p = compose.podman.run([], 'run', podman_args, sleep=0) + podman_args.insert(1, "--rm") + p = compose.podman.run([], "run", podman_args, sleep=0) sys.exit(p.returncode) -@cmd_run(podman_compose, 'exec', 'execute a command in a running container') + +@cmd_run(podman_compose, "exec", "execute a command in a running container") def compose_exec(compose, args): compose.assert_services(args.service) - container_names=compose.container_names_by_service[args.service] - container_name=container_names[args.index - 1] + container_names = compose.container_names_by_service[args.service] + container_name = container_names[args.index - 1] cnt = compose.container_by_name[container_name] - podman_args = ['--interactive'] - if args.privileged: podman_args += ['--privileged'] - if args.user: podman_args += ['--user', args.user] - if args.workdir: podman_args += ['--workdir', args.workdir] - if not args.T: podman_args += ['--tty'] - env = dict(cnt.get('environment', {})) + podman_args = ["--interactive"] + if args.privileged: + podman_args += ["--privileged"] + if args.user: + podman_args += ["--user", args.user] + if args.workdir: + podman_args += ["--workdir", args.workdir] + if not args.T: + podman_args += ["--tty"] + env = dict(cnt.get("environment", {})) if args.env: - additional_env_vars = dict(map(lambda each: each.split('='), args.env)) + additional_env_vars = dict(map(lambda each: each.split("="), args.env)) env.update(additional_env_vars) for name, value in env.items(): - podman_args += ['--env', "%s=%s" % (name, value)] + podman_args += ["--env", "%s=%s" % (name, value)] podman_args += [container_name] if args.cnt_command is not None and len(args.cnt_command) > 0: podman_args += args.cnt_command - p = compose.podman.run([], 'exec', podman_args, sleep=0) + p = compose.podman.run([], "exec", podman_args, sleep=0) sys.exit(p.returncode) @@ -1714,28 +2046,32 @@ def transfer_service_status(compose, args, action): if service not in container_names_by_service: raise ValueError("unknown service: " + service) targets.extend(container_names_by_service[service]) - if action in ['stop', 'restart']: + if action in ["stop", "restart"]: targets = list(reversed(targets)) - podman_args=[] - timeout=getattr(args, 'timeout', None) + podman_args = [] + timeout = getattr(args, "timeout", None) if timeout is not None: - podman_args.extend(['-t', str(timeout)]) + podman_args.extend(["-t", str(timeout)]) for target in targets: - compose.podman.run([], action, podman_args+[target], sleep=0) + compose.podman.run([], action, podman_args + [target], sleep=0) -@cmd_run(podman_compose, 'start', 'start specific services') + +@cmd_run(podman_compose, "start", "start specific services") def compose_start(compose, args): - transfer_service_status(compose, args, 'start') + transfer_service_status(compose, args, "start") -@cmd_run(podman_compose, 'stop', 'stop specific services') + +@cmd_run(podman_compose, "stop", "stop specific services") def compose_stop(compose, args): - transfer_service_status(compose, args, 'stop') + transfer_service_status(compose, args, "stop") -@cmd_run(podman_compose, 'restart', 'restart specific services') + +@cmd_run(podman_compose, "restart", "restart specific services") def compose_restart(compose, args): - transfer_service_status(compose, args, 'restart') + transfer_service_status(compose, args, "restart") -@cmd_run(podman_compose, 'logs', 'show logs from services') + +@cmd_run(podman_compose, "logs", "show logs from services") def compose_logs(compose, args): container_names_by_service = compose.container_names_by_service if not args.services and not args.latest: @@ -1746,198 +2082,400 @@ def compose_logs(compose, args): targets.extend(container_names_by_service[service]) podman_args = [] if args.follow: - podman_args.append('-f') + podman_args.append("-f") if args.latest: podman_args.append("-l") if args.names: - podman_args.append('-n') + podman_args.append("-n") if args.since: - podman_args.extend(['--since', args.since]) + podman_args.extend(["--since", args.since]) # the default value is to print all logs which is in podman = 0 and not # needed to be passed - if args.tail and args.tail != 'all': - podman_args.extend(['--tail', args.tail]) + if args.tail and args.tail != "all": + podman_args.extend(["--tail", args.tail]) if args.timestamps: - podman_args.append('-t') + podman_args.append("-t") if args.until: - podman_args.extend(['--until', args.until]) + podman_args.extend(["--until", args.until]) for target in targets: podman_args.append(target) - compose.podman.run([], 'logs', podman_args) + compose.podman.run([], "logs", podman_args) -@cmd_run(podman_compose, 'config', "displays the compose file") -def compose_config(compose, args): # pylint: disable=unused-argument + +@cmd_run(podman_compose, "config", "displays the compose file") +def compose_config(compose, args): # pylint: disable=unused-argument print(compose.merged_yaml) + ################### # command arguments parsing ################### -@cmd_parse(podman_compose, 'version') + +@cmd_parse(podman_compose, "version") def compose_version_parse(parser): - parser.add_argument("-f", "--format", choices=['pretty', 'json'], default='pretty', - help="Format the output") - parser.add_argument("--short", action='store_true', - help="Shows only Podman Compose's version number") + parser.add_argument( + "-f", + "--format", + choices=["pretty", "json"], + default="pretty", + help="Format the output", + ) + parser.add_argument( + "--short", + action="store_true", + help="Shows only Podman Compose's version number", + ) -@cmd_parse(podman_compose, 'up') + +@cmd_parse(podman_compose, "up") def compose_up_parse(parser): - parser.add_argument("-d", "--detach", action='store_true', - help="Detached mode: Run container in the background, print new container name. Incompatible with --abort-on-container-exit.") - parser.add_argument("--no-color", action='store_true', - help="Produce monochrome output.") - parser.add_argument("--quiet-pull", action='store_true', - help="Pull without printing progress information.") - parser.add_argument("--no-deps", action='store_true', - help="Don't start linked services.") - parser.add_argument("--force-recreate", action='store_true', - help="Recreate containers even if their configuration and image haven't changed.") - parser.add_argument("--always-recreate-deps", action='store_true', - help="Recreate dependent containers. Incompatible with --no-recreate.") - parser.add_argument("--no-recreate", action='store_true', - help="If containers already exist, don't recreate them. Incompatible with --force-recreate and -V.") - parser.add_argument("--no-build", action='store_true', - help="Don't build an image, even if it's missing.") - parser.add_argument("--no-start", action='store_true', - help="Don't start the services after creating them.") - parser.add_argument("--build", action='store_true', - help="Build images before starting containers.") - parser.add_argument("--abort-on-container-exit", action='store_true', - help="Stops all containers if any container was stopped. Incompatible with -d.") - parser.add_argument("-t", "--timeout", type=int, default=10, - help="Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)") - parser.add_argument("-V", "--renew-anon-volumes", action='store_true', - help="Recreate anonymous volumes instead of retrieving data from the previous containers.") - parser.add_argument("--remove-orphans", action='store_true', - help="Remove containers for services not defined in the Compose file.") - parser.add_argument('--scale', metavar="SERVICE=NUM", action='append', - help="Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.") - parser.add_argument("--exit-code-from", metavar='SERVICE', type=str, default=None, - help="Return the exit code of the selected service container. Implies --abort-on-container-exit.") + parser.add_argument( + "-d", + "--detach", + action="store_true", + help="Detached mode: Run container in the background, print new container name. Incompatible with --abort-on-container-exit.", + ) + parser.add_argument( + "--no-color", action="store_true", help="Produce monochrome output." + ) + parser.add_argument( + "--quiet-pull", + action="store_true", + help="Pull without printing progress information.", + ) + parser.add_argument( + "--no-deps", action="store_true", help="Don't start linked services." + ) + parser.add_argument( + "--force-recreate", + action="store_true", + help="Recreate containers even if their configuration and image haven't changed.", + ) + parser.add_argument( + "--always-recreate-deps", + action="store_true", + help="Recreate dependent containers. Incompatible with --no-recreate.", + ) + parser.add_argument( + "--no-recreate", + action="store_true", + help="If containers already exist, don't recreate them. Incompatible with --force-recreate and -V.", + ) + parser.add_argument( + "--no-build", + action="store_true", + help="Don't build an image, even if it's missing.", + ) + parser.add_argument( + "--no-start", + action="store_true", + help="Don't start the services after creating them.", + ) + parser.add_argument( + "--build", action="store_true", help="Build images before starting containers." + ) + parser.add_argument( + "--abort-on-container-exit", + action="store_true", + help="Stops all containers if any container was stopped. Incompatible with -d.", + ) + parser.add_argument( + "-t", + "--timeout", + type=int, + default=10, + help="Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)", + ) + parser.add_argument( + "-V", + "--renew-anon-volumes", + action="store_true", + help="Recreate anonymous volumes instead of retrieving data from the previous containers.", + ) + parser.add_argument( + "--remove-orphans", + action="store_true", + help="Remove containers for services not defined in the Compose file.", + ) + parser.add_argument( + "--scale", + metavar="SERVICE=NUM", + action="append", + help="Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.", + ) + parser.add_argument( + "--exit-code-from", + metavar="SERVICE", + type=str, + default=None, + help="Return the exit code of the selected service container. Implies --abort-on-container-exit.", + ) -@cmd_parse(podman_compose, 'down') + +@cmd_parse(podman_compose, "down") def compose_down_parse(parser): - parser.add_argument("-v", "--volumes", action='store_true', default=False, + parser.add_argument( + "-v", + "--volumes", + action="store_true", + default=False, help="Remove named volumes declared in the `volumes` section of the Compose file and " - "anonymous volumes attached to containers.") + "anonymous volumes attached to containers.", + ) -@cmd_parse(podman_compose, 'run') + +@cmd_parse(podman_compose, "run") def compose_run_parse(parser): - parser.add_argument("-d", "--detach", action='store_true', - help="Detached mode: Run container in the background, print new container name.") - parser.add_argument("--name", type=str, default=None, - help="Assign a name to the container") - parser.add_argument("--entrypoint", type=str, default=None, - help="Override the entrypoint of the image.") - parser.add_argument('-e', '--env', metavar="KEY=VAL", action='append', - help="Set an environment variable (can be used multiple times)") - parser.add_argument('-l', '--label', metavar="KEY=VAL", action='append', - help="Add or override a label (can be used multiple times)") - parser.add_argument("-u", "--user", type=str, default=None, - help="Run as specified username or uid") - parser.add_argument("--no-deps", action='store_true', - help="Don't start linked services") - parser.add_argument("--rm", action='store_true', - help="Remove container after run. Ignored in detached mode.") - parser.add_argument('-p', '--publish', action='append', - help="Publish a container's port(s) to the host (can be used multiple times)") - parser.add_argument("--service-ports", action='store_true', - help="Run command with the service's ports enabled and mapped to the host.") - parser.add_argument('-v', '--volume', action='append', - help="Bind mount a volume (can be used multiple times)") - parser.add_argument("-T", action='store_true', - help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.") - parser.add_argument("-w", "--workdir", type=str, default=None, - help="Working directory inside the container") - parser.add_argument('service', metavar='service', nargs=None, - help='service name') - parser.add_argument('cnt_command', metavar='command', nargs=argparse.REMAINDER, - help='command and its arguments') + parser.add_argument( + "-d", + "--detach", + action="store_true", + help="Detached mode: Run container in the background, print new container name.", + ) + parser.add_argument( + "--name", type=str, default=None, help="Assign a name to the container" + ) + parser.add_argument( + "--entrypoint", + type=str, + default=None, + help="Override the entrypoint of the image.", + ) + parser.add_argument( + "-e", + "--env", + metavar="KEY=VAL", + action="append", + help="Set an environment variable (can be used multiple times)", + ) + parser.add_argument( + "-l", + "--label", + metavar="KEY=VAL", + action="append", + help="Add or override a label (can be used multiple times)", + ) + parser.add_argument( + "-u", "--user", type=str, default=None, help="Run as specified username or uid" + ) + parser.add_argument( + "--no-deps", action="store_true", help="Don't start linked services" + ) + parser.add_argument( + "--rm", + action="store_true", + help="Remove container after run. Ignored in detached mode.", + ) + parser.add_argument( + "-p", + "--publish", + action="append", + help="Publish a container's port(s) to the host (can be used multiple times)", + ) + parser.add_argument( + "--service-ports", + action="store_true", + help="Run command with the service's ports enabled and mapped to the host.", + ) + parser.add_argument( + "-v", + "--volume", + action="append", + help="Bind mount a volume (can be used multiple times)", + ) + parser.add_argument( + "-T", + action="store_true", + help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.", + ) + parser.add_argument( + "-w", + "--workdir", + type=str, + default=None, + help="Working directory inside the container", + ) + parser.add_argument("service", metavar="service", nargs=None, help="service name") + parser.add_argument( + "cnt_command", + metavar="command", + nargs=argparse.REMAINDER, + help="command and its arguments", + ) -@cmd_parse(podman_compose, 'exec') + +@cmd_parse(podman_compose, "exec") def compose_exec_parse(parser): - parser.add_argument("-d", "--detach", action='store_true', - help="Detached mode: Run container in the background, print new container name.") - parser.add_argument("--privileged", action='store_true', default=False, - help="Give the process extended Linux capabilities inside the container") - parser.add_argument("-u", "--user", type=str, default=None, - help="Run as specified username or uid") - parser.add_argument("-T", action='store_true', - help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.") - parser.add_argument("--index", type=int, default=1, - help="Index of the container if there are multiple instances of a service") - parser.add_argument('-e', '--env', metavar="KEY=VAL", action='append', - help="Set an environment variable (can be used multiple times)") - parser.add_argument("-w", "--workdir", type=str, default=None, - help="Working directory inside the container") - parser.add_argument('service', metavar='service', nargs=None, - help='service name') - parser.add_argument('cnt_command', metavar='command', nargs=argparse.REMAINDER, - help='command and its arguments') + parser.add_argument( + "-d", + "--detach", + action="store_true", + help="Detached mode: Run container in the background, print new container name.", + ) + parser.add_argument( + "--privileged", + action="store_true", + default=False, + help="Give the process extended Linux capabilities inside the container", + ) + parser.add_argument( + "-u", "--user", type=str, default=None, help="Run as specified username or uid" + ) + parser.add_argument( + "-T", + action="store_true", + help="Disable pseudo-tty allocation. By default `podman-compose run` allocates a TTY.", + ) + parser.add_argument( + "--index", + type=int, + default=1, + help="Index of the container if there are multiple instances of a service", + ) + parser.add_argument( + "-e", + "--env", + metavar="KEY=VAL", + action="append", + help="Set an environment variable (can be used multiple times)", + ) + parser.add_argument( + "-w", + "--workdir", + type=str, + default=None, + help="Working directory inside the container", + ) + parser.add_argument("service", metavar="service", nargs=None, help="service name") + parser.add_argument( + "cnt_command", + metavar="command", + nargs=argparse.REMAINDER, + help="command and its arguments", + ) -@cmd_parse(podman_compose, ['down', 'stop', 'restart']) +@cmd_parse(podman_compose, ["down", "stop", "restart"]) def compose_parse_timeout(parser): - parser.add_argument("-t", "--timeout", + parser.add_argument( + "-t", + "--timeout", help="Specify a shutdown timeout in seconds. ", - type=int, default=10) + type=int, + default=10, + ) -@cmd_parse(podman_compose, ['logs']) + +@cmd_parse(podman_compose, ["logs"]) def compose_logs_parse(parser): - parser.add_argument("-f", "--follow", action='store_true', - help="Follow log output. The default is false") - parser.add_argument("-l", "--latest", action='store_true', - help="Act on the latest container podman is aware of") - parser.add_argument("-n", "--names", action='store_true', - help="Output the container name in the log") - parser.add_argument("--since", help="Show logs since TIMESTAMP", - type=str, default=None) - parser.add_argument("-t", "--timestamps", action='store_true', - help="Show timestamps.") - parser.add_argument("--tail", - help="Number of lines to show from the end of the logs for each " - "container.", - type=str, default="all") - parser.add_argument("--until", help="Show logs until TIMESTAMP", - type=str, default=None) - parser.add_argument('services', metavar='services', nargs='*', default=None, - help='service names') + parser.add_argument( + "-f", + "--follow", + action="store_true", + help="Follow log output. The default is false", + ) + parser.add_argument( + "-l", + "--latest", + action="store_true", + help="Act on the latest container podman is aware of", + ) + parser.add_argument( + "-n", + "--names", + action="store_true", + help="Output the container name in the log", + ) + parser.add_argument( + "--since", help="Show logs since TIMESTAMP", type=str, default=None + ) + parser.add_argument( + "-t", "--timestamps", action="store_true", help="Show timestamps." + ) + parser.add_argument( + "--tail", + help="Number of lines to show from the end of the logs for each " "container.", + type=str, + default="all", + ) + parser.add_argument( + "--until", help="Show logs until TIMESTAMP", type=str, default=None + ) + parser.add_argument( + "services", metavar="services", nargs="*", default=None, help="service names" + ) -@cmd_parse(podman_compose, 'pull') + +@cmd_parse(podman_compose, "pull") def compose_pull_parse(parser): - parser.add_argument("--force-local", action='store_true', default=False, - help="Also pull unprefixed images for services which have a build section") + parser.add_argument( + "--force-local", + action="store_true", + default=False, + help="Also pull unprefixed images for services which have a build section", + ) -@cmd_parse(podman_compose, 'push') + +@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') + 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, 'ps') + +@cmd_parse(podman_compose, "ps") def compose_ps_parse(parser): - parser.add_argument("-q", "--quiet", - help="Only display container IDs", action='store_true') + parser.add_argument( + "-q", "--quiet", help="Only display container IDs", action="store_true" + ) -@cmd_parse(podman_compose, ['build', 'up']) + +@cmd_parse(podman_compose, ["build", "up"]) def compose_build_up_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') - parser.add_argument("--build-arg", metavar="key=val", action="append", default=[], - help="Set build-time variables for services.") - parser.add_argument("--no-cache", - help="Do not use cache when building the image.", action='store_true') + 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", + ) + parser.add_argument( + "--build-arg", + metavar="key=val", + action="append", + default=[], + help="Set build-time variables for services.", + ) + parser.add_argument( + "--no-cache", + help="Do not use cache when building the image.", + action="store_true", + ) -@cmd_parse(podman_compose, ['build', 'up', 'down', 'start', 'stop', 'restart']) + +@cmd_parse(podman_compose, ["build", "up", "down", "start", "stop", "restart"]) def compose_build_parse(parser): - parser.add_argument('services', metavar='services', nargs='*',default=None, - help='affected services') + parser.add_argument( + "services", + metavar="services", + nargs="*", + default=None, + help="affected services", + ) + def main(): podman_compose.run() + if __name__ == "__main__": main() diff --git a/pytests/test_volumes.py b/pytests/test_volumes.py index 810ede3..8133115 100644 --- a/pytests/test_volumes.py +++ b/pytests/test_volumes.py @@ -2,6 +2,7 @@ import pytest from podman_compose import parse_short_mount + @pytest.fixture def multi_propagation_mount_str(): return "/foo/bar:/baz:U,Z" diff --git a/setup.py b/setup.py index 5a1921d..d2bea08 100644 --- a/setup.py +++ b/setup.py @@ -2,15 +2,15 @@ import os from setuptools import setup try: - readme = open(os.path.join(os.path.dirname(__file__), 'README.md')).read() + readme = open(os.path.join(os.path.dirname(__file__), "README.md")).read() except: - readme = '' + readme = "" setup( - name='podman-compose', + name="podman-compose", description="A script to run docker-compose.yml using podman", long_description=readme, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", @@ -23,21 +23,17 @@ setup( "Topic :: Software Development :: Build Tools", "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", ], - keywords='podman, podman-compose', - author='Muayyad Alsadi', - author_email='alsadi@gmail.com', - url='https://github.com/containers/podman-compose', - py_modules=['podman_compose'], - entry_points={ - 'console_scripts': [ - 'podman-compose = podman_compose:main' - ] - }, + keywords="podman, podman-compose", + author="Muayyad Alsadi", + author_email="alsadi@gmail.com", + url="https://github.com/containers/podman-compose", + py_modules=["podman_compose"], + entry_points={"console_scripts": ["podman-compose = podman_compose:main"]}, include_package_data=True, - license='GPL-2.0-only', + license="GPL-2.0-only", install_requires=[ - 'pyyaml', - 'python-dotenv', + "pyyaml", + "python-dotenv", ], # test_suite='tests', # tests_require=[ diff --git a/test-requirements.txt b/test-requirements.txt index 351a79f..5a20426 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,3 +6,4 @@ coverage pytest-cov pytest tox +black diff --git a/test_volumes.py b/test_volumes.py index 810ede3..8133115 100644 --- a/test_volumes.py +++ b/test_volumes.py @@ -2,6 +2,7 @@ import pytest from podman_compose import parse_short_mount + @pytest.fixture def multi_propagation_mount_str(): return "/foo/bar:/baz:U,Z" diff --git a/tests/test_podman_compose.py b/tests/test_podman_compose.py index 60bacef..62777c2 100644 --- a/tests/test_podman_compose.py +++ b/tests/test_podman_compose.py @@ -11,6 +11,7 @@ def capture(command): out, err = proc.communicate() return out, err, proc.returncode + def test_podman_compose_extends_w_file_subdir(): """ Test that podman-compose can execute podman-compose -f up with extended File which @@ -25,7 +26,7 @@ def test_podman_compose_extends_w_file_subdir(): "-f", str(main_path.joinpath("tests", "extends_w_file_subdir", "docker-compose.yml")), "up", - "-d" + "-d", ] command_check_container = [ @@ -34,7 +35,7 @@ def test_podman_compose_extends_w_file_subdir(): "ps", "--all", "--format", - "\"{{.Image}}\"" + '"{{.Image}}"', ] command_down = [ @@ -42,7 +43,7 @@ def test_podman_compose_extends_w_file_subdir(): "rmi", "--force", "localhost/subdir_test:me", - "docker.io/library/bash" + "docker.io/library/bash", ] out, err, returncode = capture(command_up) @@ -57,4 +58,4 @@ def test_podman_compose_extends_w_file_subdir(): # check container did not exists anymore out, err, returncode = capture(command_check_container) assert 0 == returncode - assert out == b'' + assert out == b""