Skip to content

Commit d532e09

Browse files
authored
Merge pull request #1189 from mokibit/add-merge-reset-override
Implement `override` and `reset` analog to docker-compose
2 parents 8bb4310 + 1dab256 commit d532e09

24 files changed

+384
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add support for `reset` and `override` tags to be used when merging several compose files.

podman_compose.py

+92-4
Original file line numberDiff line numberDiff line change
@@ -1407,6 +1407,57 @@ def flat_deps(services, with_extends=False):
14071407
rec_deps(services, name)
14081408

14091409

1410+
###################
1411+
# Override and reset tags
1412+
###################
1413+
1414+
1415+
class OverrideTag(yaml.YAMLObject):
1416+
yaml_dumper = yaml.Dumper
1417+
yaml_loader = yaml.SafeLoader
1418+
yaml_tag = '!override'
1419+
1420+
def __init__(self, value):
1421+
if len(value) > 0 and isinstance(value[0], tuple):
1422+
self.value = {}
1423+
# item is a tuple representing service's lower level key and value
1424+
for item in value:
1425+
# value can actually be a list, then all the elements from the list have to be
1426+
# collected
1427+
if isinstance(item[1].value, list):
1428+
self.value[item[0].value] = [item.value for item in item[1].value]
1429+
else:
1430+
self.value[item[0].value] = item[1].value
1431+
else:
1432+
self.value = [item.value for item in value]
1433+
1434+
@classmethod
1435+
def from_yaml(cls, loader, node):
1436+
return OverrideTag(node.value)
1437+
1438+
@classmethod
1439+
def to_yaml(cls, dumper, data):
1440+
return dumper.represent_scalar(cls.yaml_tag, data.value)
1441+
1442+
1443+
class ResetTag(yaml.YAMLObject):
1444+
yaml_dumper = yaml.Dumper
1445+
yaml_loader = yaml.SafeLoader
1446+
yaml_tag = '!reset'
1447+
1448+
@classmethod
1449+
def to_json(cls):
1450+
return cls.yaml_tag
1451+
1452+
@classmethod
1453+
def from_yaml(cls, loader, node):
1454+
return ResetTag()
1455+
1456+
@classmethod
1457+
def to_yaml(cls, dumper, data):
1458+
return dumper.represent_scalar(cls.yaml_tag, '')
1459+
1460+
14101461
async def wait_with_timeout(coro, timeout):
14111462
"""
14121463
Asynchronously waits for the given coroutine to complete with a timeout.
@@ -1605,6 +1656,12 @@ async def volume_ls(self):
16051656

16061657

16071658
def normalize_service(service, sub_dir=""):
1659+
if isinstance(service, ResetTag):
1660+
return service
1661+
1662+
if isinstance(service, OverrideTag):
1663+
service = service.value
1664+
16081665
if "build" in service:
16091666
build = service["build"]
16101667
if isinstance(build, str):
@@ -1708,6 +1765,8 @@ def rec_merge_one(target, source):
17081765
update target from source recursively
17091766
"""
17101767
done = set()
1768+
remove = set()
1769+
17111770
for key, value in source.items():
17121771
if key in target:
17131772
continue
@@ -1717,15 +1776,37 @@ def rec_merge_one(target, source):
17171776
if key in done:
17181777
continue
17191778
if key not in source:
1779+
if isinstance(value, ResetTag):
1780+
log("INFO: Unneeded !reset found for [{key}]")
1781+
remove.add(key)
1782+
1783+
if isinstance(value, OverrideTag):
1784+
log("INFO: Unneeded !override found for [{key}] with value '{value}'")
1785+
target[key] = clone(value.value)
1786+
17201787
continue
1788+
17211789
value2 = source[key]
1790+
1791+
if isinstance(value, ResetTag) or isinstance(value2, ResetTag):
1792+
remove.add(key)
1793+
continue
1794+
1795+
if isinstance(value, OverrideTag) or isinstance(value2, OverrideTag):
1796+
target[key] = (
1797+
clone(value.value) if isinstance(value, OverrideTag) else clone(value2.value)
1798+
)
1799+
continue
1800+
17221801
if key in ("command", "entrypoint"):
17231802
target[key] = clone(value2)
17241803
continue
1804+
17251805
if not isinstance(value2, type(value)):
17261806
value_type = type(value)
17271807
value2_type = type(value2)
17281808
raise ValueError(f"can't merge value of [{key}] of type {value_type} and {value2_type}")
1809+
17291810
if is_list(value2):
17301811
if key == "volumes":
17311812
# clean duplicate mount targets
@@ -1742,6 +1823,10 @@ def rec_merge_one(target, source):
17421823
rec_merge_one(value, value2)
17431824
else:
17441825
target[key] = value2
1826+
1827+
for key in remove:
1828+
del target[key]
1829+
17451830
return target
17461831

