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) | ||||
| 
 | ||||
| 
 | ||||
| ################### | ||||
| # 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. | ||||
|  | @ -1605,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): | ||||
|  | @ -1708,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 | ||||
|  | @ -1717,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 | ||||
|  | @ -1742,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 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -2027,10 +2112,13 @@ class PodmanCompose: | |||
|             content = rec_subs(content, self.environ) | ||||
|             if isinstance(services := content.get('services'), dict): | ||||
|                 for service in services.values(): | ||||
|                     if 'extends' in service and (service_file := service['extends'].get('file')): | ||||
|                         service['extends']['file'] = os.path.join( | ||||
|                             os.path.dirname(filename), service_file | ||||
|                         ) | ||||
|                     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 | ||||
|  |  | |||
|  | @ -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