Merge pull request #1189 from mokibit/add-merge-reset-override
Implement `override` and `reset` analog to docker-compose
This commit is contained in:
		
						commit
						d532e09d7d
					
				|  | @ -0,0 +1 @@ | ||||||
|  | - Add support for  `reset` and `override` tags to be used when merging several compose files. | ||||||
|  | @ -1407,6 +1407,57 @@ def flat_deps(services, with_extends=False): | ||||||
|         rec_deps(services, name) |         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): | async def wait_with_timeout(coro, timeout): | ||||||
|     """ |     """ | ||||||
|     Asynchronously waits for the given coroutine to complete with a timeout. |     Asynchronously waits for the given coroutine to complete with a timeout. | ||||||
|  | @ -1605,6 +1656,12 @@ class Podman: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def normalize_service(service, sub_dir=""): | def normalize_service(service, sub_dir=""): | ||||||
|  |     if isinstance(service, ResetTag): | ||||||
|  |         return service | ||||||
|  | 
 | ||||||
|  |     if isinstance(service, OverrideTag): | ||||||
|  |         service = service.value | ||||||
|  | 
 | ||||||
|     if "build" in service: |     if "build" in service: | ||||||
|         build = service["build"] |         build = service["build"] | ||||||
|         if isinstance(build, str): |         if isinstance(build, str): | ||||||
|  | @ -1708,6 +1765,8 @@ def rec_merge_one(target, source): | ||||||
|     update target from source recursively |     update target from source recursively | ||||||
|     """ |     """ | ||||||
|     done = set() |     done = set() | ||||||
|  |     remove = set() | ||||||
|  | 
 | ||||||
|     for key, value in source.items(): |     for key, value in source.items(): | ||||||
|         if key in target: |         if key in target: | ||||||
|             continue |             continue | ||||||
|  | @ -1717,15 +1776,37 @@ def rec_merge_one(target, source): | ||||||
|         if key in done: |         if key in done: | ||||||
|             continue |             continue | ||||||
|         if key not in source: |         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 |             continue | ||||||
|  | 
 | ||||||
|         value2 = source[key] |         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"): |         if key in ("command", "entrypoint"): | ||||||
|             target[key] = clone(value2) |             target[key] = clone(value2) | ||||||
|             continue |             continue | ||||||
|  | 
 | ||||||
|         if not isinstance(value2, type(value)): |         if not isinstance(value2, type(value)): | ||||||
|             value_type = type(value) |             value_type = type(value) | ||||||
|             value2_type = type(value2) |             value2_type = type(value2) | ||||||
|             raise ValueError(f"can't merge value of [{key}] of type {value_type} and {value2_type}") |             raise ValueError(f"can't merge value of [{key}] of type {value_type} and {value2_type}") | ||||||
|  | 
 | ||||||
|         if is_list(value2): |         if is_list(value2): | ||||||
|             if key == "volumes": |             if key == "volumes": | ||||||
|                 # clean duplicate mount targets |                 # clean duplicate mount targets | ||||||
|  | @ -1742,6 +1823,10 @@ def rec_merge_one(target, source): | ||||||
|             rec_merge_one(value, value2) |             rec_merge_one(value, value2) | ||||||
|         else: |         else: | ||||||
|             target[key] = value2 |             target[key] = value2 | ||||||
|  | 
 | ||||||
|  |     for key in remove: | ||||||
|  |         del target[key] | ||||||
|  | 
 | ||||||
|     return target |     return target | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -2027,10 +2112,13 @@ class PodmanCompose: | ||||||
|             content = rec_subs(content, self.environ) |             content = rec_subs(content, self.environ) | ||||||
|             if isinstance(services := content.get('services'), dict): |             if isinstance(services := content.get('services'), dict): | ||||||
|                 for service in services.values(): |                 for service in services.values(): | ||||||
|                     if 'extends' in service and (service_file := service['extends'].get('file')): |                     if not isinstance(service, OverrideTag) and not isinstance(service, ResetTag): | ||||||
|                         service['extends']['file'] = os.path.join( |                         if 'extends' in service and ( | ||||||
|                             os.path.dirname(filename), service_file |                             service_file := service['extends'].get('file') | ||||||
|                         ) |                         ): | ||||||
|  |                             service['extends']['file'] = os.path.join( | ||||||
|  |                                 os.path.dirname(filename), service_file | ||||||
|  |                             ) | ||||||
| 
 | 
 | ||||||
|             rec_merge(compose, content) |             rec_merge(compose, content) | ||||||
|             # If `include` is used, append included files to files |             # If `include` is used, append included files to files | ||||||
|  |  | ||||||
|  | @ -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", | ||||||
|  |             ]) | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 
 | ||||||
		Loading…
	
		Reference in New Issue