17471832

@@ -2027,10 +2112,13 @@ def _parse_compose_file(self):
20272112
content = rec_subs(content, self.environ)
20282113
if isinstance(services := content.get('services'), dict):
20292114
for service in services.values():
2030-
if 'extends' in service and (service_file := service['extends'].get('file')):
2031-
service['extends']['file'] = os.path.join(
2032-
os.path.dirname(filename), service_file
2033-
)
2115+
if not isinstance(service, OverrideTag) and not isinstance(service, ResetTag):
2116+
if 'extends' in service and (
2117+
service_file := service['extends'].get('file')
2118+
):
2119+
service['extends']['file'] = os.path.join(
2120+
os.path.dirname(filename), service_file
2121+
)
20342122

20352123
rec_merge(compose, content)
20362124
# If `include` is used, append included files to files
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "One"]
6+
ports: !override
7+
- "8111:81"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "Zero"]
6+
ports:
7+
- "8080:80"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import json
4+
import os
5+
import unittest
6+
7+
from tests.integration.test_utils import RunSubprocessMixin
8+
from tests.integration.test_utils import podman_compose_path
9+
from tests.integration.test_utils import test_path
10+
11+
12+
def compose_yaml_path():
13+
return os.path.join(os.path.join(test_path(), "override_tag_attribute"), "docker-compose.yaml")
14+
15+
16+
class TestComposeOverrideTagAttribute(unittest.TestCase, RunSubprocessMixin):
17+
# test if a service attribute from docker-compose.yaml file is overridden
18+
def test_override_tag_attribute(self):
19+
override_file = os.path.join(
20+
os.path.join(test_path(), "override_tag_attribute"),
21+
"docker-compose.override_attribute.yaml",
22+
)
23+
try:
24+
self.run_subprocess_assert_returncode([
25+
podman_compose_path(),
26+
"-f",
27+
compose_yaml_path(),
28+
"-f",
29+
override_file,
30+
"up",
31+
])
32+
# merge rules are still applied
33+
output, _ = self.run_subprocess_assert_returncode([
34+
podman_compose_path(),
35+
"-f",
36+
compose_yaml_path(),
37+
"-f",
38+
override_file,
39+
"logs",
40+
])
41+
self.assertEqual(output, b"One\n")
42+
43+
# only app service attribute "ports" was overridden
44+
output, _ = self.run_subprocess_assert_returncode([
45+
"podman",
46+
"inspect",
47+
"override_tag_attribute_app_1",
48+
])
49+
container_info = json.loads(output.decode('utf-8'))[0]
50+
self.assertEqual(
51+
container_info['NetworkSettings']["Ports"],
52+
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
53+
)
54+
finally:
55+
self.run_subprocess_assert_returncode([
56+
podman_compose_path(),
57+
"-f",
58+
compose_yaml_path(),
59+
"down",
60+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3"
2+
services:
3+
app: !override
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "One"]
6+
ports:
7+
- "8111:81"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "Zero"]
6+
ports:
7+
- "8080:80"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import json
4+
import os
5+
import unittest
6+
7+
from tests.integration.test_utils import RunSubprocessMixin
8+
from tests.integration.test_utils import podman_compose_path
9+
from tests.integration.test_utils import test_path
10+
11+
12+
def compose_yaml_path():
13+
return os.path.join(os.path.join(test_path(), "override_tag_service"), "docker-compose.yaml")
14+
15+
16+
class TestComposeOverrideTagService(unittest.TestCase, RunSubprocessMixin):
17+
# test if whole service from docker-compose.yaml file is overridden in another file
18+
def test_override_tag_service(self):
19+
override_file = os.path.join(
20+
os.path.join(test_path(), "override_tag_service"),
21+
"docker-compose.override_service.yaml",
22+
)
23+
try:
24+
self.run_subprocess_assert_returncode([
25+
podman_compose_path(),
26+
"-f",
27+
compose_yaml_path(),
28+
"-f",
29+
override_file,
30+
"up",
31+
])
32+
33+
# Whole app service was overridden in the docker-compose.override_tag_service.yaml file.
34+
# Command and port is overridden accordingly.
35+
output, _ = self.run_subprocess_assert_returncode([
36+
podman_compose_path(),
37+
"-f",
38+
compose_yaml_path(),
39+
"-f",
40+
override_file,
41+
"logs",
42+
])
43+
self.assertEqual(output, b"One\n")
44+
45+
output, _ = self.run_subprocess_assert_returncode([
46+
"podman",
47+
"inspect",
48+
"override_tag_service_app_1",
49+
])
50+
container_info = json.loads(output.decode('utf-8'))[0]
51+
self.assertEqual(
52+
container_info['NetworkSettings']["Ports"],
53+
{"81/tcp": [{"HostIp": "", "HostPort": "8111"}]},
54+
)
55+
finally:
56+
self.run_subprocess_assert_returncode([
57+
podman_compose_path(),
58+
"-f",
59+
compose_yaml_path(),
60+
"down",
61+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: !reset {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "Zero"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
3+
import os
4+
import unittest
5+
6+
from tests.integration.test_utils import RunSubprocessMixin
7+
from tests.integration.test_utils import podman_compose_path
8+
from tests.integration.test_utils import test_path
9+
10+
11+
def compose_yaml_path():
12+
return os.path.join(os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.yaml")
13+
14+
15+
class TestComposeResetTagAttribute(unittest.TestCase, RunSubprocessMixin):
16+
# test if the attribute of the service is correctly reset
17+
def test_reset_tag_attribute(self):
18+
reset_file = os.path.join(
19+
os.path.join(test_path(), "reset_tag_attribute"), "docker-compose.reset_attribute.yaml"
20+
)
21+
try:
22+
self.run_subprocess_assert_returncode([
23+
podman_compose_path(),
24+
"-f",
25+
compose_yaml_path(),
26+
"-f",
27+
reset_file,
28+
"up",
29+
])
30+
31+
# the service still exists, but its command attribute was reset in
32+
# docker-compose.reset_tag_attribute.yaml file and is now empty
33+
output, _ = self.run_subprocess_assert_returncode([
34+
podman_compose_path(),
35+
"-f",
36+
compose_yaml_path(),
37+
"-f",
38+
reset_file,
39+
"ps",
40+
])
41+
self.assertIn(b"reset_tag_attribute_app_1", output)
42+
43+
output, _ = self.run_subprocess_assert_returncode([
44+
podman_compose_path(),
45+
"-f",
46+
compose_yaml_path(),
47+
"-f",
48+
reset_file,
49+
"logs",
50+
])
51+
self.assertEqual(output, b"")
52+
finally:
53+
self.run_subprocess_assert_returncode([
54+
podman_compose_path(),
55+
"-f",
56+
compose_yaml_path(),
57+
"down",
58+
])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: "3"
2+
services:
3+
app: !reset
4+
app2:
5+
image: busybox
6+
command: ["/bin/busybox", "echo", "One"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
version: "3"
2+
services:
3+
app:
4+
image: busybox
5+
command: ["/bin/busybox", "echo", "Zero"]

0 commit comments

Comments
 (0)