mirror of
https://github.com/containers/podman-compose.git
synced 2025-07-01 21:20:18 +02:00
Compare commits
152 Commits
Author | SHA1 | Date | |
---|---|---|---|
bd29caa797 | |||
f0928dd399 | |||
6c9c09197a | |||
cda84f439f | |||
67616bdaac | |||
7497692b19 | |||
782c44d4c3 | |||
d7762a54f0 | |||
eba2ca2695 | |||
abe5965c9a | |||
9e0da82726 | |||
6acdafd5b1 | |||
8638eb9b6d | |||
e1d938ffa6 | |||
d532e09d7d | |||
1dab256cdd | |||
2a33ef5c79 | |||
5ab734026c | |||
35dc395483 | |||
38a9263424 | |||
cbe9587973 | |||
8bb43100b1 | |||
98f166d2e4 | |||
150ab02446 | |||
ff58a0bff0 | |||
8d899ebb65 | |||
342a39dcfe | |||
d6b8476573 | |||
ae41ef08c3 | |||
da46ee3910 | |||
d80c31f578 | |||
cefa68dc75 | |||
2e46ff0db2 | |||
fbc4c7da80 | |||
11879d3e94 | |||
27cf8da06f | |||
10a30ba24a | |||
a1be62fd31 | |||
15bf02a004 | |||
e45b5d5063 | |||
c46ecb226b | |||
e04b8f3a60 | |||
815450aba9 | |||
92f0a8583a | |||
5f4fc4618c | |||
4d899edeb3 | |||
f9489afaf5 | |||
7d7533772b | |||
65b455f081 | |||
1aa750bacf | |||
98b9bb9f8e | |||
170411de8b | |||
0cf1378cb5 | |||
f5a6df6dc4 | |||
f106ea0c01 | |||
b748c2666c | |||
3973c476c4 | |||
8b1bd0123c | |||
2e7d83f7f0 | |||
52e2912e0b | |||
8ef537e247 | |||
04fcc26a79 | |||
d4760712b7 | |||
7c61f24467 | |||
202c3771a9 | |||
a54f0fa573 | |||
3353697402 | |||
ca1b59c449 | |||
b9f27795c0 | |||
4cd1642be0 | |||
fd401331e5 | |||
37b27fa233 | |||
976847ef9b | |||
c6b3d497d6 | |||
10ad739746 | |||
784d798dac | |||
dd01d039bf | |||
81a0a5933e | |||
c289a3b827 | |||
baccce4f3f | |||
07af8488db | |||
cbc5a8c8b3 | |||
aeaceed7ba | |||
b1eb558b41 | |||
1cdc9e6552 | |||
2f8ed2137c | |||
838957b902 | |||
15380a809d | |||
d4e5859370 | |||
593d7c825e | |||
bfba7ba32d | |||
fe9be2d98f | |||
43a2f1d01f | |||
aa47a373ca | |||
eaec19336f | |||
34bee28bb8 | |||
bfea139362 | |||
4a81bce2a5 | |||
e626f15eff | |||
974250caa5 | |||
29404af723 | |||
d1ba2f4c7d | |||
e03d675b9b | |||
51d180d2d0 | |||
9c905f9012 | |||
bdb3e4e984 | |||
105e390f6b | |||
d79ff01e77 | |||
d9ef3d2cc6 | |||
d23ef4f481 | |||
b685bce400 | |||
7d5bf645f6 | |||
9f7ae38bac | |||
3cee4e015c | |||
498a1994ce | |||
488908f809 | |||
f7bcc4221e | |||
a73df712cc | |||
50dc19f5f8 | |||
9029dce0f6 | |||
a8282c77d6 | |||
f4b775c7e4 | |||
adf30e0475 | |||
41675c3916 | |||
6caf2eae42 | |||
3093b00326 | |||
1c21d655ba | |||
18e5fd64f3 | |||
24bdfd1e17 | |||
c2d3e156c2 | |||
ba95100cff | |||
6022669991 | |||
e29df71d42 | |||
21b9d385b2 | |||
4c17ce2434 | |||
09d54e9dcc | |||
f1dd9b374e | |||
87af67fe94 | |||
f1d663874e | |||
69ffff33f6 | |||
f376700972 | |||
9be3ec985f | |||
6e642dca1f | |||
0f2c717655 | |||
2aa042b9c7 | |||
a177603661 | |||
bc4177fbdc | |||
8206cc3ea2 | |||
60ac5e43b3 | |||
48c6c38fcd | |||
84f1fbd622 | |||
ac5291e10b |
@ -1,17 +1,10 @@
|
||||
default_install_hook_types: [pre-commit, commit-msg]
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
hooks:
|
||||
- id: black
|
||||
# It is recommended to specify the latest version of Python
|
||||
# supported by your project here, or alternatively use
|
||||
# pre-commit's default_language_version, see
|
||||
# https://pre-commit.com/#top_level-default_language_version
|
||||
language_version: python3.10
|
||||
- id: ruff
|
||||
types: [python]
|
||||
args: [
|
||||
"--check", # Don't apply changes automatically
|
||||
]
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
@ -34,3 +27,8 @@ repos:
|
||||
rev: v2.2.5
|
||||
hooks:
|
||||
- id: codespell
|
||||
|
||||
- repo: https://github.com/gklein/check_signoff
|
||||
rev: v1.0.5
|
||||
hooks:
|
||||
- id: check-signoff
|
||||
|
@ -1,7 +1,7 @@
|
||||
[MESSAGES CONTROL]
|
||||
# C0111 missing-docstring: missing-class-docstring, missing-function-docstring, missing-method-docstring, missing-module-docstrin
|
||||
# consider-using-with: we need it for color formatter pipe
|
||||
disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment
|
||||
disable=too-many-lines,too-many-branches,too-many-locals,too-many-statements,too-many-arguments,too-many-instance-attributes,fixme,multiple-statements,missing-docstring,line-too-long,consider-using-f-string,consider-using-with,unnecessary-lambda-assignment,broad-exception-caught
|
||||
# allow _ for ignored variables
|
||||
# allow generic names like a,b,c and i,j,k,l,m,n and x,y,z
|
||||
# allow k,v for key/value
|
||||
|
@ -35,7 +35,7 @@ Pull the merge commit created on the `main` branch during the step 2.
|
||||
Then run:
|
||||
|
||||
```
|
||||
./scripts/make_release.sh
|
||||
./scripts/make_release.sh $VERSION
|
||||
```
|
||||
|
||||
This will create release commit, tag and push everything.
|
||||
|
39
docs/Changelog-1.4.0.md
Normal file
39
docs/Changelog-1.4.0.md
Normal file
@ -0,0 +1,39 @@
|
||||
Version 1.4.0 (2025-05-10)
|
||||
==========================
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Fixed handling of relative includes and extends in compose files
|
||||
- Fixed error when merging arguments in list and dictionary syntax
|
||||
- Fixed issue where short-lived containers could execute twice when using `up` in detached mode
|
||||
- Fixed `up` command hanging on Podman versions earlier than 4.6.0
|
||||
- Fixed issue where `service_healthy` conditions weren't enforced during `up` command
|
||||
- Fixed support for the `--scale` flag
|
||||
- Fixed bug causing dependent containers to start despite `--no-deps` flag
|
||||
- Fixed port command behavior for dynamic host ports
|
||||
- Fixed interpolation of `COMPOSE_PROJECT_NAME` when set from top-level `name` in compose file
|
||||
- Fixed project name evaluation order to match compose spec
|
||||
- Fixed build context when using git URLs
|
||||
- Fixed `KeyError` when `down` is called with non-existent service
|
||||
- Skip `down` during `up` when no active containers exist
|
||||
- Fixed non-zero exit code on failure when using `up -d`
|
||||
- Fixed SIGINT handling during `up` command for graceful shutdown
|
||||
- Fixed `NotImplementedError` when interrupted on Windows
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Added `--quiet` flag to `config` command to suppress output
|
||||
- Added support for `pids_limit` and `deploy.resources.limits.pids`
|
||||
- Added `--abort-on-container-failure` option
|
||||
- Added `--rmi` argument to `down` command for image removal
|
||||
- Added support for `x-podman.disable-dns` to disable DNS plugin on defined networks
|
||||
- Added support for `x-podman.dns` to set DNS nameservers for defined networks
|
||||
- Improved file descriptor handling - no longer closes externally created descriptors.
|
||||
This allows descriptors created e.g. via systemd socket activation to be passed to
|
||||
containers.
|
||||
- Added support for `cpuset` configuration
|
||||
- Added support for `reset` and `override` tags when merging compose files
|
||||
- Added support for `x-podman.interface_name` to set network interface names
|
||||
- Added support for `x-podman.pod_args` to override default `--pod-args`
|
@ -27,6 +27,26 @@ services:
|
||||
|
||||
For explanations of these extensions, please refer to the [Podman Documentation](https://docs.podman.io/).
|
||||
|
||||
## Network management
|
||||
|
||||
The following extension keys are available under network configuration:
|
||||
|
||||
* `x-podman.disable-dns` - Disable the DNS plugin for the network when set to 'true'.
|
||||
* `x-podman.dns` - Set nameservers for the network using supplied addresses (cannot be used with x-podman.disable-dns`).
|
||||
|
||||
For example, the following docker-compose.yml allows all containers on the same network to use the
|
||||
specified nameservers:
|
||||
```yml
|
||||
version: "3"
|
||||
network:
|
||||
my_network:
|
||||
x-podman.dns:
|
||||
- "10.1.2.3"
|
||||
- "10.1.2.4"
|
||||
```
|
||||
|
||||
For explanations of these extensions, please refer to the
|
||||
[Podman network create command Documentation](https://docs.podman.io/en/latest/markdown/podman-network-create.1.html).
|
||||
|
||||
## Per-network MAC-addresses
|
||||
|
||||
@ -66,7 +86,7 @@ networks:
|
||||
- subnet: "192.168.1.0/24"
|
||||
|
||||
services:
|
||||
webserver
|
||||
webserver:
|
||||
image: "busybox"
|
||||
command: ["/bin/busybox", "httpd", "-f", "-h", "/etc", "-p", "8001"]
|
||||
networks:
|
||||
@ -78,6 +98,10 @@ services:
|
||||
mac_address: "02:bb:bb:bb:bb:bb" # mac_address is supported
|
||||
```
|
||||
|
||||
## Per-network interface name
|
||||
|
||||
Using `x-podman.interface_name` within a containers network config you can specify the interface name inside the container.
|
||||
|
||||
## Podman-specific network modes
|
||||
|
||||
Generic docker-compose supports the following values for `network-mode` for a container:
|
||||
@ -154,3 +178,17 @@ services:
|
||||
x-podman:
|
||||
in_pod: false
|
||||
```
|
||||
|
||||
It is also possible to override the default arguments for pod creation that are
|
||||
used when --pod-args is not passed on the command line:
|
||||
```yml
|
||||
version: "3"
|
||||
services:
|
||||
cont:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-p", "8080"]
|
||||
x-podman:
|
||||
pod_args: ["--infra=false", "--share=", "--cpus=1"]
|
||||
```
|
||||
When not set in docker-compose.yml or on the command line, the pod args default
|
||||
to `["--infra=false", "--share="]`.
|
||||
|
9
examples/docker-inline/docker-compose.yml
Normal file
9
examples/docker-inline/docker-compose.yml
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
version: '3'
|
||||
services:
|
||||
dummy:
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM alpine
|
||||
RUN echo "hello world"
|
@ -7,7 +7,7 @@
|
||||
# https://docs.docker.com/compose/django/
|
||||
# https://docs.docker.com/compose/wordpress/
|
||||
# TODO: podman pod logs --color -n -f pod_testlogs
|
||||
from __future__ import annotations
|
||||
from __future__ import annotations # If you see an error here, use Python 3.7 or greater
|
||||
|
||||
import argparse
|
||||
import asyncio.exceptions
|
||||
@ -15,7 +15,6 @@ import asyncio.subprocess
|
||||
import getpass
|
||||
import glob
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@ -25,6 +24,8 @@ import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
from asyncio import Task
|
||||
from enum import Enum
|
||||
|
||||
@ -39,7 +40,7 @@ except ImportError:
|
||||
import yaml
|
||||
from dotenv import dotenv_values
|
||||
|
||||
__version__ = "1.3.0"
|
||||
__version__ = "1.4.0"
|
||||
|
||||
script = os.path.realpath(sys.argv[0])
|
||||
|
||||
@ -270,7 +271,6 @@ def rec_subs(value, subs_dict):
|
||||
svc_envs = {k: v for k, v in value['environment'].items() if k not in subs_dict}
|
||||
# we need to add `svc_envs` to the `subs_dict` so that it can evaluate the
|
||||
# service environment that reference to another service environment.
|
||||
subs_dict.update(svc_envs)
|
||||
svc_envs = rec_subs(svc_envs, subs_dict)
|
||||
subs_dict.update(svc_envs)
|
||||
|
||||
@ -771,6 +771,22 @@ def container_to_cpu_res_args(cnt, podman_args):
|
||||
str(mem_res).lower(),
|
||||
))
|
||||
|
||||
# Handle pids limit from both container level and deploy section
|
||||
pids_limit = cnt.get("pids_limit")
|
||||
deploy_pids = limits.get("pids")
|
||||
|
||||
# Ensure consistency between pids_limit and deploy.resources.limits.pids
|
||||
if pids_limit is not None and deploy_pids is not None:
|
||||
if str(pids_limit) != str(deploy_pids):
|
||||
raise ValueError(
|
||||
f"Inconsistent PIDs limit: pids_limit ({pids_limit}) and "
|
||||
f"deploy.resources.limits.pids ({deploy_pids}) must be the same"
|
||||
)
|
||||
|
||||
final_pids_limit = pids_limit if pids_limit is not None else deploy_pids
|
||||
if final_pids_limit is not None:
|
||||
podman_args.extend(["--pids-limit", str(final_pids_limit)])
|
||||
|
||||
|
||||
def port_dict_to_str(port_desc):
|
||||
# NOTE: `mode: host|ingress` is ignored
|
||||
@ -833,6 +849,13 @@ def get_network_create_args(net_desc, proj_name, net_name):
|
||||
ipam_config_ls = ipam.get("config", [])
|
||||
if net_desc.get("enable_ipv6"):
|
||||
args.append("--ipv6")
|
||||
if net_desc.get("x-podman.disable_dns"):
|
||||
args.append("--disable-dns")
|
||||
if net_desc.get("x-podman.dns"):
|
||||
args.extend((
|
||||
"--dns",
|
||||
",".join(norm_as_list(net_desc.get("x-podman.dns"))),
|
||||
))
|
||||
|
||||
if isinstance(ipam_config_ls, dict):
|
||||
ipam_config_ls = [ipam_config_ls]
|
||||
@ -982,6 +1005,7 @@ def get_net_args_from_networks(compose, cnt):
|
||||
default_net_name = default_network_name_for_project(compose, net_, is_ext)
|
||||
net_name = ext_desc.get("name") or net_desc.get("name") or default_net_name
|
||||
|
||||
interface_name = net_config_.get("x-podman.interface_name")
|
||||
ipv4 = net_config_.get("ipv4_address")
|
||||
ipv6 = net_config_.get("ipv6_address")
|
||||
# Note: mac_address is supported by compose spec now, and x-podman.mac_address
|
||||
@ -999,6 +1023,8 @@ def get_net_args_from_networks(compose, cnt):
|
||||
mac_address = None
|
||||
|
||||
net_options = []
|
||||
if interface_name:
|
||||
net_options.append(f"interface_name={interface_name}")
|
||||
if ipv4:
|
||||
net_options.append(f"ip={ipv4}")
|
||||
if ipv6:
|
||||
@ -1020,7 +1046,7 @@ def get_net_args_from_networks(compose, cnt):
|
||||
return net_args
|
||||
|
||||
|
||||
async def container_to_args(compose, cnt, detached=True):
|
||||
async def container_to_args(compose, cnt, detached=True, no_deps=False):
|
||||
# TODO: double check -e , --add-host, -v, --read-only
|
||||
dirname = compose.dirname
|
||||
pod = cnt.get("pod", "")
|
||||
@ -1035,7 +1061,7 @@ async def container_to_args(compose, cnt, detached=True):
|
||||
deps = []
|
||||
for dep_srv in cnt.get("_deps", []):
|
||||
deps.extend(compose.container_names_by_service.get(dep_srv.name, []))
|
||||
if deps:
|
||||
if deps and not no_deps:
|
||||
deps_csv = ",".join(deps)
|
||||
podman_args.append(f"--requires={deps_csv}")
|
||||
sec = norm_as_list(cnt.get("security_opt"))
|
||||
@ -1179,6 +1205,10 @@ async def container_to_args(compose, cnt, detached=True):
|
||||
if cnt.get("runtime"):
|
||||
podman_args.extend(["--runtime", cnt["runtime"]])
|
||||
|
||||
cpuset = cnt.get("cpuset")
|
||||
if cpuset is not None:
|
||||
podman_args.extend(["--cpuset-cpus", cpuset])
|
||||
|
||||
# WIP: healthchecks are still work in progress
|
||||
healthcheck = cnt.get("healthcheck", {})
|
||||
if not isinstance(healthcheck, dict):
|
||||
@ -1289,7 +1319,8 @@ class ServiceDependencyCondition(Enum):
|
||||
try:
|
||||
return docker_to_podman_cond[value]
|
||||
except KeyError:
|
||||
raise ValueError(f"Value '{value}' is not a valid condition for a service dependency") # pylint: disable=raise-missing-from
|
||||
# pylint: disable-next=raise-missing-from
|
||||
raise ValueError(f"Value '{value}' is not a valid condition for a service dependency")
|
||||
|
||||
|
||||
class ServiceDependency:
|
||||
@ -1376,6 +1407,57 @@ def flat_deps(services, with_extends=False):
|
||||
rec_deps(services, name)
|
||||
|
||||
|
||||
###################
|
||||
# Override and reset tags
|
||||
###################
|
||||
|
||||
|
||||
class OverrideTag(yaml.YAMLObject):
|
||||
yaml_dumper = yaml.Dumper
|
||||
yaml_loader = yaml.SafeLoader
|
||||
yaml_tag = '!override'
|
||||
|
||||
def __init__(self, value):
|
||||
if len(value) > 0 and isinstance(value[0], tuple):
|
||||
self.value = {}
|
||||
# item is a tuple representing service's lower level key and value
|
||||
for item in value:
|
||||
# value can actually be a list, then all the elements from the list have to be
|
||||
# collected
|
||||
if isinstance(item[1].value, list):
|
||||
self.value[item[0].value] = [item.value for item in item[1].value]
|
||||
else:
|
||||
self.value[item[0].value] = item[1].value
|
||||
else:
|
||||
self.value = [item.value for item in value]
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, loader, node):
|
||||
return OverrideTag(node.value)
|
||||
|
||||
@classmethod
|
||||
def to_yaml(cls, dumper, data):
|
||||
return dumper.represent_scalar(cls.yaml_tag, data.value)
|
||||
|
||||
|
||||
class ResetTag(yaml.YAMLObject):
|
||||
yaml_dumper = yaml.Dumper
|
||||
yaml_loader = yaml.SafeLoader
|
||||
yaml_tag = '!reset'
|
||||
|
||||
@classmethod
|
||||
def to_json(cls):
|
||||
return cls.yaml_tag
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, loader, node):
|
||||
return ResetTag()
|
||||
|
||||
@classmethod
|
||||
def to_yaml(cls, dumper, data):
|
||||
return dumper.represent_scalar(cls.yaml_tag, '')
|
||||
|
||||
|
||||
async def wait_with_timeout(coro, timeout):
|
||||
"""
|
||||
Asynchronously waits for the given coroutine to complete with a timeout.
|
||||
@ -1454,10 +1536,8 @@ class Podman:
|
||||
chunk = await self._readchunk(reader)
|
||||
parts = chunk.split(b"\n")
|
||||
|
||||
# Iff parts ends with '', the last part is a incomplete line;
|
||||
# The rest are complete lines
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
# Iff part is last and non-empty, we leave an ongoing line to be completed later
|
||||
if i < len(parts) - 1:
|
||||
_formatted_print_with_nl(part.decode())
|
||||
line_ongoing = False
|
||||
@ -1465,7 +1545,8 @@ class Podman:
|
||||
_formatted_print_without_nl(part.decode())
|
||||
line_ongoing = True
|
||||
if line_ongoing:
|
||||
print(file=sink, end="\n") # End the unfinished line
|
||||
# Make sure the last line ends with EOL
|
||||
print(file=sink, end="\n")
|
||||
|
||||
def exec(
|
||||
self,
|
||||
@ -1499,7 +1580,10 @@ class Podman:
|
||||
|
||||
if log_formatter is not None:
|
||||
p = await asyncio.create_subprocess_exec(
|
||||
*cmd_ls, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
*cmd_ls,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
close_fds=False,
|
||||
) # pylint: disable=consider-using-with
|
||||
|
||||
# This is hacky to make the tasks not get garbage collected
|
||||
@ -1517,7 +1601,7 @@ class Podman:
|
||||
err_t.add_done_callback(task_reference.discard)
|
||||
|
||||
else:
|
||||
p = await asyncio.create_subprocess_exec(*cmd_ls) # pylint: disable=consider-using-with
|
||||
p = await asyncio.create_subprocess_exec(*cmd_ls, close_fds=False) # pylint: disable=consider-using-with
|
||||
|
||||
try:
|
||||
exit_code = await p.wait()
|
||||
@ -1572,6 +1656,12 @@ class Podman:
|
||||
|
||||
|
||||
def normalize_service(service, sub_dir=""):
|
||||
if isinstance(service, ResetTag):
|
||||
return service
|
||||
|
||||
if isinstance(service, OverrideTag):
|
||||
service = service.value
|
||||
|
||||
if "build" in service:
|
||||
build = service["build"]
|
||||
if isinstance(build, str):
|
||||
@ -1594,6 +1684,9 @@ def normalize_service(service, sub_dir=""):
|
||||
for k, v in build["additional_contexts"].items():
|
||||
new_additional_contexts.append(f"{k}={v}")
|
||||
build["additional_contexts"] = new_additional_contexts
|
||||
if "build" in service and "args" in service["build"]:
|
||||
if isinstance(build["args"], dict):
|
||||
build["args"] = norm_as_list(build["args"])
|
||||
for key in ("command", "entrypoint"):
|
||||
if key in service:
|
||||
if isinstance(service[key], str):
|
||||
@ -1647,6 +1740,8 @@ def normalize_service_final(service: dict, project_dir: str) -> dict:
|
||||
if "build" in service:
|
||||
build = service["build"]
|
||||
context = build if isinstance(build, str) else build.get("context", ".")
|
||||
|
||||
if not is_path_git_url(context):
|
||||
context = os.path.normpath(os.path.join(project_dir, context))
|
||||
if not isinstance(service["build"], dict):
|
||||
service["build"] = {}
|
||||
@ -1670,6 +1765,8 @@ def rec_merge_one(target, source):
|
||||
update target from source recursively
|
||||
"""
|
||||
done = set()
|
||||
remove = set()
|
||||
|
||||
for key, value in source.items():
|
||||
if key in target:
|
||||
continue
|
||||
@ -1679,15 +1776,37 @@ def rec_merge_one(target, source):
|
||||
if key in done:
|
||||
continue
|
||||
if key not in source:
|
||||
if isinstance(value, ResetTag):
|
||||
log("INFO: Unneeded !reset found for [{key}]")
|
||||
remove.add(key)
|
||||
|
||||
if isinstance(value, OverrideTag):
|
||||
log("INFO: Unneeded !override found for [{key}] with value '{value}'")
|
||||
target[key] = clone(value.value)
|
||||
|
||||
continue
|
||||
|
||||
value2 = source[key]
|
||||
|
||||
if isinstance(value, ResetTag) or isinstance(value2, ResetTag):
|
||||
remove.add(key)
|
||||
continue
|
||||
|
||||
if isinstance(value, OverrideTag) or isinstance(value2, OverrideTag):
|
||||
target[key] = (
|
||||
clone(value.value) if isinstance(value, OverrideTag) else clone(value2.value)
|
||||
)
|
||||
continue
|
||||
|
||||
if key in ("command", "entrypoint"):
|
||||
target[key] = clone(value2)
|
||||
continue
|
||||
|
||||
if not 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}")
|
||||
|
||||
if is_list(value2):
|
||||
if key == "volumes":
|
||||
# clean duplicate mount targets
|
||||
@ -1704,6 +1823,10 @@ def rec_merge_one(target, source):
|
||||
rec_merge_one(value, value2)
|
||||
else:
|
||||
target[key] = value2
|
||||
|
||||
for key in remove:
|
||||
del target[key]
|
||||
|
||||
return target
|
||||
|
||||
|
||||
@ -1868,6 +1991,15 @@ class PodmanCompose:
|
||||
# otherwise use `in_pod` value provided by command line
|
||||
return self.global_args.in_pod_bool
|
||||
|
||||
def resolve_pod_args(self):
|
||||
# Priorities:
|
||||
# - Command line --pod-args
|
||||
# - docker-compose.yml x-podman.pod_args
|
||||
# - Default value
|
||||
if self.global_args.pod_args is not None:
|
||||
return shlex.split(self.global_args.pod_args)
|
||||
return self.x_podman.get("pod_args", ["--infra=false", "--share="])
|
||||
|
||||
def _parse_compose_file(self):
|
||||
args = self.global_args
|
||||
# cmd = args.command
|
||||
@ -1918,9 +2050,6 @@ class PodmanCompose:
|
||||
dotenv_path = os.path.realpath(args.env_file)
|
||||
dotenv_dict.update(dotenv_to_dict(dotenv_path))
|
||||
|
||||
# TODO: remove next line
|
||||
os.chdir(dirname)
|
||||
|
||||
os.environ.update({
|
||||
key: value for key, value in dotenv_dict.items() if key.startswith("PODMAN_")
|
||||
})
|
||||
@ -1961,12 +2090,41 @@ class PodmanCompose:
|
||||
sys.exit(1)
|
||||
content = normalize(content)
|
||||
# log(filename, json.dumps(content, indent = 2))
|
||||
|
||||
# See also https://docs.docker.com/compose/how-tos/project-name/#set-a-project-name
|
||||
# **project_name** is initialized to the argument of the `-p` command line flag.
|
||||
if not project_name:
|
||||
project_name = self.environ.get("COMPOSE_PROJECT_NAME")
|
||||
if not project_name:
|
||||
project_name = content.get("name")
|
||||
if not project_name:
|
||||
project_name = dir_basename.lower()
|
||||
# More strict then actually needed for simplicity:
|
||||
# podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]*
|
||||
project_name_normalized = norm_re.sub("", project_name)
|
||||
if not project_name_normalized:
|
||||
raise RuntimeError(f"Project name [{project_name}] normalized to empty")
|
||||
project_name = project_name_normalized
|
||||
|
||||
self.project_name = project_name
|
||||
self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name})
|
||||
|
||||
content = rec_subs(content, self.environ)
|
||||
if isinstance(services := content.get('services'), dict):
|
||||
for service in services.values():
|
||||
if not isinstance(service, OverrideTag) and not isinstance(service, ResetTag):
|
||||
if 'extends' in service and (
|
||||
service_file := service['extends'].get('file')
|
||||
):
|
||||
service['extends']['file'] = os.path.join(
|
||||
os.path.dirname(filename), service_file
|
||||
)
|
||||
|
||||
rec_merge(compose, content)
|
||||
# If `include` is used, append included files to files
|
||||
include = compose.get("include")
|
||||
if include:
|
||||
files.extend(include)
|
||||
files.extend([os.path.join(os.path.dirname(filename), i) for i in include])
|
||||
# As compose obj is updated and tested with every loop, not deleting `include`
|
||||
# from it, results in it being tested again and again, original values for
|
||||
# `include` be appended to `files`, and, included files be processed for ever.
|
||||
@ -1986,19 +2144,6 @@ class PodmanCompose:
|
||||
log.debug(" ** merged:\n%s", json.dumps(compose, indent=2))
|
||||
# ver = compose.get('version')
|
||||
|
||||
if not project_name:
|
||||
project_name = compose.get("name")
|
||||
if project_name is None:
|
||||
# More strict then actually needed for simplicity:
|
||||
# podman requires [a-zA-Z0-9][a-zA-Z0-9_.-]*
|
||||
project_name = self.environ.get("COMPOSE_PROJECT_NAME", 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
|
||||
self.environ.update({"COMPOSE_PROJECT_NAME": self.project_name})
|
||||
|
||||
services = compose.get("services")
|
||||
if services is None:
|
||||
services = {}
|
||||
@ -2069,6 +2214,22 @@ class PodmanCompose:
|
||||
container_names_by_service = {}
|
||||
self.services = services
|
||||
for service_name, service_desc in services.items():
|
||||
replicas = 1
|
||||
if "scale" in args and args.scale is not None:
|
||||
# Check `--scale` args from CLI command
|
||||
scale_args = args.scale.split('=')
|
||||
if service_name == scale_args[0]:
|
||||
replicas = try_int(scale_args[1], fallback=1)
|
||||
elif "scale" in service_desc:
|
||||
# Check `scale` value from compose yaml file
|
||||
replicas = try_int(service_desc.get("scale"), fallback=1)
|
||||
elif (
|
||||
"deploy" in service_desc
|
||||
and "replicas" in service_desc.get("deploy", {})
|
||||
and "replicated" == service_desc.get("deploy", {}).get("mode", '')
|
||||
):
|
||||
# Check `deploy: replicas:` value from compose yaml file
|
||||
# Note: All conditions are necessary to handle case
|
||||
replicas = try_int(service_desc.get("deploy", {}).get("replicas"), fallback=1)
|
||||
|
||||
container_names_by_service[service_name] = []
|
||||
@ -2122,6 +2283,7 @@ class PodmanCompose:
|
||||
self.x_podman = compose.get("x-podman", {})
|
||||
|
||||
args.in_pod_bool = self.resolve_in_pod()
|
||||
args.pod_arg_list = self.resolve_pod_args()
|
||||
pods, containers = transform(args, project_name, given_containers)
|
||||
self.pods = pods
|
||||
self.containers = containers
|
||||
@ -2200,7 +2362,7 @@ class PodmanCompose:
|
||||
help="custom arguments to be passed to `podman pod`",
|
||||
metavar="pod_args",
|
||||
type=str,
|
||||
default="--infra=false --share=",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--env-file",
|
||||
@ -2465,12 +2627,38 @@ async def compose_push(compose, args):
|
||||
await compose.podman.run([], "push", [cnt["image"]])
|
||||
|
||||
|
||||
def container_to_build_args(compose, cnt, args, path_exists):
|
||||
def is_path_git_url(path):
|
||||
r = urllib.parse.urlparse(path)
|
||||
return r.scheme == 'git' or r.path.endswith('.git')
|
||||
|
||||
|
||||
def container_to_build_args(compose, cnt, args, path_exists, cleanup_callbacks=None):
|
||||
build_desc = cnt["build"]
|
||||
if not hasattr(build_desc, "items"):
|
||||
build_desc = {"context": build_desc}
|
||||
ctx = build_desc.get("context", ".")
|
||||
dockerfile = build_desc.get("dockerfile")
|
||||
dockerfile = build_desc.get("dockerfile", "")
|
||||
dockerfile_inline = build_desc.get("dockerfile_inline")
|
||||
if dockerfile_inline is not None:
|
||||
dockerfile_inline = str(dockerfile_inline)
|
||||
# Error if both `dockerfile_inline` and `dockerfile` are set
|
||||
if dockerfile and dockerfile_inline:
|
||||
raise OSError("dockerfile_inline and dockerfile can't be used simultaneously")
|
||||
dockerfile = tempfile.NamedTemporaryFile(delete=False, suffix=".containerfile")
|
||||
dockerfile.write(dockerfile_inline.encode())
|
||||
dockerfile.close()
|
||||
dockerfile = dockerfile.name
|
||||
|
||||
def cleanup_temp_dockfile():
|
||||
if os.path.exists(dockerfile):
|
||||
os.remove(dockerfile)
|
||||
|
||||
if cleanup_callbacks is not None:
|
||||
list.append(cleanup_callbacks, cleanup_temp_dockfile)
|
||||
|
||||
build_args = []
|
||||
|
||||
if not is_path_git_url(ctx):
|
||||
if dockerfile:
|
||||
dockerfile = os.path.join(ctx, dockerfile)
|
||||
else:
|
||||
@ -2486,9 +2674,16 @@ def container_to_build_args(compose, cnt, args, path_exists):
|
||||
dockerfile = os.path.join(ctx, dockerfile)
|
||||
if path_exists(dockerfile):
|
||||
break
|
||||
if not path_exists(dockerfile):
|
||||
raise OSError("Dockerfile not found in " + ctx)
|
||||
build_args = ["-f", dockerfile, "-t", cnt["image"]]
|
||||
|
||||
if path_exists(dockerfile):
|
||||
# normalize dockerfile path, as the user could have provided unpredictable file formats
|
||||
dockerfile = os.path.normpath(os.path.join(ctx, dockerfile))
|
||||
build_args.extend(["-f", dockerfile])
|
||||
else:
|
||||
raise OSError(f"Dockerfile not found in {ctx}")
|
||||
|
||||
build_args.extend(["-t", cnt["image"]])
|
||||
|
||||
if "platform" in cnt:
|
||||
build_args.extend(["--platform", cnt["platform"]])
|
||||
for secret in build_desc.get("secrets", []):
|
||||
@ -2540,8 +2735,13 @@ async def build_one(compose, args, cnt):
|
||||
if img_id:
|
||||
return None
|
||||
|
||||
build_args = container_to_build_args(compose, cnt, args, os.path.exists)
|
||||
cleanup_callbacks = []
|
||||
build_args = container_to_build_args(
|
||||
compose, cnt, args, os.path.exists, cleanup_callbacks=cleanup_callbacks
|
||||
)
|
||||
status = await compose.podman.run([], "build", build_args)
|
||||
for c in cleanup_callbacks:
|
||||
c()
|
||||
return status
|
||||
|
||||
|
||||
@ -2563,7 +2763,7 @@ async def compose_build(compose, args):
|
||||
status = 0
|
||||
for t in asyncio.as_completed(tasks):
|
||||
s = await t
|
||||
if s is not None:
|
||||
if s is not None and s != 0:
|
||||
status = s
|
||||
|
||||
return status
|
||||
@ -2582,9 +2782,7 @@ async def create_pods(compose, args): # pylint: disable=unused-argument
|
||||
podman_args = [
|
||||
"create",
|
||||
"--name=" + pod["name"],
|
||||
]
|
||||
if args.pod_args:
|
||||
podman_args.extend(shlex.split(args.pod_args))
|
||||
] + args.pod_arg_list
|
||||
# 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", [])
|
||||
@ -2600,6 +2798,8 @@ def get_excluded(compose, args):
|
||||
if args.services:
|
||||
excluded = set(compose.services)
|
||||
for service in args.services:
|
||||
# we need 'getattr' as compose_down_parse dose not configure 'no_deps'
|
||||
if service in compose.services and not getattr(args, "no_deps", False):
|
||||
excluded -= set(x.name for x in compose.services[service]["_deps"])
|
||||
excluded.discard(service)
|
||||
log.debug("** excluding: %s", excluded)
|
||||
@ -2615,6 +2815,18 @@ async def check_dep_conditions(compose: PodmanCompose, deps: set) -> None:
|
||||
deps_cd = []
|
||||
for d in deps:
|
||||
if d.condition == condition:
|
||||
if (
|
||||
d.condition
|
||||
in (ServiceDependencyCondition.HEALTHY, ServiceDependencyCondition.UNHEALTHY)
|
||||
) and strverscmp_lt(compose.podman_version, "4.6.0"):
|
||||
log.warning(
|
||||
"Ignored %s condition check due to podman %s doesn't support %s!",
|
||||
d.name,
|
||||
compose.podman_version,
|
||||
condition.value,
|
||||
)
|
||||
continue
|
||||
|
||||
deps_cd.extend(compose.container_names_by_service[d.name])
|
||||
|
||||
if deps_cd:
|
||||
@ -2658,14 +2870,24 @@ async def run_container(
|
||||
return await compose.podman.run(*command, log_formatter=log_formatter)
|
||||
|
||||
|
||||
def deps_from_container(args, cnt):
|
||||
if args.no_deps:
|
||||
return set()
|
||||
return cnt['_deps']
|
||||
|
||||
|
||||
@cmd_run(podman_compose, "up", "Create and start the entire stack or some of its services")
|
||||
async def compose_up(compose: PodmanCompose, 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__)
|
||||
if await compose.commands["build"](compose, build_args) != 0:
|
||||
build_exit_code = await compose.commands["build"](compose, build_args)
|
||||
if build_exit_code != 0:
|
||||
log.error("Build command failed")
|
||||
if not args.dry_run:
|
||||
return build_exit_code
|
||||
|
||||
hashes = (
|
||||
(
|
||||
@ -2685,26 +2907,37 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
.splitlines()
|
||||
)
|
||||
diff_hashes = [i for i in hashes if i and i != compose.yaml_hash]
|
||||
if args.force_recreate or len(diff_hashes):
|
||||
if (args.force_recreate and len(hashes) > 0) or len(diff_hashes):
|
||||
log.info("recreating: ...")
|
||||
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False))
|
||||
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
|
||||
await compose.commands["down"](compose, down_args)
|
||||
log.info("recreating: done\n\n")
|
||||
# args.no_recreate disables check for changes (which is not implemented)
|
||||
|
||||
podman_command = "run" if args.detach and not args.no_start else "create"
|
||||
|
||||
await create_pods(compose, args)
|
||||
exit_code = 0
|
||||
for cnt in compose.containers:
|
||||
if cnt["_service"] in excluded:
|
||||
log.debug("** skipping: %s", cnt["name"])
|
||||
continue
|
||||
podman_args = await container_to_args(compose, cnt, detached=args.detach)
|
||||
subproc = await compose.podman.run([], podman_command, podman_args)
|
||||
if podman_command == "run" and subproc is not None:
|
||||
await run_container(compose, cnt["name"], cnt["_deps"], ([], "start", [cnt["name"]]))
|
||||
if args.no_start or args.detach or args.dry_run:
|
||||
return
|
||||
podman_args = await container_to_args(compose, cnt, detached=False, no_deps=args.no_deps)
|
||||
subproc_exit_code = await compose.podman.run([], "create", podman_args)
|
||||
if subproc_exit_code is not None and subproc_exit_code != 0:
|
||||
exit_code = subproc_exit_code
|
||||
|
||||
if not args.no_start and args.detach and subproc_exit_code is not None:
|
||||
container_exit_code = await run_container(
|
||||
compose, cnt["name"], deps_from_container(args, cnt), ([], "start", [cnt["name"]])
|
||||
)
|
||||
|
||||
if container_exit_code is not None and container_exit_code != 0:
|
||||
exit_code = container_exit_code
|
||||
|
||||
if args.dry_run:
|
||||
return None
|
||||
if args.no_start or args.detach:
|
||||
return exit_code
|
||||
|
||||
# TODO: handle already existing
|
||||
# TODO: if error creating do not enter loop
|
||||
# TODO: colors if sys.stdout.isatty()
|
||||
@ -2719,8 +2952,21 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
|
||||
tasks = set()
|
||||
|
||||
async def handle_sigint():
|
||||
log.info("Caught SIGINT or Ctrl+C, shutting down...")
|
||||
try:
|
||||
log.info("Shutting down gracefully, please wait...")
|
||||
down_args = argparse.Namespace(**dict(args.__dict__, volumes=False, rmi=None))
|
||||
await compose.commands["down"](compose, down_args)
|
||||
except Exception as e:
|
||||
log.error("Error during shutdown: %s", e)
|
||||
finally:
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
if sys.platform != 'win32':
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.add_signal_handler(signal.SIGINT, lambda: [t.cancel("User exit") for t in tasks])
|
||||
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(handle_sigint()))
|
||||
|
||||
for i, cnt in enumerate(compose.containers):
|
||||
# Add colored service prefix to output by piping output through sed
|
||||
@ -2737,7 +2983,7 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
run_container(
|
||||
compose,
|
||||
cnt["name"],
|
||||
cnt["_deps"],
|
||||
deps_from_container(args, cnt),
|
||||
([], "start", ["-a", cnt["name"]]),
|
||||
log_formatter=log_formatter,
|
||||
),
|
||||
@ -2755,9 +3001,20 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
|
||||
exit_code = 0
|
||||
exiting = False
|
||||
first_failed_task = None
|
||||
|
||||
while tasks:
|
||||
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||
if args.abort_on_container_exit:
|
||||
|
||||
if args.abort_on_container_failure and first_failed_task is None:
|
||||
# Generally a single returned item when using asyncio.FIRST_COMPLETED, but that's not
|
||||
# guaranteed. If multiple tasks finish at the exact same time the choice of which
|
||||
# finished "first" is arbitrary
|
||||
for t in done:
|
||||
if t.result() != 0:
|
||||
first_failed_task = t
|
||||
|
||||
if args.abort_on_container_exit or first_failed_task:
|
||||
if not exiting:
|
||||
# If 2 containers exit at the exact same time, the cancellation of the other ones
|
||||
# cause the status to overwrite. Sleeping for 1 seems to fix this and make it match
|
||||
@ -2768,6 +3025,11 @@ async def compose_up(compose: PodmanCompose, args):
|
||||
t.cancel()
|
||||
t: Task
|
||||
exiting = True
|
||||
if first_failed_task:
|
||||
# Matches docker-compose behaviour, where the exit code of the task that triggered
|
||||
# the cancellation is always propagated when aborting on failure
|
||||
exit_code = first_failed_task.result()
|
||||
else:
|
||||
for t in done:
|
||||
if t.get_name() == exit_code_from:
|
||||
exit_code = t.result()
|
||||
@ -2799,7 +3061,6 @@ async def compose_down(compose: PodmanCompose, args):
|
||||
containers = list(reversed(compose.containers))
|
||||
|
||||
down_tasks = []
|
||||
|
||||
for cnt in containers:
|
||||
if cnt["_service"] in excluded:
|
||||
continue
|
||||
@ -2820,8 +3081,10 @@ async def compose_down(compose: PodmanCompose, args):
|
||||
if cnt["_service"] in excluded:
|
||||
continue
|
||||
await compose.podman.run([], "rm", [cnt["name"]])
|
||||
|
||||
orphaned_images = set()
|
||||
if args.remove_orphans:
|
||||
names = (
|
||||
orphaned_containers = (
|
||||
(
|
||||
await compose.podman.output(
|
||||
[],
|
||||
@ -2831,13 +3094,15 @@ async def compose_down(compose: PodmanCompose, args):
|
||||
f"label=io.podman.compose.project={compose.project_name}",
|
||||
"-a",
|
||||
"--format",
|
||||
"{{ .Names }}",
|
||||
"{{ .Image }} {{ .Names }}",
|
||||
],
|
||||
)
|
||||
)
|
||||
.decode("utf-8")
|
||||
.splitlines()
|
||||
)
|
||||
orphaned_images = {item.split()[0] for item in orphaned_containers}
|
||||
names = {item.split()[1] for item in orphaned_containers}
|
||||
for name in names:
|
||||
await compose.podman.run([], "stop", [*podman_args, name])
|
||||
for name in names:
|
||||
@ -2853,6 +3118,17 @@ async def compose_down(compose: PodmanCompose, args):
|
||||
if volume_name in vol_names_to_keep:
|
||||
continue
|
||||
await compose.podman.run([], "volume", ["rm", volume_name])
|
||||
if args.rmi:
|
||||
images_to_remove = set()
|
||||
for cnt in containers:
|
||||
if cnt["_service"] in excluded:
|
||||
continue
|
||||
if args.rmi == "local" and not is_local(cnt):
|
||||
continue
|
||||
images_to_remove.add(cnt["image"])
|
||||
images_to_remove.update(orphaned_images)
|
||||
log.debug("images to remove: %s", images_to_remove)
|
||||
await compose.podman.run([], "rmi", ["--ignore", "--force"] + list(images_to_remove))
|
||||
|
||||
if excluded:
|
||||
return
|
||||
@ -2915,7 +3191,7 @@ async def compose_run(compose, args):
|
||||
|
||||
compose_run_update_container_from_args(compose, cnt, args)
|
||||
# run podman
|
||||
podman_args = await container_to_args(compose, cnt, args.detach)
|
||||
podman_args = await container_to_args(compose, cnt, args.detach, args.no_deps)
|
||||
if not args.detach:
|
||||
podman_args.insert(1, "-i")
|
||||
if args.rm:
|
||||
@ -3077,37 +3353,22 @@ async def compose_logs(compose, args):
|
||||
async def compose_config(compose, args):
|
||||
if args.services:
|
||||
for service in compose.services:
|
||||
if not args.quiet:
|
||||
print(service)
|
||||
return
|
||||
if not args.quiet:
|
||||
print(compose.merged_yaml)
|
||||
|
||||
|
||||
@cmd_run(podman_compose, "port", "Prints the public port for a port binding.")
|
||||
async def compose_port(compose, args):
|
||||
# TODO - deal with pod index
|
||||
compose.assert_services(args.service)
|
||||
containers = compose.container_names_by_service[args.service]
|
||||
container_ports = list(
|
||||
itertools.chain(*(compose.container_by_name[c]["ports"] for c in containers))
|
||||
)
|
||||
|
||||
def _published_target(port_string):
|
||||
published, target = port_string.split(":")[-2:]
|
||||
return int(published), int(target)
|
||||
|
||||
select_udp = args.protocol == "udp"
|
||||
published, target = None, None
|
||||
for p in container_ports:
|
||||
is_udp = p[-4:] == "/udp"
|
||||
|
||||
if select_udp and is_udp:
|
||||
published, target = _published_target(p[-4:])
|
||||
if not select_udp and not is_udp:
|
||||
published, target = _published_target(p)
|
||||
|
||||
if target == args.private_port:
|
||||
print(published)
|
||||
return
|
||||
output = await compose.podman.output([], "inspect", [containers[args.index - 1]])
|
||||
inspect_json = json.loads(output.decode("utf-8"))
|
||||
private_port = str(args.private_port) + "/" + args.protocol
|
||||
host_port = inspect_json[0]["NetworkSettings"]["Ports"][private_port][0]["HostPort"]
|
||||
print(host_port)
|
||||
|
||||
|
||||
@cmd_run(podman_compose, "pause", "Pause all running containers")
|
||||
@ -3259,7 +3520,7 @@ def compose_up_parse(parser):
|
||||
"--detach",
|
||||
action="store_true",
|
||||
help="Detached mode: Run container in the background, print new container name. \
|
||||
Incompatible with --abort-on-container-exit.",
|
||||
Incompatible with --abort-on-container-exit and --abort-on-container-failure.",
|
||||
)
|
||||
parser.add_argument("--no-color", action="store_true", help="Produce monochrome output.")
|
||||
parser.add_argument(
|
||||
@ -3300,7 +3561,14 @@ def compose_up_parse(parser):
|
||||
parser.add_argument(
|
||||
"--abort-on-container-exit",
|
||||
action="store_true",
|
||||
help="Stops all containers if any container was stopped. Incompatible with -d.",
|
||||
help="Stops all containers if any container was stopped. Incompatible with -d and "
|
||||
"--abort-on-container-failure.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--abort-on-container-failure",
|
||||
action="store_true",
|
||||
help="Stops all containers if any container stops with a non-zero exit code. Incompatible "
|
||||
"with -d and --abort-on-container-exit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
@ -3321,12 +3589,13 @@ def compose_up_parse(parser):
|
||||
action="store_true",
|
||||
help="Remove containers for services not defined in the Compose file.",
|
||||
)
|
||||
# `--scale` argument needs to store as single value and not append,
|
||||
# as multiple scale values could be confusing.
|
||||
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.",
|
||||
help="Scale SERVICE to NUM instances. "
|
||||
"Overrides the `scale` setting in the Compose file if present.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exit-code-from",
|
||||
@ -3353,6 +3622,15 @@ def compose_down_parse(parser):
|
||||
action="store_true",
|
||||
help="Remove containers for services not defined in the Compose file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rmi",
|
||||
type=str,
|
||||
nargs="?",
|
||||
const="all",
|
||||
choices=["local", "all"],
|
||||
help="Remove images used by services. `local` remove only images that don't have a "
|
||||
"custom tag. (`local` or `all`)",
|
||||
)
|
||||
|
||||
|
||||
@cmd_parse(podman_compose, "run")
|
||||
@ -3613,6 +3891,12 @@ def compose_config_parse(parser):
|
||||
parser.add_argument(
|
||||
"--services", help="Print the service names, one per line.", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
help="Do not print config, only parse.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
|
||||
@cmd_parse(podman_compose, "port")
|
||||
|
@ -7,3 +7,5 @@ version = attr: podman_compose.__version__
|
||||
[flake8]
|
||||
# The GitHub editor is 127 chars wide
|
||||
max-line-length=127
|
||||
# These are not being followed yet
|
||||
ignore=E222,E231,E272,E713,W503
|
@ -31,4 +31,4 @@ python-dotenv==1.0.1
|
||||
PyYAML==6.0.1
|
||||
requests
|
||||
tomlkit==0.12.4
|
||||
virtualenv==20.25.1
|
||||
virtualenv==20.26.6
|
||||
|
0
tests/integration/abort/__init__.py
Normal file
0
tests/integration/abort/__init__.py
Normal file
11
tests/integration/abort/docker-compose-fail-first.yaml
Normal file
11
tests/integration/abort/docker-compose-fail-first.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
sh1:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"]
|
||||
sh2:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]
|
||||
sh3:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]
|
11
tests/integration/abort/docker-compose-fail-none.yaml
Normal file
11
tests/integration/abort/docker-compose-fail-none.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
sh1:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
|
||||
sh2:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]
|
||||
sh3:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]
|
11
tests/integration/abort/docker-compose-fail-second.yaml
Normal file
11
tests/integration/abort/docker-compose-fail-second.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
sh1:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
|
||||
sh2:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 1"]
|
||||
sh3:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3; exit 0"]
|
@ -0,0 +1,11 @@
|
||||
version: "3"
|
||||
services:
|
||||
sh1:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 1"]
|
||||
sh2:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 1; exit 0"]
|
||||
sh3:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 2; exit 0"]
|
46
tests/integration/abort/test_podman_compose_abort.py
Normal file
46
tests/integration/abort/test_podman_compose_abort.py
Normal file
@ -0,0 +1,46 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path(failure_order):
|
||||
return os.path.join(test_path(), "abort", f"docker-compose-fail-{failure_order}.yaml")
|
||||
|
||||
|
||||
class TestComposeAbort(unittest.TestCase, RunSubprocessMixin):
|
||||
@parameterized.expand([
|
||||
("exit", "first", 0),
|
||||
("failure", "first", 1),
|
||||
("exit", "second", 0),
|
||||
("failure", "second", 1),
|
||||
("exit", "simultaneous", 0),
|
||||
("failure", "simultaneous", 1),
|
||||
("exit", "none", 0),
|
||||
("failure", "none", 0),
|
||||
])
|
||||
def test_abort(self, abort_type, failure_order, expected_exit_code):
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(
|
||||
[
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(failure_order),
|
||||
"up",
|
||||
f"--abort-on-container-{abort_type}",
|
||||
],
|
||||
expected_exit_code,
|
||||
)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(failure_order),
|
||||
"down",
|
||||
])
|
1
tests/integration/additional_contexts/__init__.py
Normal file
1
tests/integration/additional_contexts/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -7,8 +7,8 @@ import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
1
tests/integration/build/__init__.py
Normal file
1
tests/integration/build/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
1
tests/integration/build/git_url_context/__init__.py
Normal file
1
tests/integration/build/git_url_context/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
services:
|
||||
test_context:
|
||||
build:
|
||||
context: https://github.com/mokibit/test-git-url-as-context.git
|
||||
image: test-git-url-as-context
|
||||
test_context_inline:
|
||||
build: https://github.com/mokibit/test-git-url-as-context.git
|
||||
image: test-git-url-as-context-inline
|
@ -0,0 +1,55 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
from parameterized import parameterized
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
""" "Returns the path to the compose file used for this test module"""
|
||||
base_path = os.path.join(test_path(), "build/git_url_context")
|
||||
return os.path.join(base_path, "docker-compose.yml")
|
||||
|
||||
|
||||
class TestComposeBuildGitUrlAsContext(unittest.TestCase, RunSubprocessMixin):
|
||||
@parameterized.expand([
|
||||
("git_url_context_test_context_1", "data_1.txt", b'test1\r\n'),
|
||||
("git_url_context_test_context_1", "data_2.txt", b'test2\r\n'),
|
||||
("git_url_context_test_context_inline_1", "data_1.txt", b'test1\r\n'),
|
||||
("git_url_context_test_context_inline_1", "data_2.txt", b'test2\r\n'),
|
||||
])
|
||||
def test_build_git_url_as_context(self, container_name, file_name, output):
|
||||
# test if container can access specific files from git repository when git url is used as
|
||||
# a build context
|
||||
try:
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"up",
|
||||
"-d",
|
||||
])
|
||||
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"exec",
|
||||
"-ti",
|
||||
f"{container_name}",
|
||||
"sh",
|
||||
"-c",
|
||||
f"cat {file_name}",
|
||||
])
|
||||
self.assertEqual(out, output)
|
||||
finally:
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
"-t",
|
||||
"0",
|
||||
])
|
@ -5,8 +5,8 @@ import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
|
||||
|
1
tests/integration/build_fail/__init__.py
Normal file
1
tests/integration/build_fail/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
0
tests/integration/build_fail_multi/__init__.py
Normal file
0
tests/integration/build_fail_multi/__init__.py
Normal file
3
tests/integration/build_fail_multi/bad/Dockerfile
Normal file
3
tests/integration/build_fail_multi/bad/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM busybox
|
||||
|
||||
RUN false
|
8
tests/integration/build_fail_multi/docker-compose.yml
Normal file
8
tests/integration/build_fail_multi/docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
||||
version: "3"
|
||||
services:
|
||||
bad:
|
||||
build:
|
||||
context: bad
|
||||
good:
|
||||
build:
|
||||
context: good
|
3
tests/integration/build_fail_multi/good/Dockerfile
Normal file
3
tests/integration/build_fail_multi/good/Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM busybox
|
||||
#ensure that this build finishes second so that it has a chance to overwrite the return code
|
||||
RUN sleep 0.5
|
@ -0,0 +1,31 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
""" "Returns the path to the compose file used for this test module"""
|
||||
base_path = os.path.join(test_path(), "build_fail_multi")
|
||||
return os.path.join(base_path, "docker-compose.yml")
|
||||
|
||||
|
||||
class TestComposeBuildFailMulti(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_build_fail_multi(self):
|
||||
output, error = self.run_subprocess_assert_returncode(
|
||||
[
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"build",
|
||||
# prevent the successful build from being cached to ensure it runs long enough
|
||||
"--no-cache",
|
||||
],
|
||||
expected_returncode=1,
|
||||
)
|
||||
self.assertIn("RUN false", str(output))
|
||||
self.assertIn("while running runtime: exit status 1", str(error))
|
0
tests/integration/build_labels/__init__.py
Normal file
0
tests/integration/build_labels/__init__.py
Normal file
@ -5,9 +5,9 @@ import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
class TestBuildLabels(unittest.TestCase, RunSubprocessMixin):
|
||||
|
1
tests/integration/build_secrets/__init__.py
Normal file
1
tests/integration/build_secrets/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -7,8 +7,8 @@ import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
@ -9,9 +9,9 @@ import unittest
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
expected_lines = [
|
||||
"default: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFYQvN9a+toIB6jSs4zY7FMapZnHt80EKCUr/WhLwUum",
|
||||
|
1
tests/integration/default_net_behavior/__init__.py
Normal file
1
tests/integration/default_net_behavior/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -5,9 +5,9 @@ import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path(scenario):
|
1
tests/integration/deps/__init__.py
Normal file
1
tests/integration/deps/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,23 @@
|
||||
version: "3.7"
|
||||
services:
|
||||
web:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "httpd", "-f", "-h", "/etc/", "-p", "8000"]
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:8000/hosts"]
|
||||
start_period: 10s # initialization time for containers that need time to bootstrap
|
||||
interval: 10s # Time between health checks
|
||||
timeout: 5s # Time to wait for a response
|
||||
retries: 3 # Number of consecutive failures before marking as unhealthy
|
||||
sleep:
|
||||
image: nopush/podman-compose-test
|
||||
command: ["dumb-init", "/bin/busybox", "sh", "-c", "sleep 3600"]
|
||||
depends_on:
|
||||
web:
|
||||
condition: service_healthy
|
||||
tmpfs:
|
||||
- /run
|
||||
- /tmp
|
266
tests/integration/deps/test_podman_compose_deps.py
Normal file
266
tests/integration/deps/test_podman_compose_deps.py
Normal file
@ -0,0 +1,266 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import PodmanAwareRunSubprocessMixin
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import is_systemd_available
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path(suffix=""):
|
||||
return os.path.join(os.path.join(test_path(), "deps"), f"docker-compose{suffix}.yaml")
|
||||
|
||||
|
||||
class TestComposeBaseDeps(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_deps(self):
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"run",
|
||||
"--rm",
|
||||
"sleep",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"wget -O - http://web:8000/hosts",
|
||||
])
|
||||
self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output)
|
||||
self.assertIn(b"deps_web_1", output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_run_nodeps(self):
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"run",
|
||||
"--rm",
|
||||
"--no-deps",
|
||||
"sleep",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"wget -O - http://web:8000/hosts || echo Failed to connect",
|
||||
])
|
||||
self.assertNotIn(b"HTTP request sent, awaiting response... 200 OK", output)
|
||||
self.assertNotIn(b"deps_web_1", output)
|
||||
self.assertIn(b"Failed to connect", output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_up_nodeps(self):
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"up",
|
||||
"--no-deps",
|
||||
"--detach",
|
||||
"sleep",
|
||||
])
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"ps",
|
||||
])
|
||||
self.assertNotIn(b"deps_web_1", output)
|
||||
self.assertIn(b"deps_sleep_1", output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_podman_compose_run(self):
|
||||
"""
|
||||
This will test depends_on as well
|
||||
"""
|
||||
run_cmd = [
|
||||
"coverage",
|
||||
"run",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
os.path.join(test_path(), "deps", "docker-compose.yaml"),
|
||||
"run",
|
||||
"--rm",
|
||||
"sleep",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"wget -q -O - http://web:8000/hosts",
|
||||
]
|
||||
|
||||
out, _ = self.run_subprocess_assert_returncode(run_cmd)
|
||||
self.assertIn(b"127.0.0.1\tlocalhost", out)
|
||||
|
||||
# Run it again to make sure we can run it twice. I saw an issue where a second run, with
|
||||
# the container left up, would fail
|
||||
run_cmd = [
|
||||
"coverage",
|
||||
"run",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
os.path.join(test_path(), "deps", "docker-compose.yaml"),
|
||||
"run",
|
||||
"--rm",
|
||||
"sleep",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"wget -q -O - http://web:8000/hosts",
|
||||
]
|
||||
|
||||
out, _ = self.run_subprocess_assert_returncode(run_cmd)
|
||||
self.assertIn(b"127.0.0.1\tlocalhost", out)
|
||||
|
||||
# This leaves a container running. Not sure it's intended, but it matches docker-compose
|
||||
down_cmd = [
|
||||
"coverage",
|
||||
"run",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
os.path.join(test_path(), "deps", "docker-compose.yaml"),
|
||||
"down",
|
||||
]
|
||||
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
|
||||
|
||||
class TestComposeConditionalDeps(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_deps_succeeds(self):
|
||||
suffix = "-conditional-succeeds"
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(suffix),
|
||||
"run",
|
||||
"--rm",
|
||||
"sleep",
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"wget -O - http://web:8000/hosts",
|
||||
])
|
||||
self.assertIn(b"HTTP request sent, awaiting response... 200 OK", output)
|
||||
self.assertIn(b"deps_web_1", output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(suffix),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_deps_fails(self):
|
||||
suffix = "-conditional-fails"
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(suffix),
|
||||
"ps",
|
||||
])
|
||||
self.assertNotIn(b"HTTP request sent, awaiting response... 200 OK", output)
|
||||
self.assertNotIn(b"deps_web_1", output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(suffix),
|
||||
"down",
|
||||
])
|
||||
|
||||
|
||||
class TestComposeConditionalDepsHealthy(unittest.TestCase, PodmanAwareRunSubprocessMixin):
|
||||
def setUp(self):
|
||||
self.podman_version = self.retrieve_podman_version()
|
||||
|
||||
def test_up_deps_healthy(self):
|
||||
suffix = "-conditional-healthy"
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(suffix),
|
||||
"up",
|
||||
"sleep",
|
||||
"--detach",
|
||||
])
|
||||
|
||||
# Since the command `podman wait --condition=healthy` is invalid prior to 4.6.0,
|
||||
# we only validate healthy status for podman 4.6.0+, which won't be tested in the
|
||||
# CI pipeline of the podman-compose project where podman 4.3.1 is employed.
|
||||
podman_ver_major, podman_ver_minor, podman_ver_patch = self.podman_version
|
||||
if podman_ver_major >= 4 and podman_ver_minor >= 6 and podman_ver_patch >= 0:
|
||||
self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"wait",
|
||||
"--condition=running",
|
||||
"deps_web_1",
|
||||
"deps_sleep_1",
|
||||
])
|
||||
|
||||
# check both web and sleep are running
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"ps",
|
||||
"--format",
|
||||
"{{.ID}}\t{{.Names}}\t{{.Status}}\t{{.StartedAt}}",
|
||||
])
|
||||
|
||||
# extract container id of web
|
||||
decoded_out = output.decode('utf-8')
|
||||
lines = decoded_out.split("\n")
|
||||
|
||||
web_lines = [line for line in lines if "web" in line]
|
||||
self.assertTrue(web_lines)
|
||||
self.assertEqual(1, len(web_lines))
|
||||
web_cnt_id, web_cnt_name, web_cnt_status, web_cnt_started = web_lines[0].split("\t")
|
||||
self.assertNotEqual("", web_cnt_id)
|
||||
self.assertEqual("deps_web_1", web_cnt_name)
|
||||
|
||||
sleep_lines = [line for line in lines if "sleep" in line]
|
||||
self.assertTrue(sleep_lines)
|
||||
self.assertEqual(1, len(sleep_lines))
|
||||
sleep_cnt_id, sleep_cnt_name, _, sleep_cnt_started = sleep_lines[0].split("\t")
|
||||
self.assertNotEqual("", sleep_cnt_id)
|
||||
self.assertEqual("deps_sleep_1", sleep_cnt_name)
|
||||
|
||||
# When test case is executed inside container like github actions, the absence of
|
||||
# systemd prevents health check from working properly, resulting in failure to
|
||||
# transit to healthy state. As a result, we only assert the `healthy` state where
|
||||
# systemd is functioning.
|
||||
if (
|
||||
is_systemd_available()
|
||||
and podman_ver_major >= 4
|
||||
and podman_ver_minor >= 6
|
||||
and podman_ver_patch >= 0
|
||||
):
|
||||
self.assertIn("healthy", web_cnt_status)
|
||||
self.assertGreaterEqual(int(sleep_cnt_started), int(web_cnt_started))
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
1
tests/integration/env-file-tests/__init__.py
Normal file
1
tests/integration/env-file-tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_base_path():
|
||||
@ -233,7 +233,7 @@ class TestComposeEnvFile(unittest.TestCase, RunSubprocessMixin):
|
||||
[
|
||||
'ZZVAR1=This value is loaded but should be overwritten\r',
|
||||
'ZZVAR2=This value is loaded from .env in project/ directory\r',
|
||||
'ZZVAR3=$ZZVAR3\r',
|
||||
'ZZVAR3=\r',
|
||||
'',
|
||||
],
|
||||
)
|
1
tests/integration/env-tests/__init__.py
Normal file
1
tests/integration/env-tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -1,5 +1,7 @@
|
||||
version: "3"
|
||||
|
||||
name: my-project-name
|
||||
|
||||
services:
|
||||
env-test:
|
||||
image: busybox
|
||||
@ -8,3 +10,9 @@ services:
|
||||
ZZVAR1: myval1
|
||||
ZZVAR2: 2-$ZZVAR1
|
||||
ZZVAR3: 3-$ZZVAR2
|
||||
|
||||
project-name-test:
|
||||
image: busybox
|
||||
command: sh -c "echo $$PNAME"
|
||||
environment:
|
||||
PNAME: ${COMPOSE_PROJECT_NAME}
|
||||
|
89
tests/integration/env-tests/test_podman_compose_env.py
Normal file
89
tests/integration/env-tests/test_podman_compose_env.py
Normal file
@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "env-tests"), "container-compose.yml")
|
||||
|
||||
|
||||
class TestComposeEnv(unittest.TestCase, RunSubprocessMixin):
|
||||
"""Test that inline environment variable overrides environment variable from compose file."""
|
||||
|
||||
def test_env(self):
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"run",
|
||||
"-l",
|
||||
"monkey",
|
||||
"-e",
|
||||
"ZZVAR1=myval2",
|
||||
"env-test",
|
||||
])
|
||||
self.assertIn("ZZVAR1='myval2'", str(output))
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
"""
|
||||
Tests interpolation of COMPOSE_PROJECT_NAME in the podman-compose config,
|
||||
which is different from external environment variables because COMPOSE_PROJECT_NAME
|
||||
is a predefined environment variable generated from the `name` value in the top-level
|
||||
of the compose.yaml.
|
||||
|
||||
See also
|
||||
- https://docs.docker.com/reference/compose-file/interpolation/
|
||||
- https://docs.docker.com/reference/compose-file/version-and-name/#name-top-level-element
|
||||
- https://docs.docker.com/compose/how-tos/environment-variables/envvars/
|
||||
- https://github.com/compose-spec/compose-spec/blob/main/04-version-and-name.md
|
||||
"""
|
||||
|
||||
def test_project_name(self):
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"run",
|
||||
"project-name-test",
|
||||
])
|
||||
self.assertIn("my-project-name", str(output))
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_project_name_override(self):
|
||||
try:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"run",
|
||||
"-e",
|
||||
"COMPOSE_PROJECT_NAME=project-name-override",
|
||||
"project-name-test",
|
||||
])
|
||||
self.assertIn("project-name-override", str(output))
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
1
tests/integration/exit-from/__init__.py
Normal file
1
tests/integration/exit-from/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
@ -52,3 +52,16 @@ class TestComposeExitFrom(unittest.TestCase, RunSubprocessMixin):
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_podman_compose_exit_from(self):
|
||||
up_cmd = [
|
||||
"coverage",
|
||||
"run",
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
os.path.join(test_path(), "exit-from", "docker-compose.yaml"),
|
||||
"up",
|
||||
]
|
||||
|
||||
self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh1"], 1)
|
||||
self.run_subprocess_assert_returncode(up_cmd + ["--exit-code-from", "sh2"], 2)
|
1
tests/integration/extends/__init__.py
Normal file
1
tests/integration/extends/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
@ -80,18 +80,25 @@ class TestComposeExteds(unittest.TestCase, RunSubprocessMixin):
|
||||
"env1",
|
||||
])
|
||||
lines = output.decode('utf-8').split('\n')
|
||||
# HOSTNAME name is random string so is ignored in asserting
|
||||
lines = sorted([line for line in lines if not line.startswith("HOSTNAME")])
|
||||
# Test selected env variables to improve robustness
|
||||
lines = sorted([
|
||||
line
|
||||
for line in lines
|
||||
if line.startswith("BAR")
|
||||
or line.startswith("BAZ")
|
||||
or line.startswith("FOO")
|
||||
or line.startswith("HOME")
|
||||
or line.startswith("PATH")
|
||||
or line.startswith("container")
|
||||
])
|
||||
self.assertEqual(
|
||||
lines,
|
||||
[
|
||||
'',
|
||||
'BAR=local',
|
||||
'BAZ=local',
|
||||
'FOO=original',
|
||||
'HOME=/root',
|
||||
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
'TERM=xterm',
|
||||
'container=podman',
|
||||
],
|
||||
)
|
1
tests/integration/extends_w_empty_service/__init__.py
Normal file
1
tests/integration/extends_w_empty_service/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -2,10 +2,11 @@
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
@ -37,3 +38,26 @@ class TestComposeExtendsWithEmptyService(unittest.TestCase, RunSubprocessMixin):
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
def test_podman_compose_extends_w_empty_service(self):
|
||||
"""
|
||||
Test that podman-compose can execute podman-compose -f <file> up with extended File which
|
||||
includes an empty service. (e.g. if the file is used as placeholder for more complex
|
||||
configurations.)
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent.parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "integration", "extends_w_empty_service", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
self.run_subprocess_assert_returncode(command_up)
|
1
tests/integration/extends_w_file/__init__.py
Normal file
1
tests/integration/extends_w_file/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
1
tests/integration/extends_w_file_subdir/__init__.py
Normal file
1
tests/integration/extends_w_file_subdir/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -5,31 +5,47 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def base_path():
|
||||
"""Returns the base path for the project"""
|
||||
return Path(__file__).parent.parent.parent
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "extends_w_file_subdir"), "docker-compose.yml")
|
||||
|
||||
|
||||
def test_path():
|
||||
"""Returns the path to the tests directory"""
|
||||
return os.path.join(base_path(), "tests/integration")
|
||||
class TestComposeExtendsWithFileSubdir(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_extends_w_file_subdir(self): # when file is Dockerfile for building the image
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(
|
||||
[
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"up",
|
||||
],
|
||||
)
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"ps",
|
||||
])
|
||||
self.assertIn("extends_w_file_subdir_web_1", str(output))
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
||||
|
||||
|
||||
def podman_compose_path():
|
||||
"""Returns the path to the podman compose script"""
|
||||
return os.path.join(base_path(), "podman_compose.py")
|
||||
|
||||
|
||||
class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_extends_w_file_subdir(self):
|
||||
def test_podman_compose_extends_w_file_subdir(self):
|
||||
"""
|
||||
Test that podman-compose can execute podman-compose -f <file> up with extended File which
|
||||
includes a build context
|
||||
:return:
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent.parent
|
||||
main_path = Path(__file__).parent.parent.parent.parent
|
||||
|
||||
command_up = [
|
||||
"coverage",
|
||||
@ -86,26 +102,3 @@ class TestPodmanCompose(unittest.TestCase, RunSubprocessMixin):
|
||||
# check container did not exists anymore
|
||||
out, _ = self.run_subprocess_assert_returncode(command_check_container)
|
||||
self.assertEqual(out, b'')
|
||||
|
||||
def test_extends_w_empty_service(self):
|
||||
"""
|
||||
Test that podman-compose can execute podman-compose -f <file> up with extended File which
|
||||
includes an empty service. (e.g. if the file is used as placeholder for more complex
|
||||
configurations.)
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent.parent
|
||||
|
||||
command_up = [
|
||||
"python3",
|
||||
str(main_path.joinpath("podman_compose.py")),
|
||||
"-f",
|
||||
str(
|
||||
main_path.joinpath(
|
||||
"tests", "integration", "extends_w_empty_service", "docker-compose.yml"
|
||||
)
|
||||
),
|
||||
"up",
|
||||
"-d",
|
||||
]
|
||||
|
||||
self.run_subprocess_assert_returncode(command_up)
|
1
tests/integration/filesystem/__init__.py
Normal file
1
tests/integration/filesystem/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -4,9 +4,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
class TestFilesystem(unittest.TestCase, RunSubprocessMixin):
|
||||
@ -35,8 +35,7 @@ class TestFilesystem(unittest.TestCase, RunSubprocessMixin):
|
||||
"container1",
|
||||
])
|
||||
|
||||
# BUG: figure out why cat is called twice
|
||||
self.assertEqual(out, b'data_compose_symlink\ndata_compose_symlink\n')
|
||||
self.assertEqual(out, b'data_compose_symlink\n')
|
||||
|
||||
finally:
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
1
tests/integration/in_pod/__init__.py
Normal file
1
tests/integration/in_pod/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -8,7 +8,7 @@ from tests.integration.test_utils import RunSubprocessMixin
|
||||
|
||||
def base_path():
|
||||
"""Returns the base path for the project"""
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
|
||||
|
||||
def test_path():
|
||||
@ -21,6 +21,16 @@ def podman_compose_path():
|
||||
return os.path.join(base_path(), "podman_compose.py")
|
||||
|
||||
|
||||
def is_root():
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def failure_exitcode_when_rootful():
|
||||
if is_root():
|
||||
return 125
|
||||
return 0
|
||||
|
||||
|
||||
# If a compose file has userns_mode set, setting in_pod to True, results in error.
|
||||
# Default in_pod setting is True, unless compose file provides otherwise.
|
||||
# Compose file provides custom in_pod option, which can be overridden by command line in_pod option.
|
||||
@ -64,7 +74,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
@ -96,7 +106,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -142,7 +152,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
@ -188,7 +198,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
@ -221,7 +231,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -255,7 +265,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -301,7 +311,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
@ -334,7 +344,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -368,7 +378,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -402,7 +412,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
||||
@ -448,7 +458,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(command_up)
|
||||
self.run_subprocess_assert_returncode(command_up, failure_exitcode_when_rootful())
|
||||
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode(down_cmd)
|
||||
@ -482,7 +492,7 @@ class TestPodmanComposeInPod(unittest.TestCase, RunSubprocessMixin):
|
||||
]
|
||||
|
||||
try:
|
||||
out, err = self.run_subprocess_assert_returncode(command_up)
|
||||
out, err = self.run_subprocess_assert_returncode(command_up, 125)
|
||||
self.assertEqual(b"Error: --userns and --pod cannot be set together" in err, True)
|
||||
|
||||
finally:
|
1
tests/integration/include/__init__.py
Normal file
1
tests/integration/include/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -12,7 +12,7 @@ class TestPodmanComposeInclude(unittest.TestCase, RunSubprocessMixin):
|
||||
Test that podman-compose can execute podman-compose -f <file> up with include
|
||||
:return:
|
||||
"""
|
||||
main_path = Path(__file__).parent.parent.parent
|
||||
main_path = Path(__file__).parent.parent.parent.parent
|
||||
|
||||
command_up = [
|
||||
"coverage",
|
1
tests/integration/interpolation/__init__.py
Normal file
1
tests/integration/interpolation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
1
tests/integration/ipam_default/__init__.py
Normal file
1
tests/integration/ipam_default/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -4,9 +4,9 @@ import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
@ -6,9 +6,9 @@ import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
class TestLifetime(unittest.TestCase, RunSubprocessMixin):
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "One"]
|
||||
ports: !override
|
||||
- "8111:81"
|
@ -0,0 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "Zero"]
|
||||
ports:
|
||||
- "8080:80"
|
@ -0,0 +1,60 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "override_tag_attribute"), "docker-compose.yaml")
|
||||
|
||||
|
||||
class TestComposeOverrideTagAttribute(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if a service attribute from docker-compose.yaml file is overridden
|
||||
def test_override_tag_attribute(self):
|
||||
override_file = os.path.join(
|
||||
os.path.join(test_path(), "override_tag_attribute"),
|
||||
"docker-compose.override_attribute.yaml",
|
||||
)
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
override_file,
|
||||
"up",
|
||||
])
|
||||
# merge rules are still applied
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
override_file,
|
||||
"logs",
|
||||
])
|
||||
self.assertEqual(output, b"One\n")
|
||||
|
||||
# only app service attribute "ports" was overridden
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"inspect",
|
||||
"override_tag_attribute_app_1",
|
||||
])
|
||||
container_info = json.loads(output.decode('utf-8'))[0]
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
|
||||
)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
app: !override
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "One"]
|
||||
ports:
|
||||
- "8111:81"
|
@ -0,0 +1,7 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "Zero"]
|
||||
ports:
|
||||
- "8080:80"
|
@ -0,0 +1,61 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "override_tag_service"), "docker-compose.yaml")
|
||||
|
||||
|
||||
class TestComposeOverrideTagService(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if whole service from docker-compose.yaml file is overridden in another file
|
||||
def test_override_tag_service(self):
|
||||
override_file = os.path.join(
|
||||
os.path.join(test_path(), "override_tag_service"),
|
||||
"docker-compose.override_service.yaml",
|
||||
)
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
override_file,
|
||||
"up",
|
||||
])
|
||||
|
||||
# Whole app service was overridden in the docker-compose.override_tag_service.yaml file.
|
||||
# Command and port is overridden accordingly.
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
override_file,
|
||||
"logs",
|
||||
])
|
||||
self.assertEqual(output, b"One\n")
|
||||
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"inspect",
|
||||
"override_tag_service_app_1",
|
||||
])
|
||||
container_info = json.loads(output.decode('utf-8'))[0]
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
|
||||
)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,5 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: !reset {}
|
@ -0,0 +1,5 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "Zero"]
|
@ -0,0 +1,58 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.yaml")
|
||||
|
||||
|
||||
class TestComposeResetTagAttribute(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if the attribute of the service is correctly reset
|
||||
def test_reset_tag_attribute(self):
|
||||
reset_file = os.path.join(
|
||||
os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.reset_attribute.yaml"
|
||||
)
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"up",
|
||||
])
|
||||
|
||||
# the service still exists, but its command attribute was reset in
|
||||
# docker-compose.reset_tag_attribute.yaml file and is now empty
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"ps",
|
||||
])
|
||||
self.assertIn(b"reset_tag_attribute_app_1", output)
|
||||
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"logs",
|
||||
])
|
||||
self.assertEqual(output, b"")
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,6 @@
|
||||
version: "3"
|
||||
services:
|
||||
app: !reset
|
||||
app2:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "One"]
|
@ -0,0 +1,5 @@
|
||||
version: "3"
|
||||
services:
|
||||
app:
|
||||
image: busybox
|
||||
command: ["/bin/busybox", "echo", "Zero"]
|
@ -0,0 +1,59 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "reset_tag_service"), "docker-compose.yaml")
|
||||
|
||||
|
||||
class TestComposeResetTagService(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if whole service from docker-compose.yaml file is reset
|
||||
def test_reset_tag_service(self):
|
||||
reset_file = os.path.join(
|
||||
os.path.join(test_path(), "reset_tag_service"), "docker-compose.reset_service.yaml"
|
||||
)
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"up",
|
||||
])
|
||||
|
||||
# app service was fully reset in docker-compose.reset_tag_service.yaml file, therefore
|
||||
# does not exist. A new service was created instead.
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"ps",
|
||||
])
|
||||
self.assertNotIn(b"reset_tag_service_app_1", output)
|
||||
self.assertIn(b"reset_tag_service_app2_1", output)
|
||||
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"-f",
|
||||
reset_file,
|
||||
"logs",
|
||||
])
|
||||
self.assertEqual(output, b"One\n")
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
])
|
1
tests/integration/merge/volumes_merge/__init__.py
Normal file
1
tests/integration/merge/volumes_merge/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,75 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path(compose_name):
|
||||
""" "Returns the path to the compose file used for this test module"""
|
||||
base_path = os.path.join(test_path(), "volumes_merge/")
|
||||
return os.path.join(base_path, compose_name)
|
||||
|
||||
|
||||
class TestComposeVolumesMerge(unittest.TestCase, RunSubprocessMixin):
|
||||
def test_volumes_merge(self):
|
||||
# test if additional compose file overrides host path and access mode of a volume
|
||||
try:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path("docker-compose.yaml"),
|
||||
"-f",
|
||||
compose_yaml_path("docker-compose.override.yaml"),
|
||||
"up",
|
||||
"-d",
|
||||
])
|
||||
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"exec",
|
||||
"-ti",
|
||||
"volumes_merge_web_1",
|
||||
"cat",
|
||||
"/var/www/html/index.html",
|
||||
"/var/www/html/index2.html",
|
||||
"/var/www/html/index3.html",
|
||||
])
|
||||
self.assertEqual(
|
||||
out,
|
||||
b"The file from docker-compose.override.yaml\r\n"
|
||||
b"The file from docker-compose.override.yaml\r\n"
|
||||
b"The file from docker-compose.override.yaml\r\n",
|
||||
)
|
||||
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
"podman",
|
||||
"inspect",
|
||||
"volumes_merge_web_1",
|
||||
])
|
||||
volumes_info = json.loads(out.decode('utf-8'))[0]
|
||||
binds_info = volumes_info["HostConfig"]["Binds"]
|
||||
binds_info.sort()
|
||||
|
||||
file_path = os.path.join(test_path(), "volumes_merge/override.txt")
|
||||
expected = [
|
||||
f'{file_path}:/var/www/html/index.html:ro,rprivate,rbind',
|
||||
f'{file_path}:/var/www/html/index2.html:rw,rprivate,rbind',
|
||||
f'{file_path}:/var/www/html/index3.html:rw,rprivate,rbind',
|
||||
]
|
||||
self.assertEqual(binds_info, expected)
|
||||
finally:
|
||||
out, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path("docker-compose.yaml"),
|
||||
"-f",
|
||||
compose_yaml_path("docker-compose.override.yaml"),
|
||||
"down",
|
||||
"-t",
|
||||
"0",
|
||||
])
|
1
tests/integration/multicompose/__init__.py
Normal file
1
tests/integration/multicompose/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -3,9 +3,9 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
1
tests/integration/nethost/__init__.py
Normal file
1
tests/integration/nethost/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -5,9 +5,9 @@ import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
1
tests/integration/nets_test1/__init__.py
Normal file
1
tests/integration/nets_test1/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -6,9 +6,9 @@ import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
@ -59,9 +59,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
|
||||
)
|
||||
|
||||
# check if Host port is the same as provided by the service port
|
||||
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
|
||||
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
|
||||
self.assertIsNotNone(
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
|
||||
)
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
|
||||
)
|
||||
|
||||
self.assertEqual(container_info["Config"]["Hostname"], "web1")
|
||||
@ -77,9 +81,13 @@ class TestComposeNetsTest1(unittest.TestCase, RunSubprocessMixin):
|
||||
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test1_default"
|
||||
)
|
||||
|
||||
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
|
||||
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
|
||||
self.assertIsNotNone(
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
|
||||
)
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
|
||||
)
|
||||
|
||||
self.assertEqual(container_info["Config"]["Hostname"], "web2")
|
1
tests/integration/nets_test2/__init__.py
Normal file
1
tests/integration/nets_test2/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
@ -6,9 +6,9 @@ import unittest
|
||||
|
||||
import requests
|
||||
|
||||
from tests.integration.test_podman_compose import podman_compose_path
|
||||
from tests.integration.test_podman_compose import test_path
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
@ -59,9 +59,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
|
||||
)
|
||||
|
||||
# check if Host port is the same as prodvided by the service port
|
||||
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
|
||||
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
|
||||
self.assertIsNotNone(
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
|
||||
)
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"8001/tcp": [{"HostIp": "", "HostPort": "8001"}]},
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8001"
|
||||
)
|
||||
|
||||
self.assertEqual(container_info["Config"]["Hostname"], "web1")
|
||||
@ -78,9 +82,13 @@ class TestComposeNetsTest2(unittest.TestCase, RunSubprocessMixin):
|
||||
list(container_info["NetworkSettings"]["Networks"].keys())[0], "nets_test2_mystack"
|
||||
)
|
||||
|
||||
self.assertIsNotNone(container_info['NetworkSettings']["Ports"].get("8001/tcp", None))
|
||||
self.assertGreater(len(container_info['NetworkSettings']["Ports"]["8001/tcp"]), 0)
|
||||
self.assertIsNotNone(
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0].get("HostPort", None)
|
||||
)
|
||||
self.assertEqual(
|
||||
container_info['NetworkSettings']["Ports"],
|
||||
{"8001/tcp": [{"HostIp": "", "HostPort": "8002"}]},
|
||||
container_info['NetworkSettings']["Ports"]["8001/tcp"][0]["HostPort"], "8002"
|
||||
)
|
||||
|
||||
self.assertEqual(container_info["Config"]["Hostname"], "web2")
|
@ -41,5 +41,5 @@ services:
|
||||
aliases:
|
||||
- alias21
|
||||
volumes:
|
||||
- ./test2.txt:/var/www/html/index.txt:ro,z
|
||||
- ./test3.txt:/var/www/html/index.txt:ro,z
|
||||
|
||||
|
1
tests/integration/nets_test3/test3.txt
Normal file
1
tests/integration/nets_test3/test3.txt
Normal file
@ -0,0 +1 @@
|
||||
test3
|
@ -0,0 +1,68 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from parameterized import parameterized
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "nets_test3"), "docker-compose.yml")
|
||||
|
||||
|
||||
class TestComposeNetsTest3(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if services can access the networks of other services using their respective aliases
|
||||
@parameterized.expand([
|
||||
("nets_test3_web2_1", "web3", b"test3", 0),
|
||||
("nets_test3_web2_1", "alias11", b"test3", 0),
|
||||
("nets_test3_web2_1", "alias12", b"test3", 0),
|
||||
("nets_test3_web2_1", "alias21", b"test3", 0),
|
||||
("nets_test3_web1_1", "web3", b"test3", 0),
|
||||
("nets_test3_web1_1", "alias11", b"test3", 0),
|
||||
("nets_test3_web1_1", "alias12", b"test3", 0),
|
||||
# connection fails as web1 service does not know net2 and its aliases
|
||||
("nets_test3_web1_1", "alias21", b"", 1),
|
||||
])
|
||||
def test_nets_test3(
|
||||
self, container_name, nework_alias_name, expected_text, expected_returncode
|
||||
):
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(
|
||||
[
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"up",
|
||||
"-d",
|
||||
],
|
||||
)
|
||||
# check connection from different services to network aliases of web3 service
|
||||
cmd = [
|
||||
"podman",
|
||||
"exec",
|
||||
"-it",
|
||||
f"{container_name}",
|
||||
"/bin/busybox",
|
||||
"wget",
|
||||
"-O",
|
||||
"-",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
f"http://{nework_alias_name}:8001/index.txt",
|
||||
]
|
||||
out, _, returncode = self.run_subprocess(cmd)
|
||||
self.assertEqual(expected_returncode, returncode)
|
||||
self.assertEqual(expected_text, out.strip())
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
"-t",
|
||||
"0",
|
||||
])
|
@ -0,0 +1,74 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from tests.integration.test_utils import RunSubprocessMixin
|
||||
from tests.integration.test_utils import podman_compose_path
|
||||
from tests.integration.test_utils import test_path
|
||||
|
||||
|
||||
def compose_yaml_path():
|
||||
return os.path.join(os.path.join(test_path(), "nets_test_ip"), "docker-compose.yml")
|
||||
|
||||
|
||||
class TestComposeNetsTestIp(unittest.TestCase, RunSubprocessMixin):
|
||||
# test if services retain custom ipv4_address and mac_address matching the subnet provided
|
||||
# in networks top-level element
|
||||
def test_nets_test_ip(self):
|
||||
try:
|
||||
self.run_subprocess_assert_returncode(
|
||||
[
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"up",
|
||||
"-d",
|
||||
],
|
||||
)
|
||||
|
||||
expected_results = [
|
||||
(
|
||||
"web1",
|
||||
b"inet 172.19.1.10/24 ",
|
||||
b"link/ether 02:01:01:00:01:01 ",
|
||||
b"inet 172.19.2.10/24 ",
|
||||
b"link/ether 02:01:01:00:02:01 ",
|
||||
b"",
|
||||
),
|
||||
("web2", b"", b"", b"inet 172.19.2.11/24 ", b"", b"link/ether 02:01:01:00:02:02 "),
|
||||
("web3", b"", b"", b"inet 172.19.2.", b"", b""),
|
||||
("web4", b"inet 172.19.1.13/24 ", b"", b"inet 172.19.2.", b"", b""),
|
||||
]
|
||||
|
||||
for (
|
||||
service_name,
|
||||
shared_network_ip,
|
||||
shared_network_mac_address,
|
||||
internal_network_ip,
|
||||
internal_network_mac_address,
|
||||
mac_address,
|
||||
) in expected_results:
|
||||
output, _ = self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"exec",
|
||||
service_name,
|
||||
"ip",
|
||||
"a",
|
||||
])
|
||||
self.assertIn(shared_network_ip, output)
|
||||
self.assertIn(shared_network_mac_address, output)
|
||||
self.assertIn(internal_network_ip, output)
|
||||
self.assertIn(internal_network_mac_address, output)
|
||||
self.assertIn(mac_address, output)
|
||||
finally:
|
||||
self.run_subprocess_assert_returncode([
|
||||
podman_compose_path(),
|
||||
"-f",
|
||||
compose_yaml_path(),
|
||||
"down",
|
||||
"-t",
|
||||
"0",
|
||||
])
|
1
tests/integration/network/__init__.py
Normal file
1
tests/integration/network/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user