mirror of
https://github.com/containers/podman-compose.git
synced 2025-07-01 13:10:24 +02:00
Compare commits
11 Commits
devel-asyn
...
v0.1.10
Author | SHA1 | Date | |
---|---|---|---|
e9b103eb23 | |||
bbaa786739 | |||
d1d0f9e452 | |||
d8dba61e08 | |||
3343910763 | |||
34ec4b3cb9 | |||
f4a78ae812 | |||
00b9ce1ee4 | |||
749d188321 | |||
e879529976 | |||
1555417958 |
18
README.md
18
README.md
@ -6,10 +6,19 @@ This project is aimed to provide drop-in replacement for `docker-compose`,
|
|||||||
and it's very useful for certain cases because:
|
and it's very useful for certain cases because:
|
||||||
|
|
||||||
- can run rootless
|
- can run rootless
|
||||||
- only depend on `podman` and Python3 and [PyYAML](https://pyyaml.org/)
|
|
||||||
- no daemon, no setup.
|
- no daemon, no setup.
|
||||||
- can be used by developers to run single-machine containerized stacks using single familiar YAML file
|
- can be used by developers to run single-machine containerized stacks using single familiar YAML file
|
||||||
|
|
||||||
|
This project only depend on:
|
||||||
|
|
||||||
|
* `podman`
|
||||||
|
* Python3
|
||||||
|
* [PyYAML](https://pyyaml.org/)
|
||||||
|
* [python-dotenv](https://pypi.org/project/python-dotenv/)
|
||||||
|
|
||||||
|
And it's formed as a single python file script that you can drop into your PATH and run.
|
||||||
|
|
||||||
|
|
||||||
For production-like single-machine containerized environment consider
|
For production-like single-machine containerized environment consider
|
||||||
|
|
||||||
- [k3s](https://k3s.io) | [k3s github](https://github.com/rancher/k3s)
|
- [k3s](https://k3s.io) | [k3s github](https://github.com/rancher/k3s)
|
||||||
@ -20,9 +29,12 @@ For production-like single-machine containerized environment consider
|
|||||||
For the real thing (multi-node clusters) check any production
|
For the real thing (multi-node clusters) check any production
|
||||||
OpenShift/Kubernetes distribution like [OKD](https://www.okd.io/minishift/).
|
OpenShift/Kubernetes distribution like [OKD](https://www.okd.io/minishift/).
|
||||||
|
|
||||||
## NOTE
|
## Versions
|
||||||
|
|
||||||
This project is still under development.
|
If you have legacy version of `podman` (before 3.x) you might need to stick with legacy `podman-compose` `0.1.x` branch.
|
||||||
|
The legacy branch 0.1.x uses mappings and workarounds to compensate for rootless limitations.
|
||||||
|
|
||||||
|
Modern podman versions (>=3.4) do not have those limitations and thus you can use latest and stable 1.x branch.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -32,10 +32,10 @@ except ImportError:
|
|||||||
# import fnmatch
|
# import fnmatch
|
||||||
# fnmatch.fnmatchcase(env, "*_HOST")
|
# fnmatch.fnmatchcase(env, "*_HOST")
|
||||||
|
|
||||||
import json
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
|
||||||
__version__ = '0.1.8'
|
__version__ = '0.1.10'
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
PY3 = sys.version_info[0] == 3
|
||||||
if PY3:
|
if PY3:
|
||||||
@ -112,8 +112,7 @@ def parse_short_mount(mount_str, basedir):
|
|||||||
# User-relative path
|
# User-relative path
|
||||||
# - ~/configs:/etc/configs/:ro
|
# - ~/configs:/etc/configs/:ro
|
||||||
mount_type = "bind"
|
mount_type = "bind"
|
||||||
# TODO: should we use os.path.realpath(basedir)?
|
mount_src = os.path.realpath(os.path.join(basedir, os.path.expanduser(mount_src)))
|
||||||
mount_src = os.path.join(basedir, os.path.expanduser(mount_src))
|
|
||||||
else:
|
else:
|
||||||
# Named volume
|
# Named volume
|
||||||
# - datavolume:/var/lib/mysql
|
# - datavolume:/var/lib/mysql
|
||||||
@ -355,6 +354,8 @@ def tr_cntnet(project_name, services, given_containers):
|
|||||||
infra = dict(
|
infra = dict(
|
||||||
name=infra_name,
|
name=infra_name,
|
||||||
image="k8s.gcr.io/pause:3.1",
|
image="k8s.gcr.io/pause:3.1",
|
||||||
|
_service=None,
|
||||||
|
service_name=None
|
||||||
)
|
)
|
||||||
for cnt0 in given_containers:
|
for cnt0 in given_containers:
|
||||||
cnt = dict(cnt0, network_mode="container:"+infra_name)
|
cnt = dict(cnt0, network_mode="container:"+infra_name)
|
||||||
@ -404,6 +405,16 @@ def assert_volume(compose, mount_dict):
|
|||||||
create volume if needed
|
create volume if needed
|
||||||
"""
|
"""
|
||||||
vol = mount_dict.get("_vol", None)
|
vol = mount_dict.get("_vol", None)
|
||||||
|
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)))
|
||||||
|
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
|
proj_name = compose.project_name
|
||||||
vol_name = vol["name"]
|
vol_name = vol["name"]
|
||||||
@ -706,7 +717,6 @@ def container_to_args(compose, cnt, detached=True):
|
|||||||
for i in tmpfs_ls:
|
for i in tmpfs_ls:
|
||||||
podman_args.extend(['--tmpfs', i])
|
podman_args.extend(['--tmpfs', i])
|
||||||
for volume in cnt.get('volumes', []):
|
for volume in cnt.get('volumes', []):
|
||||||
# TODO: should we make it os.path.realpath(os.path.join(, i))?
|
|
||||||
podman_args.extend(get_mount_args(compose, cnt, volume))
|
podman_args.extend(get_mount_args(compose, cnt, volume))
|
||||||
log = cnt.get('logging')
|
log = cnt.get('logging')
|
||||||
if log is not None:
|
if log is not None:
|
||||||
@ -855,7 +865,8 @@ def flat_deps(services, with_extends=False):
|
|||||||
if ext != name: deps.add(ext)
|
if ext != name: deps.add(ext)
|
||||||
continue
|
continue
|
||||||
deps_ls = srv.get("depends_on", None) or []
|
deps_ls = srv.get("depends_on", None) or []
|
||||||
if not is_list(deps_ls): deps_ls=[deps_ls]
|
if is_str(deps_ls): deps_ls=[deps_ls]
|
||||||
|
elif is_dict(deps_ls): deps_ls=list(deps_ls.keys())
|
||||||
deps.update(deps_ls)
|
deps.update(deps_ls)
|
||||||
# parse link to get service name and remove alias
|
# parse link to get service name and remove alias
|
||||||
links_ls = srv.get("links", None) or []
|
links_ls = srv.get("links", None) or []
|
||||||
@ -903,10 +914,22 @@ class Podman:
|
|||||||
time.sleep(sleep)
|
time.sleep(sleep)
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
def volume_inspect_all(self):
|
||||||
|
output = self.output(["volume", "inspect", "--all"]).decode('utf-8')
|
||||||
|
return json.loads(output)
|
||||||
|
|
||||||
|
def volume_rm(self, name):
|
||||||
|
return self.run(["volume", "rm", name])
|
||||||
|
|
||||||
def normalize_service(service):
|
def normalize_service(service):
|
||||||
for key in ("env_file", "security_opt"):
|
for key in ("env_file", "security_opt", "volumes"):
|
||||||
if key not in service: continue
|
if key not in service: continue
|
||||||
if is_str(service[key]): service[key]=[service[key]]
|
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=="seccomp:unconfined" or item=="apparmor:unconfined":
|
||||||
|
sec_ls[ix] = item.replace(":", "=")
|
||||||
for key in ("environment", "labels"):
|
for key in ("environment", "labels"):
|
||||||
if key not in service: continue
|
if key not in service: continue
|
||||||
service[key] = norm_as_dict(service[key])
|
service[key] = norm_as_dict(service[key])
|
||||||
@ -942,7 +965,15 @@ def rec_merge_one(target, source):
|
|||||||
if type(value2)!=type(value):
|
if type(value2)!=type(value):
|
||||||
raise ValueError("can't merge value of {} of type {} and {}".format(key, type(value), type(value2)))
|
raise ValueError("can't merge value of {} of type {} and {}".format(key, type(value), type(value2)))
|
||||||
if is_list(value2):
|
if is_list(value2):
|
||||||
value.extend(value2)
|
if key == 'volumes':
|
||||||
|
# clean duplicate mount targets
|
||||||
|
pts = set([ 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)
|
||||||
|
else:
|
||||||
|
value.extend(value2)
|
||||||
elif is_dict(value2):
|
elif is_dict(value2):
|
||||||
rec_merge_one(value, value2)
|
rec_merge_one(value, value2)
|
||||||
else:
|
else:
|
||||||
@ -983,6 +1014,27 @@ def resolve_extends(services, service_names, environ):
|
|||||||
new_service = rec_merge({}, from_service, service)
|
new_service = rec_merge({}, from_service, service)
|
||||||
services[name] = new_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",
|
||||||
|
"compose.override.yaml",
|
||||||
|
"compose.override.yml",
|
||||||
|
"podman-compose.yaml",
|
||||||
|
"podman-compose.yml",
|
||||||
|
"docker-compose.yml",
|
||||||
|
"docker-compose.yaml",
|
||||||
|
"docker-compose.override.yml",
|
||||||
|
"docker-compose.override.yaml",
|
||||||
|
"container-compose.yml",
|
||||||
|
"container-compose.yaml",
|
||||||
|
"container-compose.override.yml",
|
||||||
|
"container-compose.override.yaml",
|
||||||
|
]
|
||||||
|
|
||||||
class PodmanCompose:
|
class PodmanCompose:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -1042,23 +1094,14 @@ class PodmanCompose:
|
|||||||
def _parse_compose_file(self):
|
def _parse_compose_file(self):
|
||||||
args = self.global_args
|
args = self.global_args
|
||||||
cmd = args.command
|
cmd = args.command
|
||||||
|
pathsep = os.environ.get("COMPOSE_PATH_SEPARATOR", None) or os.pathsep
|
||||||
if not args.file:
|
if not args.file:
|
||||||
args.file = list(filter(os.path.exists, [
|
default_str = os.environ.get("COMPOSE_FILE", None)
|
||||||
"compose.yaml",
|
if default_str:
|
||||||
"compose.yml",
|
default_ls = default_str.split(pathsep)
|
||||||
"compose.override.yaml",
|
else:
|
||||||
"compose.override.yml",
|
default_ls = COMPOSE_DEFAULT_LS
|
||||||
"podman-compose.yaml",
|
args.file = list(filter(os.path.exists, default_ls))
|
||||||
"podman-compose.yml",
|
|
||||||
"docker-compose.yml",
|
|
||||||
"docker-compose.yaml",
|
|
||||||
"docker-compose.override.yml",
|
|
||||||
"docker-compose.override.yaml",
|
|
||||||
"container-compose.yml",
|
|
||||||
"container-compose.yaml",
|
|
||||||
"container-compose.override.yml",
|
|
||||||
"container-compose.override.yaml"
|
|
||||||
]))
|
|
||||||
files = args.file
|
files = args.file
|
||||||
if not files:
|
if not files:
|
||||||
print("no compose.yaml, docker-compose.yml or container-compose.yml file found, pass files with -f")
|
print("no compose.yaml, docker-compose.yml or container-compose.yml file found, pass files with -f")
|
||||||
@ -1078,7 +1121,7 @@ class PodmanCompose:
|
|||||||
dry_run = args.dry_run
|
dry_run = args.dry_run
|
||||||
transform_policy = args.transform_policy
|
transform_policy = args.transform_policy
|
||||||
host_env = None
|
host_env = None
|
||||||
dirname = os.path.dirname(filename)
|
dirname = os.path.realpath(os.path.dirname(filename))
|
||||||
dir_basename = os.path.basename(dirname)
|
dir_basename = os.path.basename(dirname)
|
||||||
self.dirname = dirname
|
self.dirname = dirname
|
||||||
# TODO: remove next line
|
# TODO: remove next line
|
||||||
@ -1095,17 +1138,13 @@ class PodmanCompose:
|
|||||||
|
|
||||||
dotenv_path = os.path.join(dirname, ".env")
|
dotenv_path = os.path.join(dirname, ".env")
|
||||||
self.environ = dict(os.environ)
|
self.environ = dict(os.environ)
|
||||||
if os.path.isfile(dotenv_path):
|
self.environ.update(dotenv_to_dict(dotenv_path))
|
||||||
with open(dotenv_path, 'r') as f:
|
|
||||||
dotenv_ls = [l.strip() for l in f if l.strip() and not l.startswith('#')]
|
|
||||||
self.environ.update(dict([l.split("=", 1) for l in dotenv_ls if "=" in l]))
|
|
||||||
# TODO: should read and respect those env variables
|
|
||||||
# see: https://docs.docker.com/compose/reference/envvars/
|
# see: https://docs.docker.com/compose/reference/envvars/
|
||||||
# see: https://docs.docker.com/compose/env-file/
|
# see: https://docs.docker.com/compose/env-file/
|
||||||
self.environ.update({
|
self.environ.update({
|
||||||
"COMPOSE_FILE": os.path.basename(filename),
|
"COMPOSE_FILE": os.path.basename(filename),
|
||||||
"COMPOSE_PROJECT_NAME": self.project_name,
|
"COMPOSE_PROJECT_NAME": self.project_name,
|
||||||
"COMPOSE_PATH_SEPARATOR": ":",
|
"COMPOSE_PATH_SEPARATOR": pathsep,
|
||||||
})
|
})
|
||||||
compose = {'_dirname': dirname}
|
compose = {'_dirname': dirname}
|
||||||
for filename in files:
|
for filename in files:
|
||||||
@ -1491,6 +1530,12 @@ def compose_down(compose, args):
|
|||||||
return
|
return
|
||||||
for pod in compose.pods:
|
for pod in compose.pods:
|
||||||
compose.podman.run([], "pod", ["rm", pod["name"]], sleep=0)
|
compose.podman.run([], "pod", ["rm", pod["name"]], sleep=0)
|
||||||
|
if args.volumes:
|
||||||
|
volumes = compose.podman.volume_inspect_all()
|
||||||
|
for volume in volumes:
|
||||||
|
project = volume.get("Labels", {}).get("io.podman.compose.project")
|
||||||
|
if project == compose.project_name:
|
||||||
|
compose.podman.volume_rm(volume["Name"])
|
||||||
|
|
||||||
@cmd_run(podman_compose, 'ps', 'show status of containers')
|
@cmd_run(podman_compose, 'ps', 'show status of containers')
|
||||||
def compose_ps(compose, args):
|
def compose_ps(compose, args):
|
||||||
@ -1511,7 +1556,7 @@ def compose_run(compose, args):
|
|||||||
up_args = argparse.Namespace(**dict(args.__dict__,
|
up_args = argparse.Namespace(**dict(args.__dict__,
|
||||||
detach=True, services=deps,
|
detach=True, services=deps,
|
||||||
# defaults
|
# defaults
|
||||||
no_build=False, build=True, force_recreate=False, no_start=False,
|
no_build=False, build=True, force_recreate=False, no_start=False, no_cache=False, build_arg=[],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
compose.commands['up'](compose, up_args)
|
compose.commands['up'](compose, up_args)
|
||||||
@ -1650,6 +1695,12 @@ def compose_up_parse(parser):
|
|||||||
parser.add_argument("--exit-code-from", metavar='SERVICE', type=str, default=None,
|
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.")
|
help="Return the exit code of the selected service container. Implies --abort-on-container-exit.")
|
||||||
|
|
||||||
|
@cmd_parse(podman_compose, 'down')
|
||||||
|
def compose_down_parse(parser):
|
||||||
|
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.")
|
||||||
|
|
||||||
@cmd_parse(podman_compose, 'run')
|
@cmd_parse(podman_compose, 'run')
|
||||||
def compose_run_parse(parser):
|
def compose_run_parse(parser):
|
||||||
parser.add_argument("-d", "--detach", action='store_true',
|
parser.add_argument("-d", "--detach", action='store_true',
|
||||||
|
@ -3,3 +3,5 @@
|
|||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
|
|
||||||
pyyaml
|
pyyaml
|
||||||
|
python-dotenv
|
||||||
|
|
||||||
|
3
setup.py
3
setup.py
@ -36,7 +36,8 @@ setup(
|
|||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
license='GPL-2.0-only',
|
license='GPL-2.0-only',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pyyaml'
|
'pyyaml',
|
||||||
|
'python-dotenv',
|
||||||
],
|
],
|
||||||
# test_suite='tests',
|
# test_suite='tests',
|
||||||
# tests_require=[
|
# tests_require=[
|
||||||
|
12
tests/seccomp/docker-compose.yml
Normal file
12
tests/seccomp/docker-compose.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
web1:
|
||||||
|
image: busybox
|
||||||
|
command: httpd -f -p 80 -h /var/www/html
|
||||||
|
volumes:
|
||||||
|
- ./docker-compose.yml:/var/www/html/index.html
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
security_opt:
|
||||||
|
- seccomp:unconfined
|
||||||
|
|
Reference in New Issue
Block a user