From 6669c3b7efeec3edca9975d1658ab8e3dcffa0b2 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 14 May 2022 20:26:32 +0800 Subject: [PATCH 01/93] feat: add parser --- compose_viz/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 1f44bbc..de34128 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -7,4 +7,5 @@ class Parser: def parse(self, file_path: str) -> Compose: # validate input file using `docker-compose config -q sys.argv[1]` first + return Compose([]) raise NotImplementedError From 5df0ade777482b6d6f3dad87b5a0ac4a86a52c90 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 14 May 2022 21:30:18 +0800 Subject: [PATCH 02/93] chore: implement Parser.parse --- compose_viz/parser.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index de34128..e035e45 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,5 +1,7 @@ +from re import S from compose_viz.compose import Compose - +from compose_viz.compose import Service +import yaml class Parser: def __init__(self): @@ -7,5 +9,36 @@ class Parser: def parse(self, file_path: str) -> Compose: # validate input file using `docker-compose config -q sys.argv[1]` first - return Compose([]) - raise NotImplementedError + # load the yaml file + with open(file_path, "r") as f: + try: + yaml_data = yaml.safe_load(f) + except yaml.YAMLError as exc: + raise yaml.YAMLError + # validate the yaml file + if not yaml_data: + print("Error: empty yaml file") + raise ValueError + if not yaml_data.get("services"): + print("Error: no services found") + raise ValueError + # parse services data into Service objects + services_data = yaml_data["services"] + services = [] + for service, service_name in zip(services_data.values(), services_data.keys()): + #print("name: {}".format(service_name)) + if service.get("image"): + service_image = service["image"] + #print("image: {}".format(service_image)) + if service.get("networks"): + service_networks = service["networks"] + #print("networks: {}".format(service_networks)) + services.append(Service( + name=service_name, + image=service_image, + networks=service_networks, + )) + # create Compose object + compose = Compose(services) + + return compose From 9ddfd2c1ee7cad125a13063c4aae37fcaa7b728f Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 14 May 2022 20:26:32 +0800 Subject: [PATCH 03/93] feat: add parser --- compose_viz/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 1f44bbc..de34128 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -7,4 +7,5 @@ class Parser: def parse(self, file_path: str) -> Compose: # validate input file using `docker-compose config -q sys.argv[1]` first + return Compose([]) raise NotImplementedError From 42e68fb4f7893ed5bbbd89666cea79bb4c957b51 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 14 May 2022 21:30:18 +0800 Subject: [PATCH 04/93] chore: implement Parser.parse --- compose_viz/parser.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index de34128..e035e45 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,5 +1,7 @@ +from re import S from compose_viz.compose import Compose - +from compose_viz.compose import Service +import yaml class Parser: def __init__(self): @@ -7,5 +9,36 @@ class Parser: def parse(self, file_path: str) -> Compose: # validate input file using `docker-compose config -q sys.argv[1]` first - return Compose([]) - raise NotImplementedError + # load the yaml file + with open(file_path, "r") as f: + try: + yaml_data = yaml.safe_load(f) + except yaml.YAMLError as exc: + raise yaml.YAMLError + # validate the yaml file + if not yaml_data: + print("Error: empty yaml file") + raise ValueError + if not yaml_data.get("services"): + print("Error: no services found") + raise ValueError + # parse services data into Service objects + services_data = yaml_data["services"] + services = [] + for service, service_name in zip(services_data.values(), services_data.keys()): + #print("name: {}".format(service_name)) + if service.get("image"): + service_image = service["image"] + #print("image: {}".format(service_image)) + if service.get("networks"): + service_networks = service["networks"] + #print("networks: {}".format(service_networks)) + services.append(Service( + name=service_name, + image=service_image, + networks=service_networks, + )) + # create Compose object + compose = Compose(services) + + return compose From 064da8092c41c3ad5e3e152c06e36d506994ca71 Mon Sep 17 00:00:00 2001 From: uccu Date: Sun, 15 May 2022 01:15:31 +0800 Subject: [PATCH 05/93] chore: add compose and service getter --- compose_viz/compose.py | 6 +++++- compose_viz/service.py | 48 +++++++++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/compose_viz/compose.py b/compose_viz/compose.py index 6c65a8a..4cafca1 100644 --- a/compose_viz/compose.py +++ b/compose_viz/compose.py @@ -4,7 +4,11 @@ from compose_viz.service import Service class Compose: def __init__(self, services: List[Service]) -> None: - self.services = services + self._services = services def extract_networks(self) -> List[str]: raise NotImplementedError + + @property + def services(self): + return self._services diff --git a/compose_viz/service.py b/compose_viz/service.py index 6e42483..5b809e3 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -3,11 +3,43 @@ from typing import List class Service: def __init__(self, name: str, image: str, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: List[str] = []) -> None: - self.name = name - self.image = image - self.ports = ports - self.networks = networks - self.volumes = volumes - self.depends_on = depends_on - self.links = links - self.extends = extends + self._name = name + self._image = image + self._ports = ports + self._networks = networks + self._volumes = volumes + self._depends_on = depends_on + self._links = links + self._extends = extends + + @property + def name(self): + return self._name + + @property + def image(self): + return self._image + + @property + def ports(self): + return self._ports + + @property + def networks(self): + return self._networks + + @property + def volumes(self): + return self._volumes + + @property + def depends_on(self): + return self._depends_on + + @property + def links(self): + return self._links + + @property + def extends(self): + return self._extends From 84535809088f5b4cb1a06e5fc140d50e85ae8764 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 15 May 2022 02:27:33 +0800 Subject: [PATCH 06/93] style: format using autopep8 --- compose_viz/parser.py | 1 + compose_viz/service.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index e035e45..8e24687 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -3,6 +3,7 @@ from compose_viz.compose import Compose from compose_viz.compose import Service import yaml + class Parser: def __init__(self): pass diff --git a/compose_viz/service.py b/compose_viz/service.py index 5b809e3..b61ccff 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -15,7 +15,7 @@ class Service: @property def name(self): return self._name - + @property def image(self): return self._image From a4520ff134594f0ea376f76e023d2cf86d6b0d4f Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 15 May 2022 02:28:27 +0800 Subject: [PATCH 07/93] chore: update dependencies --- poetry.lock | 45 ++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 381ddcb..02f538e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -110,6 +110,14 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "tomli" version = "2.0.1" @@ -138,7 +146,7 @@ test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov ( [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "a80ea7abd86b8e5579a192dfa02a55d2219a3a1850bad12da89c30aa42e99156" +content-hash = "f9abb7a4321c120cbd771d7269efc378a123f9ac9920a9413f3b61f1d6604663" [metadata.files] atomicwrites = [ @@ -181,6 +189,41 @@ pytest = [ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, diff --git a/pyproject.toml b/pyproject.toml index 3b7de75..cca4290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ include = [ [tool.poetry.dependencies] python = "^3.9" typer = "^0.4.1" +PyYAML = "^6.0" [tool.poetry.dev-dependencies] pytest = "^7.1.2" From 7f59cebb859cc3758d1f47390f7f9dbe6cf78045 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 15 May 2022 02:42:27 +0800 Subject: [PATCH 08/93] test: use a better way to make assertion --- tests/test_parse_file.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index bfc6b4a..c66e336 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -25,4 +25,14 @@ def test_parse_file(): parser = Parser() actual = parser.parse('tests/in/000001.yaml') - assert actual == expected + assert len(actual.services) == len(expected.services) + + for actual_service, expected_service in zip(actual.services, expected.services): + assert actual_service.name == expected_service.name + assert actual_service.image == expected_service.image + assert actual_service.ports == expected_service.ports + assert actual_service.networks == expected_service.networks + assert actual_service.volumes == expected_service.volumes + assert actual_service.depends_on == expected_service.depends_on + assert actual_service.links == expected_service.links + assert actual_service.extends == expected_service.extends From 0e9afd81337466dcc9ec983d276eb09c992a95fc Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 15 May 2022 19:18:10 +0800 Subject: [PATCH 09/93] chore: update command descriptions --- compose_viz/cli.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compose_viz/cli.py b/compose_viz/cli.py index d2bf1d3..00c1ddd 100644 --- a/compose_viz/cli.py +++ b/compose_viz/cli.py @@ -27,23 +27,23 @@ def _version_callback(value: bool) -> None: @app.callback() def compose_viz( input_path: str, - output_path: Optional[str] = typer.Option( - None, - "--output_path", + output_path: str = typer.Option( + "./compose-viz.png", + "--output-path", "-o", - help="Output path for the generated visualization.", + help="Output path for the generated visualization file.", ), format: VisualizationFormats = typer.Option( "PNG", "--format", "-m", - help="Output format for the generated visualization.", + help="Output format for the generated visualization file.", ), _: Optional[bool] = typer.Option( None, "--version", "-v", - help="Show the version of compose_viz.", + help="Show the version of compose-viz.", callback=_version_callback, is_eager=True, ) @@ -58,4 +58,4 @@ def compose_viz( def start_cli() -> None: - app(prog_name=__app_name__) + app(prog_name="cpv") From 2f274ac00201020ce5ec48eb8ddd3be0a210379a Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 15 May 2022 19:24:36 +0800 Subject: [PATCH 10/93] chore: update README.md --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 060e486..c4bd58a 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@
  • Getting Started
      +
    • Installation
    • Usage
    • Options
    • Example
    • @@ -62,20 +63,32 @@ ## Getting Started +### Installation + +#### Using `pip` + +`pip install compose-viz` + +#### Using `.whl` + +See [releases](https://github.com/compose-viz/compose-viz/releases). + ### Usage -`python3 compose-viz.py [OPTIONS] [input-file]` +`cpv [OPTIONS] INPUT_PATH` ### Options -| Option | Necessity | Description | Default Value | -| ----------------------------- | --------- | ----------------- | --------------- | -| `-o --output-file` | Optional | Output file path. | `./compose.png` | -| `-m --output-format=DOT, PNG` | Optional | Output format. | PNG | +| Option | Description | +| ------------------------ | ------------------------------------------------------------------------------ | +| `-o, --output-path` | Output path for the generated visualization file. [default: ./compose-viz.png] | +| `-m, --format [PNG,DOT]` | Output format for the generated visualization file. [default: PNG] | +| `-v, --version` | Show the version of compose-viz. | +| `--help` | Show help and exit. | ### Example -`python3 compose-viz.py docker-compose.yaml` +`cpv -o docker-compose-viz.png docker-compose.yml`

      (back to top)

      From d53d0a77cfff807df935fe1739757191b12c5cf5 Mon Sep 17 00:00:00 2001 From: uccu Date: Tue, 17 May 2022 00:53:29 +0800 Subject: [PATCH 11/93] fix: use ruamel instead of yaml --- compose_viz/parser.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 8e24687..9b4e892 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,7 +1,7 @@ from re import S from compose_viz.compose import Compose from compose_viz.compose import Service -import yaml +from ruamel.yaml import YAML class Parser: @@ -13,9 +13,10 @@ class Parser: # load the yaml file with open(file_path, "r") as f: try: - yaml_data = yaml.safe_load(f) - except yaml.YAMLError as exc: - raise yaml.YAMLError + yaml = YAML() + yaml_data = yaml.load(f) + except YAML.YAMLError as exc: + raise YAML.YAMLError # validate the yaml file if not yaml_data: print("Error: empty yaml file") From 26f704f6e187e07bc04a9fc3dbb6cc764bce754c Mon Sep 17 00:00:00 2001 From: Chuan Ou Yang Date: Tue, 17 May 2022 00:55:57 +0800 Subject: [PATCH 12/93] chore: add graph dependencies --- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 02f538e..e47e55f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,6 +39,19 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "graphviz" +version = "0.20" +description = "Simple Python interface for Graphviz" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +dev = ["tox (>=3)", "flake8", "pep8-naming", "wheel", "twine"] +docs = ["sphinx (>=4)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"] +test = ["pytest (>=7)", "pytest-mock (>=3)", "mock (>=4)", "pytest-cov", "coverage"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -146,7 +159,7 @@ test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov ( [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "f9abb7a4321c120cbd771d7269efc378a123f9ac9920a9413f3b61f1d6604663" +content-hash = "f9f303620dc1f23552238ab8cd7c4db52ef8ee897076bb941866826bc7004dfb" [metadata.files] atomicwrites = [ @@ -165,6 +178,10 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +graphviz = [ + {file = "graphviz-0.20-py3-none-any.whl", hash = "sha256:62c5f48bcc534a45b4588c548ff75e419c1f1f3a33d31a91796ae80a7f581e4a"}, + {file = "graphviz-0.20.zip", hash = "sha256:76bdfb73f42e72564ffe9c7299482f9d72f8e6cb8d54bce7b48ab323755e9ba5"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, diff --git a/pyproject.toml b/pyproject.toml index cca4290..0d33426 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ include = [ python = "^3.9" typer = "^0.4.1" PyYAML = "^6.0" +graphviz = "^0.20" [tool.poetry.dev-dependencies] pytest = "^7.1.2" From 23793d89026f0ad60c1b48f50f2a761c8998e036 Mon Sep 17 00:00:00 2001 From: Chuan Ou Yang Date: Tue, 17 May 2022 00:57:14 +0800 Subject: [PATCH 13/93] feat: add compose renderer --- compose_viz/cli.py | 4 ++- compose_viz/graph.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 3 ++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 compose_viz/graph.py diff --git a/compose_viz/cli.py b/compose_viz/cli.py index 00c1ddd..8e4ef9b 100644 --- a/compose_viz/cli.py +++ b/compose_viz/cli.py @@ -3,7 +3,7 @@ import typer from typing import Optional from compose_viz import __app_name__, __version__ from compose_viz.parser import Parser - +from compose_viz.graph import Graph class VisualizationFormats(str, Enum): png = "PNG" @@ -54,6 +54,8 @@ def compose_viz( if compose: typer.echo(f"Successfully parsed {input_path}") + Graph(compose, output_path).render(format) + raise typer.Exit() diff --git a/compose_viz/graph.py b/compose_viz/graph.py new file mode 100644 index 0000000..9f8a7fa --- /dev/null +++ b/compose_viz/graph.py @@ -0,0 +1,72 @@ +import graphviz + +from compose_viz.compose import Compose + + +def apply_vertex_style(type) -> dict: + style = { + 'service': { + 'shape': 'component', + }, + 'volume': { + 'shape': 'folder', + }, + 'network': { + 'shape': 'pentagon', + }, + 'port': { + 'shape': 'circle', + }, + } + + return style[type] + + +def apply_edge_style(type) -> dict: + style = { + 'ports': { + 'style': 'solid', + }, + 'links': { + 'style': 'solid', + }, + 'volumes': { + 'style': 'dashed', + }, + 'depends_on': { + 'style': 'dotted', + } + } + + return style[type] + + +class Graph: + def __init__(self, compose: Compose, filename: str) -> None: + self.dot = graphviz.Digraph() + self.dot.attr('graph', background='#ffffff', pad='0.5', ratio='fill') + self.compose = compose + self.filename = filename + + def add_vertex(self, name: str, type: str) -> None: + self.dot.node(name, **apply_vertex_style(type)) + + def add_edge(self, head: str, tail: str, type: str) -> None: + self.dot.edge(head, tail, **apply_edge_style(type)) + + def render(self, format: str, cleanup: bool = True) -> None: + for service in self.compose.services: + self.add_vertex(service.name, 'service') + for network in service.networks: + self.add_vertex("net#" + network, 'network') + self.add_edge(service.name, "net#" + network, 'links') + for volume in service.volumes: + self.add_vertex(volume, 'volume') + self.add_edge(service.name, volume, 'links') + for port in service.ports: + self.add_vertex(port, 'port') + self.add_edge(service.name, port, 'ports') + for depends_on in service.depends_on: + self.dot.edge(depends_on, service.name, 'depends_on') + + self.dot.render(outfile=self.filename, format=format, cleanup=cleanup) diff --git a/tests/test_cli.py b/tests/test_cli.py index 3f427b5..bc0841e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,5 @@ +import os + from typer.testing import CliRunner from compose_viz import cli @@ -11,3 +13,4 @@ def test_cli(): assert result.exit_code == 0 assert f"Successfully parsed {input_path}\n" in result.stdout + assert os.path.exists("compose-viz.png") From b86a1f7fe3fafc0f07d81a88a5bbc1406636b647 Mon Sep 17 00:00:00 2001 From: uccu Date: Tue, 17 May 2022 01:27:59 +0800 Subject: [PATCH 14/93] fix: fix parse yaml test implement --- compose_viz/parser.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 9b4e892..6d345fb 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -13,7 +13,7 @@ class Parser: # load the yaml file with open(file_path, "r") as f: try: - yaml = YAML() + yaml = YAML(typ='safe', pure=True) yaml_data = yaml.load(f) except YAML.YAMLError as exc: raise YAML.YAMLError @@ -33,7 +33,10 @@ class Parser: service_image = service["image"] #print("image: {}".format(service_image)) if service.get("networks"): - service_networks = service["networks"] + if(type(service["networks"]) is list): + service_networks = service["networks"] + else: + service_networks = list(service["networks"].keys()) #print("networks: {}".format(service_networks)) services.append(Service( name=service_name, From ea2bd6f50b2527cd4f29f6c4259bbc6077bb9b74 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 17 May 2022 02:24:04 +0800 Subject: [PATCH 15/93] chore: add ruamel.yaml in dependency --- poetry.lock | 56 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index e47e55f..a3222f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -131,6 +131,29 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "ruamel.yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel.yaml.clib" +version = "0.2.6" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "tomli" version = "2.0.1" @@ -159,7 +182,7 @@ test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov ( [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "f9f303620dc1f23552238ab8cd7c4db52ef8ee897076bb941866826bc7004dfb" +content-hash = "f189e4baf985a53a0388422f970dd8c481332fe1d9f18d7d2bd06e4904c49bdf" [metadata.files] atomicwrites = [ @@ -241,6 +264,37 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +"ruamel.yaml" = [ + {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, + {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, +] +"ruamel.yaml.clib" = [ + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, + {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, + {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, + {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, + {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, + {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, + {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, + {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, diff --git a/pyproject.toml b/pyproject.toml index 0d33426..97d774c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ python = "^3.9" typer = "^0.4.1" PyYAML = "^6.0" graphviz = "^0.20" +"ruamel.yaml" = "^0.17.21" [tool.poetry.dev-dependencies] pytest = "^7.1.2" From 2870b43b12b70d2a97bd6786473578e33f94ef3c Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 17 May 2022 02:36:41 +0800 Subject: [PATCH 16/93] chore: add graphviz in ci/cd workflows --- .github/workflows/cd.yml | 3 ++- .github/workflows/ci.yml | 4 ++++ .github/workflows/release-tagged-version.yml | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0868300..4377be7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -14,4 +14,5 @@ jobs: - name: Build and Publish to PyPi uses: JRubics/poetry-publish@v1.10 with: - pypi_token: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file + pypi_token: ${{ secrets.PYPI_TOKEN }} + extra_build_dependency_packages: "graphviz" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c40e9d4..c7f38c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ jobs: - name: Switch to Current Branch run: git checkout ${{ env.BRANCH }} + + - run: | + sudo apt-get update + sudo apt-get install -y graphviz - name: Setup Python 3.10.4 uses: actions/setup-python@v3 diff --git a/.github/workflows/release-tagged-version.yml b/.github/workflows/release-tagged-version.yml index 3c6d547..d10f37c 100644 --- a/.github/workflows/release-tagged-version.yml +++ b/.github/workflows/release-tagged-version.yml @@ -21,6 +21,10 @@ jobs: - name: Switch to Current Branch run: git checkout ${{ env.BRANCH }} + + - run: | + sudo apt-get update + sudo apt-get install -y graphviz - name: Setup Python 3.10.4 uses: actions/setup-python@v3 From 659dc9bc2905a584e133600f6114146772d818db Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 17 May 2022 02:49:48 +0800 Subject: [PATCH 17/93] style: format using autopep8 --- compose_viz/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose_viz/cli.py b/compose_viz/cli.py index 8e4ef9b..8cac6f4 100644 --- a/compose_viz/cli.py +++ b/compose_viz/cli.py @@ -5,6 +5,7 @@ from compose_viz import __app_name__, __version__ from compose_viz.parser import Parser from compose_viz.graph import Graph + class VisualizationFormats(str, Enum): png = "PNG" dot = "DOT" From 8eb932577d177a4ec4fa365d4765072e8c025c15 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 12:19:21 +0800 Subject: [PATCH 18/93] test: add parametrize tests --- tests/test_parse_file.py | 72 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index c66e336..6019897 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1,10 +1,10 @@ +import pytest from compose_viz.parser import Parser from compose_viz.compose import Compose from compose_viz.service import Service - -def test_parse_file(): - expected: Compose = Compose([ +@pytest.mark.parametrize("test_input,expected",[ + ('tests/in/000001.yaml',Compose([ Service( name='frontend', image='awesome/webapp', @@ -20,10 +20,72 @@ def test_parse_file(): image='awesome/backend', networks=['back-tier', 'admin'], ), + ])), + ('tests/in/000010.yaml',Compose([ + Service( + name='base', + image='busybox', + user='root', + ), + Service( + name='common', + image='busybox', + extends='base' + ), + Service( + name='cli', + extends='common' + ), + ])), + ('tests/in/000100.yaml',Compose([ + #version='3.9' + Service( + build='.', + ports=['8000:5000'] + ), + Service( + name='redis', + image='redis:alpine', + ), + ])), + ('tests/in/001000.yaml',Compose([ + Service( + name='web', + build='.', + depends_on=['db','redis'] + ), + Service( + name='redis', + image='redis' + ), + Service( + name='db', + image='postgres' + ), + ])), + ('tests/in/010000.yaml',Compose([ + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'] + ) + ])), + ('tests/in/100000.yaml',Compose([ + Service( + name='web', + build='.', + links=['db:database'] + ), + Service( + name='db', + image='postgres' + ) + ])), ]) - + +def test_parse_file(test_input, expected): parser = Parser() - actual = parser.parse('tests/in/000001.yaml') + actual = parser.parse(test_input) assert len(actual.services) == len(expected.services) From 1b9941a1eab5c7bead61bf29ebe9295327796cfa Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 14:13:07 +0800 Subject: [PATCH 19/93] test: add parametrize tests --- tests/test_parse_file.py | 238 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 224 insertions(+), 14 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 6019897..5f27672 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -3,7 +3,7 @@ from compose_viz.parser import Parser from compose_viz.compose import Compose from compose_viz.service import Service -@pytest.mark.parametrize("test_input,expected",[ +@pytest.mark.parametrize('test_input,expected',[ ('tests/in/000001.yaml',Compose([ Service( name='frontend', @@ -25,7 +25,6 @@ from compose_viz.service import Service Service( name='base', image='busybox', - user='root', ), Service( name='common', @@ -34,54 +33,265 @@ from compose_viz.service import Service ), Service( name='cli', - extends='common' + extends='common', + ), + ])), + ('tests/in/000011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + extends=['frontend'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + extends=['frontend'], ), ])), ('tests/in/000100.yaml',Compose([ - #version='3.9' Service( build='.', - ports=['8000:5000'] + ports=['8000:5000'], ), Service( name='redis', image='redis:alpine', ), ])), + ('tests/in/000101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ports=['8000:5000'] + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + ports='8000:5001', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + ports='8000:5010', + networks=['back-tier', 'admin'], + ), + ])), + ('tests/in/000110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ports=['8000:5000'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + extends=['frontend'], + ), + Service( + name='backend', + image='awesome/backend', + extends=['frontend'], + ports=['8000:5001'], + ), + ])), + ('tests/in/000111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ports=['8000:5000'] + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + extends=['frontend'], + ), + Service( + name='backend', + image='awesome/backend', + ports=['8000:5001'], + networks=['back-tier', 'admin'], + extends=['frontend'], + ), + ])), ('tests/in/001000.yaml',Compose([ Service( name='web', build='.', - depends_on=['db','redis'] + depends_on=['db','redis'], ), Service( name='redis', - image='redis' + image='redis', ), Service( name='db', - image='postgres' + image='postgres', + ), + ])), + ('tests/in/001001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + depends_on=['monitoring','backend'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + ), + ])), + ('tests/in/001010.yaml',Compose([ + Service( + name='web', + build='.', + depends_on=['db','redis'], + extends=['redis'] + ), + Service( + name='redis', + image='redis', + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/001011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + depends_on=['monitoring','backend'], + extends=['backend'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + ), + ])), + ('tests/in/001100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ports=['8000:5000'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + depends_on=['backend'], + ports=['8000:5010'], + ), + Service( + name='backend', + image='awesome/backend', + ports=['8000:5001'], + ), + ])), + ('tests/in/001101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + depends_on=['backend'], + ports=['8000:5010'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + ), + ])), + ('tests/in/001110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ports=['8000:5000'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + depends_on=['backend'], + extends=['frontend'], + ports=['8000:5010'], + ), + Service( + name='backend', + image='awesome/backend', + ports=['8000:5001'], + ), + ])), + ('tests/in/001111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + depends_on=['backend'], + extends=['frontend'], + ports=['8000:5010'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], ), ])), ('tests/in/010000.yaml',Compose([ Service( name='backend', image='awesome/backend', - volumes=['db-data'] - ) + volumes=['db-data'], + ), ])), + + + + + ('tests/in/100000.yaml',Compose([ Service( name='web', build='.', - links=['db:database'] + links=['db:database'], ), Service( name='db', - image='postgres' - ) + image='postgres', + ), ])), - ]) +]) def test_parse_file(test_input, expected): parser = Parser() From f07add890502c692f144689f6c0478fc6577e120 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 14:21:51 +0800 Subject: [PATCH 20/93] test: add parametrize tests --- tests/test_parse_file.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 5f27672..9ff4253 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -29,11 +29,11 @@ from compose_viz.service import Service Service( name='common', image='busybox', - extends='base' + extends=['base'], ), Service( name='cli', - extends='common', + extends=['common'], ), ])), ('tests/in/000011.yaml',Compose([ @@ -57,7 +57,6 @@ from compose_viz.service import Service ])), ('tests/in/000100.yaml',Compose([ Service( - build='.', ports=['8000:5000'], ), Service( @@ -69,19 +68,19 @@ from compose_viz.service import Service Service( name='frontend', image='awesome/webapp', - ports=['8000:5000'] + ports=['8000:5000'], networks=['front-tier', 'back-tier'], ), Service( name='monitoring', image='awesome/monitoring', - ports='8000:5001', + ports=['8000:5001'], networks=['admin'], ), Service( name='backend', image='awesome/backend', - ports='8000:5010', + ports=['8000:5010'], networks=['back-tier', 'admin'], ), ])), @@ -127,7 +126,6 @@ from compose_viz.service import Service ('tests/in/001000.yaml',Compose([ Service( name='web', - build='.', depends_on=['db','redis'], ), Service( @@ -160,7 +158,6 @@ from compose_viz.service import Service ('tests/in/001010.yaml',Compose([ Service( name='web', - build='.', depends_on=['db','redis'], extends=['redis'] ), @@ -275,7 +272,24 @@ from compose_viz.service import Service volumes=['db-data'], ), ])), - + ('tests/in/010001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + ), + ])), @@ -283,7 +297,6 @@ from compose_viz.service import Service ('tests/in/100000.yaml',Compose([ Service( name='web', - build='.', links=['db:database'], ), Service( From 012fd8d698c18ed16c733046e98738d4289cb14f Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 15:12:24 +0800 Subject: [PATCH 21/93] feat: add extends --- compose_viz/extends.py | 12 ++++++++++++ compose_viz/service.py | 4 +++- tests/test_extends.py | 6 ++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 compose_viz/extends.py create mode 100644 tests/test_extends.py diff --git a/compose_viz/extends.py b/compose_viz/extends.py new file mode 100644 index 0000000..0ec2903 --- /dev/null +++ b/compose_viz/extends.py @@ -0,0 +1,12 @@ +class Extends: + def __init__(self, from_file: str, service_name: str): + self._from_file = from_file + self._service_name = service_name + + @property + def from_file(self): + return self._from_file + + @property + def service_name(self): + return self._service_name \ No newline at end of file diff --git a/compose_viz/service.py b/compose_viz/service.py index b61ccff..acf28aa 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -1,8 +1,10 @@ from typing import List +from compose_viz.extends import Extends + class Service: - def __init__(self, name: str, image: str, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: List[str] = []) -> None: + def __init__(self, name: str, image: str = None, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: Extends = None) -> None: self._name = name self._image = image self._ports = ports diff --git a/tests/test_extends.py b/tests/test_extends.py new file mode 100644 index 0000000..3cf9c2f --- /dev/null +++ b/tests/test_extends.py @@ -0,0 +1,6 @@ +import pytest +from compose_viz.service import Service + +def test_parse_file(): + with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): + Service(name='frontend') From a58e81803320fdb736b70765be9b4a96215fa7d9 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 15:14:32 +0800 Subject: [PATCH 22/93] feat: imeplement value error in service initialization --- compose_viz/service.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/compose_viz/service.py b/compose_viz/service.py index acf28aa..f1da6a8 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -6,6 +6,10 @@ from compose_viz.extends import Extends class Service: def __init__(self, name: str, image: str = None, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: Extends = None) -> None: self._name = name + + if image is None and extends is None: + raise ValueError(f"Both image and extends are not defined in service '{name}', aborting.") + self._image = image self._ports = ports self._networks = networks From d1ae549d48e514babd060aafbec6e5c846557193 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 15:24:51 +0800 Subject: [PATCH 23/93] test: update test_extends.py --- tests/test_extends.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_extends.py b/tests/test_extends.py index 3cf9c2f..ce4662b 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -1,6 +1,19 @@ import pytest +from compose_viz.extends import Extends from compose_viz.service import Service -def test_parse_file(): +def test_extend_init(): + try: + Extends(service_name='frontend', from_file='tests/in/000001.yaml') + Extends(service_name='frontend') + + assert True + except: + assert False + + with pytest.raises(TypeError): + Extends(from_file='tests/in/000001.yaml') + +def test_service_init(): with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): Service(name='frontend') From 01aeb56189e1bf3c53889cacf4c7ef7588792702 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 15:26:03 +0800 Subject: [PATCH 24/93] chore: implement TypeError in init of Extends --- compose_viz/extends.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/compose_viz/extends.py b/compose_viz/extends.py index 0ec2903..fb3af9f 100644 --- a/compose_viz/extends.py +++ b/compose_viz/extends.py @@ -1,12 +1,12 @@ class Extends: - def __init__(self, from_file: str, service_name: str): - self._from_file = from_file + def __init__(self, service_name: str, from_file: str = None): self._service_name = service_name - - @property - def from_file(self): - return self._from_file + self._from_file = from_file @property def service_name(self): - return self._service_name \ No newline at end of file + return self._service_name + + @property + def from_file(self): + return self._from_file \ No newline at end of file From 0aa761db7c702f394a51631bcb87385ed821b4ca Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 16:03:33 +0800 Subject: [PATCH 25/93] test: change extends & add tests --- tests/test_parse_file.py | 142 +++++++++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 13 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 9ff4253..ed8185c 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -2,6 +2,7 @@ import pytest from compose_viz.parser import Parser from compose_viz.compose import Compose from compose_viz.service import Service +from compose_viz.extends import Extends @pytest.mark.parametrize('test_input,expected',[ ('tests/in/000001.yaml',Compose([ @@ -29,7 +30,7 @@ from compose_viz.service import Service Service( name='common', image='busybox', - extends=['base'], + extends=Extends(service_name='frontend'), ), Service( name='cli', @@ -46,13 +47,13 @@ from compose_viz.service import Service name='monitoring', image='awesome/monitoring', networks=['admin'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ), Service( name='backend', image='awesome/backend', networks=['back-tier', 'admin'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ), ])), ('tests/in/000100.yaml',Compose([ @@ -93,12 +94,12 @@ from compose_viz.service import Service Service( name='monitoring', image='awesome/monitoring', - extends=['frontend'], + extends=Extends(service_name='frontend'), ), Service( name='backend', image='awesome/backend', - extends=['frontend'], + extends=Extends(service_name='frontend'), ports=['8000:5001'], ), ])), @@ -106,21 +107,21 @@ from compose_viz.service import Service Service( name='frontend', image='awesome/webapp', - ports=['8000:5000'] + ports=['8000:5000'], networks=['front-tier', 'back-tier'], ), Service( name='monitoring', image='awesome/monitoring', networks=['admin'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ), Service( name='backend', image='awesome/backend', ports=['8000:5001'], networks=['back-tier', 'admin'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ), ])), ('tests/in/001000.yaml',Compose([ @@ -159,7 +160,7 @@ from compose_viz.service import Service Service( name='web', depends_on=['db','redis'], - extends=['redis'] + extends=Extends(service_name='redis'), ), Service( name='redis', @@ -176,7 +177,7 @@ from compose_viz.service import Service image='awesome/webapp', networks=['front-tier', 'back-tier'], depends_on=['monitoring','backend'], - extends=['backend'], + extends=Extends(service_name='backend'), ), Service( name='monitoring', @@ -236,7 +237,7 @@ from compose_viz.service import Service name='monitoring', image='awesome/monitoring', depends_on=['backend'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ports=['8000:5010'], ), Service( @@ -256,7 +257,7 @@ from compose_viz.service import Service image='awesome/monitoring', networks=['admin'], depends_on=['backend'], - extends=['frontend'], + extends=Extends(service_name='frontend'), ports=['8000:5010'], ), Service( @@ -290,10 +291,125 @@ from compose_viz.service import Service volumes=['db-data'], ), ])), + ('tests/in/010010.yaml',Compose([ + Service( + name='common', + image='busybox', + volumes=['common-volume'], + ), + Service( + name='cli', + extends=Extends(service_name='common'), + volumes=['cli-volume'], + ), + ])), + ('tests/in/010011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + extends=Extends(service_name='monitoring'), + ), + ])), + ('tests/in/010100.yaml',Compose([ + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + ports=["8000:5000"], + ), + ])), + ('tests/in/010111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + extends=Extends(service_name='monitoring'), + ports=['8000:5000'], + ), + ])), + ('tests/in/011100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + ), + ])), - + ('tests/in/011110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + ), + ])), + ('tests/in/011111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='monitoring'), + ports=['8000:5010'], + ), + ])), ('tests/in/100000.yaml',Compose([ Service( name='web', From af6284a111918ca851f4552595e241694bbe9ea8 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 16:08:15 +0800 Subject: [PATCH 26/93] test: fix typo --- tests/test_parse_file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index ed8185c..0e7d10e 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -58,6 +58,7 @@ from compose_viz.extends import Extends ])), ('tests/in/000100.yaml',Compose([ Service( + name='web', ports=['8000:5000'], ), Service( From c001420db0dfcb612ec51e95ca18ca4a5d816274 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 16:37:43 +0800 Subject: [PATCH 27/93] test: update test_service_init --- tests/test_extends.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_extends.py b/tests/test_extends.py index ce4662b..daab3c5 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -17,3 +17,6 @@ def test_extend_init(): def test_service_init(): with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): Service(name='frontend') + + with pytest.raises(ValueError, match=r"Only one of image and extends can be defined in service '{name}', aborting."): + Service(name='frontend', image='image', extends=Extends(service_name='frontend', from_file='tests/in/000001.yaml')) From 83db62937cf416afec08d15f765ad542fa5dc26c Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 16:39:43 +0800 Subject: [PATCH 28/93] fix: typo in test_service_init --- tests/test_extends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_extends.py b/tests/test_extends.py index daab3c5..4020277 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -18,5 +18,5 @@ def test_service_init(): with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): Service(name='frontend') - with pytest.raises(ValueError, match=r"Only one of image and extends can be defined in service '{name}', aborting."): + with pytest.raises(ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting."): Service(name='frontend', image='image', extends=Extends(service_name='frontend', from_file='tests/in/000001.yaml')) From 86853f4a3ee5b439ad41e1ba30b0a4efbae28025 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 16:40:20 +0800 Subject: [PATCH 29/93] feat: implement infeasible service initialization --- compose_viz/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compose_viz/service.py b/compose_viz/service.py index f1da6a8..feaaf53 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -10,6 +10,9 @@ class Service: if image is None and extends is None: raise ValueError(f"Both image and extends are not defined in service '{name}', aborting.") + if image is not None and extends is not None: + raise ValueError(f"Only one of image and extends can be defined in service '{name}', aborting.") + self._image = image self._ports = ports self._networks = networks From 3c5c80917017ddffa8d05912cb74c778ecca5a1a Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 17:26:43 +0800 Subject: [PATCH 30/93] test: add some tests --- tests/test_parse_file.py | 144 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 2 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 0e7d10e..7313365 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -331,7 +331,42 @@ from compose_viz.extends import Extends ports=["8000:5000"], ), ])), - + ('tests/in/010101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + ports=['8000:5000'], + ), + ])), + ('tests/in/010110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + extends=Extends(service_name='monitoring'), + ports=['8000:5000'], + ), + ])), ('tests/in/010111.yaml',Compose([ Service( name='frontend', @@ -352,7 +387,78 @@ from compose_viz.extends import Extends ports=['8000:5000'], ), ])), - + ('tests/in/011000.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + depends_on=['backend'], + volumes=['db-data'], + ), + Service( + name='backend', + image='awesome/backend', + ), + ])), + ('tests/in/011001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier','admin'], + volumes=['db-data'], + depends_on=['monitoring'], + ), + ])), + ('tests/in/011010.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ), + ])), + ('tests/in/011011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier','back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier','admin'], + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ), + ])), ('tests/in/011100.yaml',Compose([ Service( name='frontend', @@ -371,6 +477,40 @@ from compose_viz.extends import Extends ports=['8000:5010'], ), ])), + ('tests/in/011101.yaml',Compose([ + Service( + name='vote', + depends_on=['redis'], + volumes=['app'], + ports=['5000:80'], + networks=['front-tier','back-tier'], + ), + Service( + name='result', + depends_on=['db'], + volumes=['app'], + ports=['5001:80','5858:5858'], + networks=['front-tier','back-tier'], + ), + Service( + name='worker', + depends_on=['redis','db'], + networks=['back-tier'], + ), + Service( + name='redis', + image='redis:5.0-alpine3.10', + volumes=[''], ##### + ports=['6379'], + networks=['back-tier'], + ), + Service( + name='db', + image='postgres:9.4', + volumes=[''], ##### + networks=['back-tier'], + ), + ])), ('tests/in/011110.yaml',Compose([ Service( From 687c761139a71dac5bd61cf1a17b506aaaa2a41b Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 17:38:14 +0800 Subject: [PATCH 31/93] fix: infeasible tests --- tests/in/000010.yaml | 1 - tests/in/000011.yaml | 2 -- tests/in/000110.yaml | 2 -- tests/in/000111.yaml | 2 -- tests/in/001011.yaml | 1 - tests/in/001110.yaml | 2 -- tests/in/001111.yaml | 1 - tests/in/010011.yaml | 1 - tests/in/010110.yaml | 1 - tests/in/010111.yaml | 1 - tests/in/011010.yaml | 1 - tests/in/011011.yaml | 1 - tests/in/011110.yaml | 1 - tests/in/011111.yaml | 1 - tests/in/100010.yaml | 1 - tests/in/100011.yaml | 1 - tests/in/100110.yaml | 1 - tests/in/100111.yaml | 1 - tests/in/101010.yaml | 1 - tests/in/101011.yaml | 1 - tests/in/101110.yaml | 1 - tests/in/101111.yaml | 1 - tests/in/110010.yaml | 1 - tests/in/110011.yaml | 1 - tests/in/110110.yaml | 1 - tests/in/110111.yaml | 1 - tests/in/111010.yaml | 1 - tests/in/111011.yaml | 1 - tests/in/111110.yaml | 1 - tests/in/111111.yaml | 1 - 30 files changed, 34 deletions(-) diff --git a/tests/in/000010.yaml b/tests/in/000010.yaml index 1bb47e2..1d1084a 100644 --- a/tests/in/000010.yaml +++ b/tests/in/000010.yaml @@ -3,7 +3,6 @@ services: image: busybox user: root common: - image: busybox extends: service: base cli: diff --git a/tests/in/000011.yaml b/tests/in/000011.yaml index 2336456..515f31f 100644 --- a/tests/in/000011.yaml +++ b/tests/in/000011.yaml @@ -6,14 +6,12 @@ services: - back-tier monitoring: - image: awesome/monitoring networks: - admin extends: service: frontend backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/000110.yaml b/tests/in/000110.yaml index 9ac40e0..6eb8f19 100644 --- a/tests/in/000110.yaml +++ b/tests/in/000110.yaml @@ -5,12 +5,10 @@ services: - "8000:5000" monitoring: - image: awesome/monitoring extends: service: frontend backend: - image: awesome/backend extends: service: frontend ports: diff --git a/tests/in/000111.yaml b/tests/in/000111.yaml index 42fab6c..5195a82 100644 --- a/tests/in/000111.yaml +++ b/tests/in/000111.yaml @@ -8,14 +8,12 @@ services: - "8000:5000" monitoring: - image: awesome/monitoring networks: - admin extends: service: frontend backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/001011.yaml b/tests/in/001011.yaml index 7ef900c..bd1b850 100644 --- a/tests/in/001011.yaml +++ b/tests/in/001011.yaml @@ -1,6 +1,5 @@ services: frontend: - image: awesome/webapp networks: - front-tier - back-tier diff --git a/tests/in/001110.yaml b/tests/in/001110.yaml index 187d4b9..3b83196 100644 --- a/tests/in/001110.yaml +++ b/tests/in/001110.yaml @@ -5,7 +5,6 @@ services: - "8000:5000" monitoring: - image: awesome/monitoring depends_on: - backend extends: @@ -14,7 +13,6 @@ services: - "8000:5010" backend: - image: awesome/backend extends: service: frontend ports: diff --git a/tests/in/001111.yaml b/tests/in/001111.yaml index 332ed69..acd9932 100644 --- a/tests/in/001111.yaml +++ b/tests/in/001111.yaml @@ -6,7 +6,6 @@ services: - back-tier monitoring: - image: awesome/monitoring networks: - admin depends_on: diff --git a/tests/in/010011.yaml b/tests/in/010011.yaml index fad0350..0d36cf2 100644 --- a/tests/in/010011.yaml +++ b/tests/in/010011.yaml @@ -11,7 +11,6 @@ services: - admin backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/010110.yaml b/tests/in/010110.yaml index ef63463..bfbffd6 100644 --- a/tests/in/010110.yaml +++ b/tests/in/010110.yaml @@ -6,7 +6,6 @@ services: image: awesome/monitoring backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/010111.yaml b/tests/in/010111.yaml index b3d9888..6cb7eb8 100644 --- a/tests/in/010111.yaml +++ b/tests/in/010111.yaml @@ -11,7 +11,6 @@ services: - admin backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/011010.yaml b/tests/in/011010.yaml index a65c17e..6d09ea6 100644 --- a/tests/in/011010.yaml +++ b/tests/in/011010.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/011011.yaml b/tests/in/011011.yaml index 67bfde1..e98f01a 100644 --- a/tests/in/011011.yaml +++ b/tests/in/011011.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/011110.yaml b/tests/in/011110.yaml index 97f8c0d..928200f 100644 --- a/tests/in/011110.yaml +++ b/tests/in/011110.yaml @@ -9,7 +9,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/011111.yaml b/tests/in/011111.yaml index 797150d..1b0db20 100644 --- a/tests/in/011111.yaml +++ b/tests/in/011111.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/100010.yaml b/tests/in/100010.yaml index 5f72c8a..cbe9d92 100644 --- a/tests/in/100010.yaml +++ b/tests/in/100010.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend extends: service: frontend links: diff --git a/tests/in/100011.yaml b/tests/in/100011.yaml index bbd4fd2..8f0cd92 100644 --- a/tests/in/100011.yaml +++ b/tests/in/100011.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/100110.yaml b/tests/in/100110.yaml index 34b31ca..cfd53d9 100644 --- a/tests/in/100110.yaml +++ b/tests/in/100110.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend extends: service: frontend ports: diff --git a/tests/in/100111.yaml b/tests/in/100111.yaml index 3c718f8..25df8c8 100644 --- a/tests/in/100111.yaml +++ b/tests/in/100111.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/101010.yaml b/tests/in/101010.yaml index ec3ee66..a99477d 100644 --- a/tests/in/101010.yaml +++ b/tests/in/101010.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend depends_on: - monitoring links: diff --git a/tests/in/101011.yaml b/tests/in/101011.yaml index 3b42bb5..82e7c24 100644 --- a/tests/in/101011.yaml +++ b/tests/in/101011.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/101110.yaml b/tests/in/101110.yaml index 6428644..6468f37 100644 --- a/tests/in/101110.yaml +++ b/tests/in/101110.yaml @@ -8,7 +8,6 @@ services: backend: - image: awesome/backend depends_on: - monitoring extends: diff --git a/tests/in/101111.yaml b/tests/in/101111.yaml index ce845ca..81bd981 100644 --- a/tests/in/101111.yaml +++ b/tests/in/101111.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/110010.yaml b/tests/in/110010.yaml index 297dddd..477467a 100644 --- a/tests/in/110010.yaml +++ b/tests/in/110010.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/110011.yaml b/tests/in/110011.yaml index 8778ea3..412dba1 100644 --- a/tests/in/110011.yaml +++ b/tests/in/110011.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/110110.yaml b/tests/in/110110.yaml index 72f8a30..61b97d3 100644 --- a/tests/in/110110.yaml +++ b/tests/in/110110.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/110111.yaml b/tests/in/110111.yaml index 0bda8ef..8b7017c 100644 --- a/tests/in/110111.yaml +++ b/tests/in/110111.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/111010.yaml b/tests/in/111010.yaml index fcd05b9..8126c95 100644 --- a/tests/in/111010.yaml +++ b/tests/in/111010.yaml @@ -7,7 +7,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/111011.yaml b/tests/in/111011.yaml index 1fbf1b1..596aa40 100644 --- a/tests/in/111011.yaml +++ b/tests/in/111011.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: diff --git a/tests/in/111110.yaml b/tests/in/111110.yaml index 4250938..41d68d3 100644 --- a/tests/in/111110.yaml +++ b/tests/in/111110.yaml @@ -6,7 +6,6 @@ services: backend: - image: awesome/backend volumes: - type: volume source: db-data diff --git a/tests/in/111111.yaml b/tests/in/111111.yaml index e506c33..25d25a2 100644 --- a/tests/in/111111.yaml +++ b/tests/in/111111.yaml @@ -12,7 +12,6 @@ services: backend: - image: awesome/backend networks: back-tier: aliases: From 63556d734f3c23fdebbd8d3e6ea2beed8bb8efbc Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 18:01:35 +0800 Subject: [PATCH 32/93] fix: extends and image conflict problem --- tests/test_parse_file.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 7313365..b13b607 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -29,12 +29,11 @@ from compose_viz.extends import Extends ), Service( name='common', - image='busybox', extends=Extends(service_name='frontend'), ), Service( name='cli', - extends=['common'], + extends=Extends(service_name='common'), ), ])), ('tests/in/000011.yaml',Compose([ @@ -45,13 +44,11 @@ from compose_viz.extends import Extends ), Service( name='monitoring', - image='awesome/monitoring', networks=['admin'], extends=Extends(service_name='frontend'), ), Service( name='backend', - image='awesome/backend', networks=['back-tier', 'admin'], extends=Extends(service_name='frontend'), ), @@ -94,12 +91,10 @@ from compose_viz.extends import Extends ), Service( name='monitoring', - image='awesome/monitoring', extends=Extends(service_name='frontend'), ), Service( name='backend', - image='awesome/backend', extends=Extends(service_name='frontend'), ports=['8000:5001'], ), @@ -113,13 +108,11 @@ from compose_viz.extends import Extends ), Service( name='monitoring', - image='awesome/monitoring', networks=['admin'], extends=Extends(service_name='frontend'), ), Service( name='backend', - image='awesome/backend', ports=['8000:5001'], networks=['back-tier', 'admin'], extends=Extends(service_name='frontend'), @@ -175,7 +168,6 @@ from compose_viz.extends import Extends ('tests/in/001011.yaml',Compose([ Service( name='frontend', - image='awesome/webapp', networks=['front-tier', 'back-tier'], depends_on=['monitoring','backend'], extends=Extends(service_name='backend'), @@ -236,7 +228,6 @@ from compose_viz.extends import Extends ), Service( name='monitoring', - image='awesome/monitoring', depends_on=['backend'], extends=Extends(service_name='frontend'), ports=['8000:5010'], @@ -255,7 +246,6 @@ from compose_viz.extends import Extends ), Service( name='monitoring', - image='awesome/monitoring', networks=['admin'], depends_on=['backend'], extends=Extends(service_name='frontend'), @@ -317,7 +307,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', networks=['back-tier', 'admin'], volumes=['db-data'], extends=Extends(service_name='monitoring'), @@ -361,7 +350,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', volumes=['db-data'], extends=Extends(service_name='monitoring'), ports=['8000:5000'], @@ -380,7 +368,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', networks=['back-tier', 'admin'], volumes=['db-data'], extends=Extends(service_name='monitoring'), @@ -433,7 +420,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', volumes=['db-data'], depends_on=['monitoring'], extends=Extends(service_name='frontend'), @@ -452,7 +438,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', networks=['back-tier','admin'], volumes=['db-data'], depends_on=['monitoring'], @@ -470,7 +455,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', volumes=['db-data'], depends_on=['monitoring'], extends=Extends(service_name='frontend'), @@ -523,7 +507,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', volumes=['db-data'], depends_on=['monitoring'], extends=Extends(service_name='frontend'), @@ -543,7 +526,6 @@ from compose_viz.extends import Extends ), Service( name='backend', - image='awesome/backend', networks=['back-tier', 'admin'], volumes=['db-data'], depends_on=['monitoring'], From e630bb034cafcd975afb893e22f45f36ad7f78bd Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 19:49:33 +0800 Subject: [PATCH 33/93] fix: build from image --- tests/test_parse_file.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index b13b607..5bb6b2e 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -56,6 +56,7 @@ from compose_viz.extends import Extends ('tests/in/000100.yaml',Compose([ Service( name='web', + image='build from .', ports=['8000:5000'], ), Service( @@ -121,6 +122,7 @@ from compose_viz.extends import Extends ('tests/in/001000.yaml',Compose([ Service( name='web', + image='build from .', depends_on=['db','redis'], ), Service( @@ -153,6 +155,7 @@ from compose_viz.extends import Extends ('tests/in/001010.yaml',Compose([ Service( name='web', + image='build from .', depends_on=['db','redis'], extends=Extends(service_name='redis'), ), @@ -464,6 +467,7 @@ from compose_viz.extends import Extends ('tests/in/011101.yaml',Compose([ Service( name='vote', + image='build from ./', depends_on=['redis'], volumes=['app'], ports=['5000:80'], @@ -471,6 +475,7 @@ from compose_viz.extends import Extends ), Service( name='result', + image='build from ./', depends_on=['db'], volumes=['app'], ports=['5001:80','5858:5858'], @@ -478,6 +483,7 @@ from compose_viz.extends import Extends ), Service( name='worker', + image='build from ./', depends_on=['redis','db'], networks=['back-tier'], ), @@ -536,6 +542,7 @@ from compose_viz.extends import Extends ('tests/in/100000.yaml',Compose([ Service( name='web', + image='build from .', links=['db:database'], ), Service( From 1c82643dcad73fb1fa0b28d31e4039485e8f8b83 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 20:59:30 +0800 Subject: [PATCH 34/93] fix: build from image and extends conflict --- tests/in/001010.yaml | 1 - tests/test_parse_file.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/in/001010.yaml b/tests/in/001010.yaml index a598e6b..b65dbf3 100644 --- a/tests/in/001010.yaml +++ b/tests/in/001010.yaml @@ -1,6 +1,5 @@ services: web: - build: . depends_on: - db - redis diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 5bb6b2e..45acb7d 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -155,7 +155,6 @@ from compose_viz.extends import Extends ('tests/in/001010.yaml',Compose([ Service( name='web', - image='build from .', depends_on=['db','redis'], extends=Extends(service_name='redis'), ), From 366f605efb135ec90542f8b6f580887403e0c252 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 22:03:27 +0800 Subject: [PATCH 35/93] test: add rest of the tests --- tests/in/011101.yaml | 94 +++++++++++++++----------------------------- 1 file changed, 31 insertions(+), 63 deletions(-) diff --git a/tests/in/011101.yaml b/tests/in/011101.yaml index dd833f4..6e24fad 100644 --- a/tests/in/011101.yaml +++ b/tests/in/011101.yaml @@ -1,74 +1,42 @@ services: - vote: - build: ./ - # use python rather than gunicorn for local dev - command: python app.py - depends_on: - redis: - condition: service_healthy - volumes: - - app - ports: - - "5000:80" + frontend: + image: awesome/webapp networks: - front-tier - back-tier - result: - build: ./ - # use nodemon rather than node for local dev - command: nodemon server.js - depends_on: - db: - condition: service_healthy + monitoring: + image: awesome/monitoring + networks: + - admin + + + backend: + image: awesome/backend + networks: + back-tier: + aliases: + - database + admin: + aliases: + - mysql volumes: - - app + - type: volume + source: db-data + target: /data + volume: + nocopy: true + - type: bind + source: /var/run/postgres/postgres.sock + target: /var/run/postgres/postgres.sock + depends_on: + - monitoring ports: - - "5001:80" - - "5858:5858" - networks: - - front-tier - - back-tier - - worker: - build: - context: ./ - depends_on: - redis: - condition: service_healthy - db: - condition: service_healthy - networks: - - back-tier - - redis: - image: redis:5.0-alpine3.10 - volumes: - - "./healthchecks:/healthchecks" - healthcheck: - test: /healthchecks/redis.sh - interval: "5s" - ports: ["6379"] - networks: - - back-tier - - db: - image: postgres:9.4 - environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" - volumes: - - "db-data:/var/lib/postgresql/data" - - "./healthchecks:/healthchecks" - healthcheck: - test: /healthchecks/postgres.sh - interval: "5s" - networks: - - back-tier - -volumes: - db-data: + - "8000:5010" networks: front-tier: back-tier: + admin: +volumes: + db-data: From 04535bb8340a27da4c5cdfba696ad75f70540449 Mon Sep 17 00:00:00 2001 From: wolfyeva Date: Wed, 18 May 2022 22:03:52 +0800 Subject: [PATCH 36/93] test: added all of the test --- tests/test_parse_file.py | 728 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 698 insertions(+), 30 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 45acb7d..15f2dcc 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1,3 +1,4 @@ +from os import link import pytest from compose_viz.parser import Parser from compose_viz.compose import Compose @@ -465,42 +466,24 @@ from compose_viz.extends import Extends ])), ('tests/in/011101.yaml',Compose([ Service( - name='vote', - image='build from ./', - depends_on=['redis'], - volumes=['app'], - ports=['5000:80'], - networks=['front-tier','back-tier'], + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], ), Service( - name='result', - image='build from ./', - depends_on=['db'], - volumes=['app'], - ports=['5001:80','5858:5858'], - networks=['front-tier','back-tier'], + name='monitoring', + image='awesome/monitoring', + networks=['admin'], ), Service( - name='worker', - image='build from ./', - depends_on=['redis','db'], - networks=['back-tier'], - ), - Service( - name='redis', - image='redis:5.0-alpine3.10', - volumes=[''], ##### - ports=['6379'], - networks=['back-tier'], - ), - Service( - name='db', - image='postgres:9.4', - volumes=[''], ##### - networks=['back-tier'], + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + ports=['8000:5010'], ), ])), - ('tests/in/011110.yaml',Compose([ Service( name='frontend', @@ -549,6 +532,691 @@ from compose_viz.extends import Extends image='postgres', ), ])), + ('tests/in/100001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100010.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/100111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101000.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + depends_on=['monitoring'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + depends_on=['monitoring'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101010.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + depends_on=['monitoring'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + depends_on=['monitoring'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/101111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110000.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + volumes=['db-data'], + links=['db:database'], + ), + Service( + name='backend', + image='awesome/backend', + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110010.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + volumes=['db-data'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + volumes=['db-data'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/110111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111000.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + depends_on=['backend'], + volumes=['db-data'], + links=['db:database'], + ), + Service( + name='backend', + image='awesome/backend', + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111001.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111010.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111011.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111100.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + image='awesome/backend', + volumes=['db-data'], + depends_on=['monitoring'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111101.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111110.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + ), + Service( + name='monitoring', + image='awesome/monitoring', + ), + Service( + name='backend', + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), + ('tests/in/111111.yaml',Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + networks=['back-tier', 'admin'], + volumes=['db-data'], + depends_on=['monitoring'], + extends=Extends(service_name='frontend'), + ports=['8000:5010'], + links=['db:database'], + ), + Service( + name='db', + image='postgres', + ), + ])), ]) def test_parse_file(test_input, expected): From 16768590e4cb75ac5f315a20d70fc7b47cb687a9 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 22:47:54 +0800 Subject: [PATCH 37/93] test: update test_cli --- tests/test_cli.py | 78 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index bc0841e..128fb3b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import os +import pytest from typer.testing import CliRunner from compose_viz import cli @@ -6,11 +7,78 @@ from compose_viz import cli runner = CliRunner() - -def test_cli(): - input_path = "tests/in/000001.yaml" - result = runner.invoke(cli.app, [input_path]) +@pytest.mark.parametrize("file_number", [ + "000001", + "000010", + "000011", + "000100", + "000101", + "000110", + "000111", + "001000", + "001001", + "001010", + "001011", + "001100", + "001101", + "001110", + "001111", + "010000", + "010001", + "010010", + "010011", + "010100", + "010101", + "010110", + "010111", + "011000", + "011001", + "011010", + "011011", + "011100", + "011101", + "011110", + "011111", + "100000", + "100001", + "100010", + "100011", + "100100", + "100101", + "100110", + "100111", + "101000", + "101001", + "101010", + "101011", + "101100", + "101101", + "101110", + "101111", + "110000", + "110001", + "110010", + "110011", + "110100", + "110101", + "110110", + "110111", + "111000", + "111001", + "111010", + "111011", + "111100", + "111101", + "111110", + "111111", +]) +def test_cli(file_number: str): + input_path = f"tests/in/{file_number}.yaml" + output_path = f"{file_number}.png" + result = runner.invoke(cli.app, ["-o", output_path, input_path]) assert result.exit_code == 0 assert f"Successfully parsed {input_path}\n" in result.stdout - assert os.path.exists("compose-viz.png") + assert os.path.exists(output_path) + + os.remove(output_path) From 5f08a4f6bee05ff239bfccdfc76405c842d435cb Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 23:07:30 +0800 Subject: [PATCH 38/93] feat: add pre-commit hook --- .pre-commit-config.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8847423 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +exclude: | + (?x)^( + README.md| + LICENSE| + voice_presentation_control/vosk_models/ + ) +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: + - "--max-line-length=120" + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + args: # arguments to configure black + - --line-length=120 + - repo: local + hooks: + - id: pyright + name: pyright + entry: pyright + language: node + pass_filenames: false + types: [python] + additional_dependencies: ['pyright@1.1.247'] From a5f767b1701e2e7dd627e80a47add2cd04940a67 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 23:27:13 +0800 Subject: [PATCH 39/93] chore: remove unnecessary exclude path --- .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8847423..2fa471a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ exclude: | (?x)^( README.md| - LICENSE| - voice_presentation_control/vosk_models/ + LICENSE ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks From 1ec80883f0f827eaa74cc5d81a822e74fe253601 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 23:28:18 +0800 Subject: [PATCH 40/93] chore: apply pre-commit hooks --- .github/workflows/cd.yml | 4 +- .github/workflows/ci.yml | 8 +- .github/workflows/release-tagged-version.yml | 10 +- .gitignore | 2 +- compose_viz/__main__.py | 1 - compose_viz/cli.py | 8 +- compose_viz/compose.py | 1 + compose_viz/extends.py | 7 +- compose_viz/graph.py | 52 +- compose_viz/parser.py | 44 +- compose_viz/service.py | 14 +- tests/in/000001.yaml | 2 +- tests/in/000010.yaml | 2 +- tests/in/000011.yaml | 2 +- tests/in/000100.yaml | 2 +- tests/in/000101.yaml | 2 +- tests/in/000110.yaml | 2 +- tests/in/000111.yaml | 2 +- tests/in/001000.yaml | 2 +- tests/in/001001.yaml | 2 +- tests/in/001010.yaml | 2 +- tests/in/001011.yaml | 2 +- tests/in/001100.yaml | 2 +- tests/in/001101.yaml | 2 +- tests/in/001110.yaml | 2 +- tests/in/001111.yaml | 2 +- tests/in/010000.yaml | 2 +- tests/in/010001.yaml | 2 +- tests/in/010010.yaml | 2 +- tests/in/010011.yaml | 2 +- tests/in/010100.yaml | 2 +- tests/in/010101.yaml | 2 +- tests/in/010110.yaml | 2 +- tests/in/010111.yaml | 2 +- tests/in/011000.yaml | 2 +- tests/in/011001.yaml | 4 +- tests/in/011010.yaml | 4 +- tests/in/011011.yaml | 4 +- tests/in/011100.yaml | 2 +- tests/in/011101.yaml | 2 +- tests/in/011110.yaml | 4 +- tests/in/011111.yaml | 4 +- tests/in/100000.yaml | 2 +- tests/in/100001.yaml | 4 +- tests/in/100010.yaml | 2 +- tests/in/100011.yaml | 4 +- tests/in/100100.yaml | 2 +- tests/in/100101.yaml | 4 +- tests/in/100110.yaml | 2 +- tests/in/100111.yaml | 4 +- tests/in/101000.yaml | 2 +- tests/in/101001.yaml | 4 +- tests/in/101010.yaml | 2 +- tests/in/101011.yaml | 4 +- tests/in/101100.yaml | 2 +- tests/in/101101.yaml | 4 +- tests/in/101110.yaml | 2 +- tests/in/101111.yaml | 4 +- tests/in/110000.yaml | 2 +- tests/in/110001.yaml | 4 +- tests/in/110010.yaml | 4 +- tests/in/110011.yaml | 4 +- tests/in/110100.yaml | 4 +- tests/in/110101.yaml | 4 +- tests/in/110110.yaml | 4 +- tests/in/110111.yaml | 4 +- tests/in/111000.yaml | 2 +- tests/in/111001.yaml | 4 +- tests/in/111010.yaml | 4 +- tests/in/111011.yaml | 4 +- tests/in/111100.yaml | 4 +- tests/in/111101.yaml | 4 +- tests/in/111110.yaml | 4 +- tests/in/111111.yaml | 4 +- tests/in/docker-compose.yaml | 2 +- tests/test_cli.py | 138 +- tests/test_extends.py | 29 +- tests/test_parse_file.py | 2756 ++++++++++-------- tests/test_version.py | 2 +- 79 files changed, 1807 insertions(+), 1451 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 4377be7..d8ac70b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - + - name: Build and Publish to PyPi uses: JRubics/poetry-publish@v1.10 with: pypi_token: ${{ secrets.PYPI_TOKEN }} - extra_build_dependency_packages: "graphviz" \ No newline at end of file + extra_build_dependency_packages: "graphviz" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7f38c1..411ceeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,19 +12,19 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - + - name: Switch to Current Branch run: git checkout ${{ env.BRANCH }} - run: | sudo apt-get update sudo apt-get install -y graphviz - + - name: Setup Python 3.10.4 uses: actions/setup-python@v3 with: python-version: '3.10.4' - + - name: Validate Test Files run: | docker-compose -f tests/in/000001.yaml config -q @@ -90,7 +90,7 @@ jobs: docker-compose -f tests/in/111101.yaml config -q docker-compose -f tests/in/111110.yaml config -q docker-compose -f tests/in/111111.yaml config -q - + - name: Setup Poetry uses: Gr1N/setup-poetry@v7 with: diff --git a/.github/workflows/release-tagged-version.yml b/.github/workflows/release-tagged-version.yml index d10f37c..f2ce249 100644 --- a/.github/workflows/release-tagged-version.yml +++ b/.github/workflows/release-tagged-version.yml @@ -18,19 +18,19 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - + - name: Switch to Current Branch run: git checkout ${{ env.BRANCH }} - run: | sudo apt-get update sudo apt-get install -y graphviz - + - name: Setup Python 3.10.4 uses: actions/setup-python@v3 with: python-version: '3.10.4' - + - name: Setup Poetry uses: Gr1N/setup-poetry@v7 with: @@ -38,7 +38,7 @@ jobs: - run: | poetry install --no-root poetry build - + - name: "Release Tagged Version" uses: "marvinpinto/action-automatic-releases@latest" with: @@ -46,4 +46,4 @@ jobs: prerelease: false files: | LICENSE - dist/** \ No newline at end of file + dist/** diff --git a/.gitignore b/.gitignore index fa94aa8..5d7a886 100644 --- a/.gitignore +++ b/.gitignore @@ -158,4 +158,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/compose_viz/__main__.py b/compose_viz/__main__.py index 4065f3b..8ade738 100644 --- a/compose_viz/__main__.py +++ b/compose_viz/__main__.py @@ -1,5 +1,4 @@ from compose_viz.cli import start_cli - if __name__ == "__main__": start_cli() diff --git a/compose_viz/cli.py b/compose_viz/cli.py index 8cac6f4..e028d70 100644 --- a/compose_viz/cli.py +++ b/compose_viz/cli.py @@ -1,9 +1,11 @@ from enum import Enum -import typer from typing import Optional + +import typer + from compose_viz import __app_name__, __version__ -from compose_viz.parser import Parser from compose_viz.graph import Graph +from compose_viz.parser import Parser class VisualizationFormats(str, Enum): @@ -47,7 +49,7 @@ def compose_viz( help="Show the version of compose-viz.", callback=_version_callback, is_eager=True, - ) + ), ) -> None: parser = Parser() compose = parser.parse(input_path) diff --git a/compose_viz/compose.py b/compose_viz/compose.py index 4cafca1..0734342 100644 --- a/compose_viz/compose.py +++ b/compose_viz/compose.py @@ -1,4 +1,5 @@ from typing import List + from compose_viz.service import Service diff --git a/compose_viz/extends.py b/compose_viz/extends.py index fb3af9f..9948aa0 100644 --- a/compose_viz/extends.py +++ b/compose_viz/extends.py @@ -1,5 +1,8 @@ +from typing import Optional + + class Extends: - def __init__(self, service_name: str, from_file: str = None): + def __init__(self, service_name: str, from_file: Optional[str] = None): self._service_name = service_name self._from_file = from_file @@ -9,4 +12,4 @@ class Extends: @property def from_file(self): - return self._from_file \ No newline at end of file + return self._from_file diff --git a/compose_viz/graph.py b/compose_viz/graph.py index 9f8a7fa..09761cf 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -5,17 +5,17 @@ from compose_viz.compose import Compose def apply_vertex_style(type) -> dict: style = { - 'service': { - 'shape': 'component', + "service": { + "shape": "component", }, - 'volume': { - 'shape': 'folder', + "volume": { + "shape": "folder", }, - 'network': { - 'shape': 'pentagon', + "network": { + "shape": "pentagon", }, - 'port': { - 'shape': 'circle', + "port": { + "shape": "circle", }, } @@ -24,18 +24,18 @@ def apply_vertex_style(type) -> dict: def apply_edge_style(type) -> dict: style = { - 'ports': { - 'style': 'solid', + "ports": { + "style": "solid", }, - 'links': { - 'style': 'solid', + "links": { + "style": "solid", }, - 'volumes': { - 'style': 'dashed', + "volumes": { + "style": "dashed", + }, + "depends_on": { + "style": "dotted", }, - 'depends_on': { - 'style': 'dotted', - } } return style[type] @@ -44,7 +44,7 @@ def apply_edge_style(type) -> dict: class Graph: def __init__(self, compose: Compose, filename: str) -> None: self.dot = graphviz.Digraph() - self.dot.attr('graph', background='#ffffff', pad='0.5', ratio='fill') + self.dot.attr("graph", background="#ffffff", pad="0.5", ratio="fill") self.compose = compose self.filename = filename @@ -56,17 +56,17 @@ class Graph: def render(self, format: str, cleanup: bool = True) -> None: for service in self.compose.services: - self.add_vertex(service.name, 'service') + self.add_vertex(service.name, "service") for network in service.networks: - self.add_vertex("net#" + network, 'network') - self.add_edge(service.name, "net#" + network, 'links') + self.add_vertex("net#" + network, "network") + self.add_edge(service.name, "net#" + network, "links") for volume in service.volumes: - self.add_vertex(volume, 'volume') - self.add_edge(service.name, volume, 'links') + self.add_vertex(volume, "volume") + self.add_edge(service.name, volume, "links") for port in service.ports: - self.add_vertex(port, 'port') - self.add_edge(service.name, port, 'ports') + self.add_vertex(port, "port") + self.add_edge(service.name, port, "ports") for depends_on in service.depends_on: - self.dot.edge(depends_on, service.name, 'depends_on') + self.dot.edge(depends_on, service.name, "depends_on") self.dot.render(outfile=self.filename, format=format, cleanup=cleanup) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 6d345fb..be545e8 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,48 +1,60 @@ -from re import S -from compose_viz.compose import Compose -from compose_viz.compose import Service +from typing import List, Optional + from ruamel.yaml import YAML +from compose_viz.compose import Compose, Service + class Parser: def __init__(self): pass def parse(self, file_path: str) -> Compose: - # validate input file using `docker-compose config -q sys.argv[1]` first # load the yaml file with open(file_path, "r") as f: try: - yaml = YAML(typ='safe', pure=True) + yaml = YAML(typ="safe", pure=True) yaml_data = yaml.load(f) - except YAML.YAMLError as exc: - raise YAML.YAMLError + except Exception as e: + raise Exception(f"Error parsing file {file_path}: {e}") + # validate the yaml file if not yaml_data: print("Error: empty yaml file") raise ValueError + if not yaml_data.get("services"): print("Error: no services found") raise ValueError + # parse services data into Service objects services_data = yaml_data["services"] services = [] + for service, service_name in zip(services_data.values(), services_data.keys()): - #print("name: {}".format(service_name)) + # print("name: {}".format(service_name)) + + service_image: Optional[str] = None if service.get("image"): service_image = service["image"] - #print("image: {}".format(service_image)) + # print("image: {}".format(service_image)) + + service_networks: List[str] = [] if service.get("networks"): - if(type(service["networks"]) is list): + if type(service["networks"]) is list: service_networks = service["networks"] else: service_networks = list(service["networks"].keys()) - #print("networks: {}".format(service_networks)) - services.append(Service( - name=service_name, - image=service_image, - networks=service_networks, - )) + # print("networks: {}".format(service_networks)) + + services.append( + Service( + name=service_name, + image=service_image, + networks=service_networks, + ) + ) + # create Compose object compose = Compose(services) diff --git a/compose_viz/service.py b/compose_viz/service.py index feaaf53..336bab9 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -1,10 +1,20 @@ -from typing import List +from typing import List, Optional from compose_viz.extends import Extends class Service: - def __init__(self, name: str, image: str = None, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: Extends = None) -> None: + def __init__( + self, + name: str, + image: Optional[str] = None, + ports: List[str] = [], + networks: List[str] = [], + volumes: List[str] = [], + depends_on: List[str] = [], + links: List[str] = [], + extends: Optional[Extends] = None, + ) -> None: self._name = name if image is None and extends is None: diff --git a/tests/in/000001.yaml b/tests/in/000001.yaml index 1683eba..b6e30ec 100644 --- a/tests/in/000001.yaml +++ b/tests/in/000001.yaml @@ -23,4 +23,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/000010.yaml b/tests/in/000010.yaml index 1d1084a..377b994 100644 --- a/tests/in/000010.yaml +++ b/tests/in/000010.yaml @@ -7,4 +7,4 @@ services: service: base cli: extends: - service: common \ No newline at end of file + service: common diff --git a/tests/in/000011.yaml b/tests/in/000011.yaml index 515f31f..d17efbe 100644 --- a/tests/in/000011.yaml +++ b/tests/in/000011.yaml @@ -25,4 +25,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/000100.yaml b/tests/in/000100.yaml index 34c09b1..c68c007 100644 --- a/tests/in/000100.yaml +++ b/tests/in/000100.yaml @@ -5,4 +5,4 @@ services: ports: - "8000:5000" redis: - image: "redis:alpine" \ No newline at end of file + image: "redis:alpine" diff --git a/tests/in/000101.yaml b/tests/in/000101.yaml index 6d1ce63..6eb5b7f 100644 --- a/tests/in/000101.yaml +++ b/tests/in/000101.yaml @@ -29,4 +29,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/000110.yaml b/tests/in/000110.yaml index 6eb8f19..d67632a 100644 --- a/tests/in/000110.yaml +++ b/tests/in/000110.yaml @@ -12,4 +12,4 @@ services: extends: service: frontend ports: - - "8000:5001" \ No newline at end of file + - "8000:5001" diff --git a/tests/in/000111.yaml b/tests/in/000111.yaml index 5195a82..c17fb18 100644 --- a/tests/in/000111.yaml +++ b/tests/in/000111.yaml @@ -29,4 +29,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/001000.yaml b/tests/in/001000.yaml index 51adbed..f64c323 100644 --- a/tests/in/001000.yaml +++ b/tests/in/001000.yaml @@ -7,4 +7,4 @@ services: redis: image: redis db: - image: postgres \ No newline at end of file + image: postgres diff --git a/tests/in/001001.yaml b/tests/in/001001.yaml index 37a10e0..1e176a6 100644 --- a/tests/in/001001.yaml +++ b/tests/in/001001.yaml @@ -26,4 +26,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/001010.yaml b/tests/in/001010.yaml index b65dbf3..62f96e4 100644 --- a/tests/in/001010.yaml +++ b/tests/in/001010.yaml @@ -8,4 +8,4 @@ services: redis: image: redis db: - image: postgres \ No newline at end of file + image: postgres diff --git a/tests/in/001011.yaml b/tests/in/001011.yaml index bd1b850..f91c6b5 100644 --- a/tests/in/001011.yaml +++ b/tests/in/001011.yaml @@ -27,4 +27,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/001100.yaml b/tests/in/001100.yaml index e3219c8..ef3d14c 100644 --- a/tests/in/001100.yaml +++ b/tests/in/001100.yaml @@ -14,4 +14,4 @@ services: backend: image: awesome/backend ports: - - "8000:5001" \ No newline at end of file + - "8000:5001" diff --git a/tests/in/001101.yaml b/tests/in/001101.yaml index 417fe7f..04483a2 100644 --- a/tests/in/001101.yaml +++ b/tests/in/001101.yaml @@ -27,4 +27,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/001110.yaml b/tests/in/001110.yaml index 3b83196..75981f6 100644 --- a/tests/in/001110.yaml +++ b/tests/in/001110.yaml @@ -16,4 +16,4 @@ services: extends: service: frontend ports: - - "8000:5001" \ No newline at end of file + - "8000:5001" diff --git a/tests/in/001111.yaml b/tests/in/001111.yaml index acd9932..e328460 100644 --- a/tests/in/001111.yaml +++ b/tests/in/001111.yaml @@ -28,4 +28,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/010000.yaml b/tests/in/010000.yaml index b1c39d7..2b61286 100644 --- a/tests/in/010000.yaml +++ b/tests/in/010000.yaml @@ -12,4 +12,4 @@ services: target: /var/run/postgres/postgres.sock volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010001.yaml b/tests/in/010001.yaml index 74bd2ea..4361504 100644 --- a/tests/in/010001.yaml +++ b/tests/in/010001.yaml @@ -34,4 +34,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010010.yaml b/tests/in/010010.yaml index bd14c8f..9294993 100644 --- a/tests/in/010010.yaml +++ b/tests/in/010010.yaml @@ -10,4 +10,4 @@ services: - cli-volume:/var/lib/backup/data:ro volumes: common-volume: - cli-volume: \ No newline at end of file + cli-volume: diff --git a/tests/in/010011.yaml b/tests/in/010011.yaml index 0d36cf2..6337778 100644 --- a/tests/in/010011.yaml +++ b/tests/in/010011.yaml @@ -35,4 +35,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010100.yaml b/tests/in/010100.yaml index de377ee..a601ccd 100644 --- a/tests/in/010100.yaml +++ b/tests/in/010100.yaml @@ -13,4 +13,4 @@ services: ports: - "8000:5000" volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010101.yaml b/tests/in/010101.yaml index 86a28b1..098a6a9 100644 --- a/tests/in/010101.yaml +++ b/tests/in/010101.yaml @@ -36,4 +36,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010110.yaml b/tests/in/010110.yaml index bfbffd6..2727cf4 100644 --- a/tests/in/010110.yaml +++ b/tests/in/010110.yaml @@ -20,4 +20,4 @@ services: ports: - "8000:5000" volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/010111.yaml b/tests/in/010111.yaml index 6cb7eb8..19fb865 100644 --- a/tests/in/010111.yaml +++ b/tests/in/010111.yaml @@ -37,4 +37,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011000.yaml b/tests/in/011000.yaml index 4ef3c19..0e31582 100644 --- a/tests/in/011000.yaml +++ b/tests/in/011000.yaml @@ -19,4 +19,4 @@ services: backend: image: awesome/backend volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011001.yaml b/tests/in/011001.yaml index 023ab8c..495facc 100644 --- a/tests/in/011001.yaml +++ b/tests/in/011001.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -37,4 +37,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011010.yaml b/tests/in/011010.yaml index 6d09ea6..4578dc1 100644 --- a/tests/in/011010.yaml +++ b/tests/in/011010.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: volumes: @@ -22,4 +22,4 @@ services: service: frontend volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011011.yaml b/tests/in/011011.yaml index e98f01a..a263f0b 100644 --- a/tests/in/011011.yaml +++ b/tests/in/011011.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -38,4 +38,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011100.yaml b/tests/in/011100.yaml index dbebc65..2bb322e 100644 --- a/tests/in/011100.yaml +++ b/tests/in/011100.yaml @@ -25,4 +25,4 @@ services: ports: - "8000:5001" volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011101.yaml b/tests/in/011101.yaml index 6e24fad..8f366a2 100644 --- a/tests/in/011101.yaml +++ b/tests/in/011101.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend diff --git a/tests/in/011110.yaml b/tests/in/011110.yaml index 928200f..d0553d2 100644 --- a/tests/in/011110.yaml +++ b/tests/in/011110.yaml @@ -6,7 +6,7 @@ services: monitoring: image: awesome/monitoring - + backend: volumes: @@ -26,4 +26,4 @@ services: - "8000:5010" volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/011111.yaml b/tests/in/011111.yaml index 1b0db20..34389f4 100644 --- a/tests/in/011111.yaml +++ b/tests/in/011111.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -40,4 +40,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/100000.yaml b/tests/in/100000.yaml index c214153..7ea23ec 100644 --- a/tests/in/100000.yaml +++ b/tests/in/100000.yaml @@ -6,4 +6,4 @@ services: links: - "db:database" db: - image: postgres \ No newline at end of file + image: postgres diff --git a/tests/in/100001.yaml b/tests/in/100001.yaml index 82a5fa2..2c130d1 100644 --- a/tests/in/100001.yaml +++ b/tests/in/100001.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -28,4 +28,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/100010.yaml b/tests/in/100010.yaml index cbe9d92..b7d9de1 100644 --- a/tests/in/100010.yaml +++ b/tests/in/100010.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: extends: diff --git a/tests/in/100011.yaml b/tests/in/100011.yaml index 8f0cd92..7c4be01 100644 --- a/tests/in/100011.yaml +++ b/tests/in/100011.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -29,4 +29,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/100100.yaml b/tests/in/100100.yaml index 4640d65..e3b8a94 100644 --- a/tests/in/100100.yaml +++ b/tests/in/100100.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: image: awesome/backend diff --git a/tests/in/100101.yaml b/tests/in/100101.yaml index bf6cac2..a576314 100644 --- a/tests/in/100101.yaml +++ b/tests/in/100101.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -30,4 +30,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/100110.yaml b/tests/in/100110.yaml index cfd53d9..612d73f 100644 --- a/tests/in/100110.yaml +++ b/tests/in/100110.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: extends: diff --git a/tests/in/100111.yaml b/tests/in/100111.yaml index 25df8c8..4e1ca01 100644 --- a/tests/in/100111.yaml +++ b/tests/in/100111.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -31,4 +31,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/101000.yaml b/tests/in/101000.yaml index 72d5f8c..8f52814 100644 --- a/tests/in/101000.yaml +++ b/tests/in/101000.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: image: awesome/backend diff --git a/tests/in/101001.yaml b/tests/in/101001.yaml index a01f7ae..5fbf3d9 100644 --- a/tests/in/101001.yaml +++ b/tests/in/101001.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -30,4 +30,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/101010.yaml b/tests/in/101010.yaml index a99477d..1fe5809 100644 --- a/tests/in/101010.yaml +++ b/tests/in/101010.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: depends_on: diff --git a/tests/in/101011.yaml b/tests/in/101011.yaml index 82e7c24..f882bcd 100644 --- a/tests/in/101011.yaml +++ b/tests/in/101011.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -31,4 +31,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/101100.yaml b/tests/in/101100.yaml index b08993e..a7f17fe 100644 --- a/tests/in/101100.yaml +++ b/tests/in/101100.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: image: awesome/backend diff --git a/tests/in/101101.yaml b/tests/in/101101.yaml index 8b521fe..dc00820 100644 --- a/tests/in/101101.yaml +++ b/tests/in/101101.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -32,4 +32,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/101110.yaml b/tests/in/101110.yaml index 6468f37..e6cb223 100644 --- a/tests/in/101110.yaml +++ b/tests/in/101110.yaml @@ -5,7 +5,7 @@ services: monitoring: image: awesome/monitoring - + backend: depends_on: diff --git a/tests/in/101111.yaml b/tests/in/101111.yaml index 81bd981..b17b0d4 100644 --- a/tests/in/101111.yaml +++ b/tests/in/101111.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -33,4 +33,4 @@ services: networks: front-tier: back-tier: - admin: \ No newline at end of file + admin: diff --git a/tests/in/110000.yaml b/tests/in/110000.yaml index b07f8ff..f79c07a 100644 --- a/tests/in/110000.yaml +++ b/tests/in/110000.yaml @@ -21,4 +21,4 @@ services: db: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110001.yaml b/tests/in/110001.yaml index 6475de2..dab8541 100644 --- a/tests/in/110001.yaml +++ b/tests/in/110001.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -39,4 +39,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110010.yaml b/tests/in/110010.yaml index 477467a..8dcf22e 100644 --- a/tests/in/110010.yaml +++ b/tests/in/110010.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: volumes: @@ -24,4 +24,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110011.yaml b/tests/in/110011.yaml index 412dba1..2725ee8 100644 --- a/tests/in/110011.yaml +++ b/tests/in/110011.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -40,4 +40,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110100.yaml b/tests/in/110100.yaml index 9ef067c..ac0fc6c 100644 --- a/tests/in/110100.yaml +++ b/tests/in/110100.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: image: awesome/backend @@ -25,4 +25,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110101.yaml b/tests/in/110101.yaml index 2a8000c..48b9d1d 100644 --- a/tests/in/110101.yaml +++ b/tests/in/110101.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -41,4 +41,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110110.yaml b/tests/in/110110.yaml index 61b97d3..f9abebe 100644 --- a/tests/in/110110.yaml +++ b/tests/in/110110.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: volumes: @@ -26,4 +26,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/110111.yaml b/tests/in/110111.yaml index 8b7017c..76e6a4f 100644 --- a/tests/in/110111.yaml +++ b/tests/in/110111.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -42,4 +42,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111000.yaml b/tests/in/111000.yaml index 24483e4..bdcd442 100644 --- a/tests/in/111000.yaml +++ b/tests/in/111000.yaml @@ -23,4 +23,4 @@ services: db: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111001.yaml b/tests/in/111001.yaml index b6acfed..7586730 100644 --- a/tests/in/111001.yaml +++ b/tests/in/111001.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -41,4 +41,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111010.yaml b/tests/in/111010.yaml index 8126c95..7591564 100644 --- a/tests/in/111010.yaml +++ b/tests/in/111010.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: volumes: @@ -26,4 +26,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111011.yaml b/tests/in/111011.yaml index 596aa40..3714d62 100644 --- a/tests/in/111011.yaml +++ b/tests/in/111011.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -42,4 +42,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111100.yaml b/tests/in/111100.yaml index 23c650b..9a52b90 100644 --- a/tests/in/111100.yaml +++ b/tests/in/111100.yaml @@ -4,7 +4,7 @@ services: monitoring: image: awesome/monitoring - + backend: image: awesome/backend @@ -27,4 +27,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111101.yaml b/tests/in/111101.yaml index 79ebf03..4d6bead 100644 --- a/tests/in/111101.yaml +++ b/tests/in/111101.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend @@ -43,4 +43,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111110.yaml b/tests/in/111110.yaml index 41d68d3..540c9de 100644 --- a/tests/in/111110.yaml +++ b/tests/in/111110.yaml @@ -3,7 +3,7 @@ services: image: awesome/webapp monitoring: image: awesome/monitoring - + backend: volumes: @@ -27,4 +27,4 @@ services: image: postgres volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/111111.yaml b/tests/in/111111.yaml index 25d25a2..2ed41b9 100644 --- a/tests/in/111111.yaml +++ b/tests/in/111111.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: networks: @@ -44,4 +44,4 @@ networks: back-tier: admin: volumes: - db-data: \ No newline at end of file + db-data: diff --git a/tests/in/docker-compose.yaml b/tests/in/docker-compose.yaml index 3f94ba7..68e98a8 100644 --- a/tests/in/docker-compose.yaml +++ b/tests/in/docker-compose.yaml @@ -9,7 +9,7 @@ services: image: awesome/monitoring networks: - admin - + backend: image: awesome/backend diff --git a/tests/test_cli.py b/tests/test_cli.py index 128fb3b..fea0981 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,77 +1,81 @@ import os + import pytest - from typer.testing import CliRunner -from compose_viz import cli +from compose_viz import cli runner = CliRunner() -@pytest.mark.parametrize("file_number", [ - "000001", - "000010", - "000011", - "000100", - "000101", - "000110", - "000111", - "001000", - "001001", - "001010", - "001011", - "001100", - "001101", - "001110", - "001111", - "010000", - "010001", - "010010", - "010011", - "010100", - "010101", - "010110", - "010111", - "011000", - "011001", - "011010", - "011011", - "011100", - "011101", - "011110", - "011111", - "100000", - "100001", - "100010", - "100011", - "100100", - "100101", - "100110", - "100111", - "101000", - "101001", - "101010", - "101011", - "101100", - "101101", - "101110", - "101111", - "110000", - "110001", - "110010", - "110011", - "110100", - "110101", - "110110", - "110111", - "111000", - "111001", - "111010", - "111011", - "111100", - "111101", - "111110", - "111111", -]) + +@pytest.mark.parametrize( + "file_number", + [ + "000001", + "000010", + "000011", + "000100", + "000101", + "000110", + "000111", + "001000", + "001001", + "001010", + "001011", + "001100", + "001101", + "001110", + "001111", + "010000", + "010001", + "010010", + "010011", + "010100", + "010101", + "010110", + "010111", + "011000", + "011001", + "011010", + "011011", + "011100", + "011101", + "011110", + "011111", + "100000", + "100001", + "100010", + "100011", + "100100", + "100101", + "100110", + "100111", + "101000", + "101001", + "101010", + "101011", + "101100", + "101101", + "101110", + "101111", + "110000", + "110001", + "110010", + "110011", + "110100", + "110101", + "110110", + "110111", + "111000", + "111001", + "111010", + "111011", + "111100", + "111101", + "111110", + "111111", + ], +) def test_cli(file_number: str): input_path = f"tests/in/{file_number}.yaml" output_path = f"{file_number}.png" diff --git a/tests/test_extends.py b/tests/test_extends.py index 4020277..ffbe6f7 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -1,22 +1,29 @@ import pytest + from compose_viz.extends import Extends from compose_viz.service import Service - + + def test_extend_init(): try: - Extends(service_name='frontend', from_file='tests/in/000001.yaml') - Extends(service_name='frontend') + Extends(service_name="frontend", from_file="tests/in/000001.yaml") + Extends(service_name="frontend") assert True - except: - assert False + except Exception as e: + assert False, e with pytest.raises(TypeError): - Extends(from_file='tests/in/000001.yaml') - + Extends(from_file="tests/in/000001.yaml") # type: ignore + + def test_service_init(): with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): - Service(name='frontend') - - with pytest.raises(ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting."): - Service(name='frontend', image='image', extends=Extends(service_name='frontend', from_file='tests/in/000001.yaml')) + Service(name="frontend") + + with pytest.raises( + ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." + ): + Service( + name="frontend", image="image", extends=Extends(service_name="frontend", from_file="tests/in/000001.yaml") + ) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 15f2dcc..9272ffa 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1,1224 +1,1542 @@ -from os import link import pytest -from compose_viz.parser import Parser -from compose_viz.compose import Compose -from compose_viz.service import Service -from compose_viz.extends import Extends -@pytest.mark.parametrize('test_input,expected',[ - ('tests/in/000001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/000010.yaml',Compose([ - Service( - name='base', - image='busybox', - ), - Service( - name='common', - extends=Extends(service_name='frontend'), - ), - Service( - name='cli', - extends=Extends(service_name='common'), - ), - ])), - ('tests/in/000011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - networks=['admin'], - extends=Extends(service_name='frontend'), - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - extends=Extends(service_name='frontend'), - ), - ])), - ('tests/in/000100.yaml',Compose([ - Service( - name='web', - image='build from .', - ports=['8000:5000'], - ), - Service( - name='redis', - image='redis:alpine', - ), - ])), - ('tests/in/000101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ports=['8000:5000'], - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - ports=['8000:5001'], - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - ports=['8000:5010'], - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/000110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ports=['8000:5000'], - ), - Service( - name='monitoring', - extends=Extends(service_name='frontend'), - ), - Service( - name='backend', - extends=Extends(service_name='frontend'), - ports=['8000:5001'], - ), - ])), - ('tests/in/000111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ports=['8000:5000'], - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - networks=['admin'], - extends=Extends(service_name='frontend'), - ), - Service( - name='backend', - ports=['8000:5001'], - networks=['back-tier', 'admin'], - extends=Extends(service_name='frontend'), - ), - ])), - ('tests/in/001000.yaml',Compose([ - Service( - name='web', - image='build from .', - depends_on=['db','redis'], - ), - Service( - name='redis', - image='redis', - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/001001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - depends_on=['monitoring','backend'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/001010.yaml',Compose([ - Service( - name='web', - depends_on=['db','redis'], - extends=Extends(service_name='redis'), - ), - Service( - name='redis', - image='redis', - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/001011.yaml',Compose([ - Service( - name='frontend', - networks=['front-tier', 'back-tier'], - depends_on=['monitoring','backend'], - extends=Extends(service_name='backend'), - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/001100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ports=['8000:5000'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - depends_on=['backend'], - ports=['8000:5010'], - ), - Service( - name='backend', - image='awesome/backend', - ports=['8000:5001'], - ), - ])), - ('tests/in/001101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - depends_on=['backend'], - ports=['8000:5010'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/001110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ports=['8000:5000'], - ), - Service( - name='monitoring', - depends_on=['backend'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - ), - Service( - name='backend', - image='awesome/backend', - ports=['8000:5001'], - ), - ])), - ('tests/in/001111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - networks=['admin'], - depends_on=['backend'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ), - ])), - ('tests/in/010000.yaml',Compose([ - Service( - name='backend', - image='awesome/backend', - volumes=['db-data'], - ), - ])), - ('tests/in/010001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - ), - ])), - ('tests/in/010010.yaml',Compose([ - Service( - name='common', - image='busybox', - volumes=['common-volume'], - ), - Service( - name='cli', - extends=Extends(service_name='common'), - volumes=['cli-volume'], - ), - ])), - ('tests/in/010011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - extends=Extends(service_name='monitoring'), - ), - ])), - ('tests/in/010100.yaml',Compose([ - Service( - name='backend', - image='awesome/backend', - volumes=['db-data'], - ports=["8000:5000"], - ), - ])), - ('tests/in/010101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - ports=['8000:5000'], - ), - ])), - ('tests/in/010110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - extends=Extends(service_name='monitoring'), - ports=['8000:5000'], - ), - ])), - ('tests/in/010111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - extends=Extends(service_name='monitoring'), - ports=['8000:5000'], - ), - ])), - ('tests/in/011000.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - depends_on=['backend'], - volumes=['db-data'], - ), - Service( - name='backend', - image='awesome/backend', - ), - ])), - ('tests/in/011001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier','admin'], - volumes=['db-data'], - depends_on=['monitoring'], - ), - ])), - ('tests/in/011010.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ), - ])), - ('tests/in/011011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier','back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier','admin'], - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ), - ])), - ('tests/in/011100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - ), - ])), - ('tests/in/011101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - ports=['8000:5010'], - ), - ])), - ('tests/in/011110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - ), - ])), - ('tests/in/011111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='monitoring'), - ports=['8000:5010'], - ), - ])), - ('tests/in/100000.yaml',Compose([ - Service( - name='web', - image='build from .', - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100010.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - image='awesome/backend', - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/100111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101000.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - image='awesome/backend', - depends_on=['monitoring'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - depends_on=['monitoring'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101010.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - image='awesome/backend', - depends_on=['monitoring'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - depends_on=['monitoring'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/101111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110000.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - volumes=['db-data'], - links=['db:database'], - ), - Service( - name='backend', - image='awesome/backend', - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110010.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - image='awesome/backend', - volumes=['db-data'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/110111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111000.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - depends_on=['backend'], - volumes=['db-data'], - links=['db:database'], - ), - Service( - name='backend', - image='awesome/backend', - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111001.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111010.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111011.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111100.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - image='awesome/backend', - volumes=['db-data'], - depends_on=['monitoring'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111101.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - image='awesome/backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111110.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - ), - Service( - name='monitoring', - image='awesome/monitoring', - ), - Service( - name='backend', - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), - ('tests/in/111111.yaml',Compose([ - Service( - name='frontend', - image='awesome/webapp', - networks=['front-tier', 'back-tier'], - ), - Service( - name='monitoring', - image='awesome/monitoring', - networks=['admin'], - ), - Service( - name='backend', - networks=['back-tier', 'admin'], - volumes=['db-data'], - depends_on=['monitoring'], - extends=Extends(service_name='frontend'), - ports=['8000:5010'], - links=['db:database'], - ), - Service( - name='db', - image='postgres', - ), - ])), -]) - +from compose_viz.compose import Compose +from compose_viz.extends import Extends +from compose_viz.parser import Parser +from compose_viz.service import Service + + +@pytest.mark.parametrize( + "test_input,expected", + [ + ( + "tests/in/000001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/000010.yaml", + Compose( + [ + Service( + name="base", + image="busybox", + ), + Service( + name="common", + extends=Extends(service_name="frontend"), + ), + Service( + name="cli", + extends=Extends(service_name="common"), + ), + ] + ), + ), + ( + "tests/in/000011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + networks=["admin"], + extends=Extends(service_name="frontend"), + ), + Service( + name="backend", + networks=["back-tier", "admin"], + extends=Extends(service_name="frontend"), + ), + ] + ), + ), + ( + "tests/in/000100.yaml", + Compose( + [ + Service( + name="web", + image="build from .", + ports=["8000:5000"], + ), + Service( + name="redis", + image="redis:alpine", + ), + ] + ), + ), + ( + "tests/in/000101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ports=["8000:5000"], + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + ports=["8000:5001"], + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + ports=["8000:5010"], + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/000110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ports=["8000:5000"], + ), + Service( + name="monitoring", + extends=Extends(service_name="frontend"), + ), + Service( + name="backend", + extends=Extends(service_name="frontend"), + ports=["8000:5001"], + ), + ] + ), + ), + ( + "tests/in/000111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ports=["8000:5000"], + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + networks=["admin"], + extends=Extends(service_name="frontend"), + ), + Service( + name="backend", + ports=["8000:5001"], + networks=["back-tier", "admin"], + extends=Extends(service_name="frontend"), + ), + ] + ), + ), + ( + "tests/in/001000.yaml", + Compose( + [ + Service( + name="web", + image="build from .", + depends_on=["db", "redis"], + ), + Service( + name="redis", + image="redis", + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/001001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + depends_on=["monitoring", "backend"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/001010.yaml", + Compose( + [ + Service( + name="web", + depends_on=["db", "redis"], + extends=Extends(service_name="redis"), + ), + Service( + name="redis", + image="redis", + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/001011.yaml", + Compose( + [ + Service( + name="frontend", + networks=["front-tier", "back-tier"], + depends_on=["monitoring", "backend"], + extends=Extends(service_name="backend"), + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/001100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ports=["8000:5000"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + depends_on=["backend"], + ports=["8000:5010"], + ), + Service( + name="backend", + image="awesome/backend", + ports=["8000:5001"], + ), + ] + ), + ), + ( + "tests/in/001101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + depends_on=["backend"], + ports=["8000:5010"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/001110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ports=["8000:5000"], + ), + Service( + name="monitoring", + depends_on=["backend"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + ), + Service( + name="backend", + image="awesome/backend", + ports=["8000:5001"], + ), + ] + ), + ), + ( + "tests/in/001111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + networks=["admin"], + depends_on=["backend"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ), + ] + ), + ), + ( + "tests/in/010000.yaml", + Compose( + [ + Service( + name="backend", + image="awesome/backend", + volumes=["db-data"], + ), + ] + ), + ), + ( + "tests/in/010001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + ), + ] + ), + ), + ( + "tests/in/010010.yaml", + Compose( + [ + Service( + name="common", + image="busybox", + volumes=["common-volume"], + ), + Service( + name="cli", + extends=Extends(service_name="common"), + volumes=["cli-volume"], + ), + ] + ), + ), + ( + "tests/in/010011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + extends=Extends(service_name="monitoring"), + ), + ] + ), + ), + ( + "tests/in/010100.yaml", + Compose( + [ + Service( + name="backend", + image="awesome/backend", + volumes=["db-data"], + ports=["8000:5000"], + ), + ] + ), + ), + ( + "tests/in/010101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + ports=["8000:5000"], + ), + ] + ), + ), + ( + "tests/in/010110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + extends=Extends(service_name="monitoring"), + ports=["8000:5000"], + ), + ] + ), + ), + ( + "tests/in/010111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + extends=Extends(service_name="monitoring"), + ports=["8000:5000"], + ), + ] + ), + ), + ( + "tests/in/011000.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + depends_on=["backend"], + volumes=["db-data"], + ), + Service( + name="backend", + image="awesome/backend", + ), + ] + ), + ), + ( + "tests/in/011001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + ), + ] + ), + ), + ( + "tests/in/011010.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ), + ] + ), + ), + ( + "tests/in/011011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ), + ] + ), + ), + ( + "tests/in/011100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + ), + ] + ), + ), + ( + "tests/in/011101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + ports=["8000:5010"], + ), + ] + ), + ), + ( + "tests/in/011110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + ), + ] + ), + ), + ( + "tests/in/011111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="monitoring"), + ports=["8000:5010"], + ), + ] + ), + ), + ( + "tests/in/100000.yaml", + Compose( + [ + Service( + name="web", + image="build from .", + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100010.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + image="awesome/backend", + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/100111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101000.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + image="awesome/backend", + depends_on=["monitoring"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + depends_on=["monitoring"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101010.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + image="awesome/backend", + depends_on=["monitoring"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + depends_on=["monitoring"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/101111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110000.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + volumes=["db-data"], + links=["db:database"], + ), + Service( + name="backend", + image="awesome/backend", + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110010.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + image="awesome/backend", + volumes=["db-data"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/110111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111000.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + depends_on=["backend"], + volumes=["db-data"], + links=["db:database"], + ), + Service( + name="backend", + image="awesome/backend", + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111001.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111010.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111011.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111100.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + image="awesome/backend", + volumes=["db-data"], + depends_on=["monitoring"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111101.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + image="awesome/backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111110.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + ), + Service( + name="monitoring", + image="awesome/monitoring", + ), + Service( + name="backend", + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ( + "tests/in/111111.yaml", + Compose( + [ + Service( + name="frontend", + image="awesome/webapp", + networks=["front-tier", "back-tier"], + ), + Service( + name="monitoring", + image="awesome/monitoring", + networks=["admin"], + ), + Service( + name="backend", + networks=["back-tier", "admin"], + volumes=["db-data"], + depends_on=["monitoring"], + extends=Extends(service_name="frontend"), + ports=["8000:5010"], + links=["db:database"], + ), + Service( + name="db", + image="postgres", + ), + ] + ), + ), + ], +) def test_parse_file(test_input, expected): parser = Parser() actual = parser.parse(test_input) diff --git a/tests/test_version.py b/tests/test_version.py index 1d2abd6..a3bd0eb 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,6 +1,6 @@ from typer.testing import CliRunner -from compose_viz import cli, __app_name__, __version__ +from compose_viz import __app_name__, __version__, cli runner = CliRunner() From c1e7a28af04bf7258bb6a1f3b8d90c07b36d682f Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 23:33:56 +0800 Subject: [PATCH 41/93] ci: add pre-commit hooks --- .github/workflows/ci.yml | 8 ++- poetry.lock | 151 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + tests/test_parse_file.py | 2 +- 4 files changed, 158 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 411ceeb..7238bf9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,10 @@ jobs: run: | poetry install --no-root - - name: Validate Custom Input File + - name: Execute pre-commit run: | - poetry run python -m pytest + poetry run python -m pre_commit run --all-files --show-diff-on-failure + + - name: Run Pytest + run: | + poetry run python -m pytest --tb=line diff --git a/poetry.lock b/poetry.lock index a3222f9..bf2bd9d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,14 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + [[package]] name = "click" version = "8.1.3" @@ -39,6 +47,26 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.7.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "graphviz" version = "0.20" @@ -52,6 +80,17 @@ dev = ["tox (>=3)", "flake8", "pep8-naming", "wheel", "twine"] docs = ["sphinx (>=4)", "sphinx-autodoc-typehints", "sphinx-rtd-theme"] test = ["pytest (>=7)", "pytest-mock (>=3)", "mock (>=4)", "pytest-cov", "coverage"] +[[package]] +name = "identify" +version = "2.5.0" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -60,6 +99,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "21.3" @@ -71,6 +118,18 @@ python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -83,6 +142,22 @@ python-versions = ">=3.6" dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "2.19.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + [[package]] name = "py" version = "1.11.0" @@ -154,6 +229,22 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "tomli" version = "2.0.1" @@ -179,10 +270,28 @@ dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"] +[[package]] +name = "virtualenv" +version = "20.14.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "f189e4baf985a53a0388422f970dd8c481332fe1d9f18d7d2bd06e4904c49bdf" +content-hash = "e4dfedc159ef4b0088a5694e59b21c62b16ed06dca7ef14f7fc422b6db7b414f" [metadata.files] atomicwrites = [ @@ -193,6 +302,10 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -201,22 +314,46 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] +filelock = [ + {file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"}, + {file = "filelock-3.7.0.tar.gz", hash = "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20"}, +] graphviz = [ {file = "graphviz-0.20-py3-none-any.whl", hash = "sha256:62c5f48bcc534a45b4588c548ff75e419c1f1f3a33d31a91796ae80a7f581e4a"}, {file = "graphviz-0.20.zip", hash = "sha256:76bdfb73f42e72564ffe9c7299482f9d72f8e6cb8d54bce7b48ab323755e9ba5"}, ] +identify = [ + {file = "identify-2.5.0-py2.py3-none-any.whl", hash = "sha256:3acfe15a96e4272b4ec5662ee3e231ceba976ef63fd9980ed2ce9cc415df393f"}, + {file = "identify-2.5.0.tar.gz", hash = "sha256:c83af514ea50bf2be2c4a3f2fb349442b59dc87284558ae9ff54191bff3541d2"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] +pre-commit = [ + {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, + {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -295,6 +432,14 @@ pyyaml = [ {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, @@ -303,3 +448,7 @@ typer = [ {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, ] +virtualenv = [ + {file = "virtualenv-20.14.1-py2.py3-none-any.whl", hash = "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a"}, + {file = "virtualenv-20.14.1.tar.gz", hash = "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"}, +] diff --git a/pyproject.toml b/pyproject.toml index 97d774c..512740c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ graphviz = "^0.20" [tool.poetry.dev-dependencies] pytest = "^7.1.2" +pre-commit = "^2.19.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 9272ffa..1c6150c 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -7,7 +7,7 @@ from compose_viz.service import Service @pytest.mark.parametrize( - "test_input,expected", + "test_input, expected", [ ( "tests/in/000001.yaml", From 431e540d5dd1e6991379d7657918f68d070f1b2e Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 18 May 2022 23:50:30 +0800 Subject: [PATCH 42/93] ci: set pytest log to short to prevent redundant messages --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7238bf9..29d7a2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,4 +106,4 @@ jobs: - name: Run Pytest run: | - poetry run python -m pytest --tb=line + poetry run python -m pytest --tb=short From 650fc9dc46cb8ebdc420c298de416f7e80eb4aec Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:34:26 +0800 Subject: [PATCH 43/93] test: parser exceptions --- .pre-commit-config.yaml | 3 ++- tests/in/000000.yaml | 0 tests/in/invalid.yaml | 3 +++ tests/in/no-services.yaml | 3 +++ tests/test_extends.py | 13 ------------- tests/test_parser.py | 18 ++++++++++++++++++ tests/test_service.py | 16 ++++++++++++++++ 7 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 tests/in/000000.yaml create mode 100644 tests/in/invalid.yaml create mode 100644 tests/in/no-services.yaml create mode 100644 tests/test_parser.py create mode 100644 tests/test_service.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2fa471a..29b731d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,8 @@ exclude: | (?x)^( README.md| - LICENSE + LICENSE| + tests/in/invalid.yaml ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/tests/in/000000.yaml b/tests/in/000000.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/in/invalid.yaml b/tests/in/invalid.yaml new file mode 100644 index 0000000..8e7a231 --- /dev/null +++ b/tests/in/invalid.yaml @@ -0,0 +1,3 @@ +what-is-this: + "an invalid yaml" + "test purpose" diff --git a/tests/in/no-services.yaml b/tests/in/no-services.yaml new file mode 100644 index 0000000..a0bf4a1 --- /dev/null +++ b/tests/in/no-services.yaml @@ -0,0 +1,3 @@ +what-is-this: + - "a yaml file without services" + - "test purpose" diff --git a/tests/test_extends.py b/tests/test_extends.py index ffbe6f7..8b28cd1 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -1,7 +1,6 @@ import pytest from compose_viz.extends import Extends -from compose_viz.service import Service def test_extend_init(): @@ -15,15 +14,3 @@ def test_extend_init(): with pytest.raises(TypeError): Extends(from_file="tests/in/000001.yaml") # type: ignore - - -def test_service_init(): - with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): - Service(name="frontend") - - with pytest.raises( - ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." - ): - Service( - name="frontend", image="image", extends=Extends(service_name="frontend", from_file="tests/in/000001.yaml") - ) diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..e28a9f9 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,18 @@ +import pytest + +from compose_viz.parser import Parser + + +def test_parser_error_parsing_file(): + with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/in/invalid.yaml'.*"): + Parser().parse("tests/in/invalid.yaml") + + +def test_parser_invalid_yaml(): + with pytest.raises(RuntimeError, match=r"Empty yaml file, aborting."): + Parser().parse("tests/in/000000.yaml") + + +def test_parser_no_services_found(): + with pytest.raises(RuntimeError, match=r"No services found, aborting."): + Parser().parse("tests/in/no-services.yaml") diff --git a/tests/test_service.py b/tests/test_service.py new file mode 100644 index 0000000..4006e2d --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,16 @@ +import pytest + +from compose_viz.extends import Extends +from compose_viz.service import Service + + +def test_service_init(): + with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): + Service(name="frontend") + + with pytest.raises( + ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." + ): + Service( + name="frontend", image="image", extends=Extends(service_name="frontend", from_file="tests/in/000001.yaml") + ) From f864346828508bae12738f4e9c1e9a649f3d40eb Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:34:53 +0800 Subject: [PATCH 44/93] feat: implement parser exceptions --- compose_viz/parser.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index be545e8..fe90d56 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -16,16 +16,14 @@ class Parser: yaml = YAML(typ="safe", pure=True) yaml_data = yaml.load(f) except Exception as e: - raise Exception(f"Error parsing file {file_path}: {e}") + raise RuntimeError(f"Error parsing file '{file_path}': {e}") # validate the yaml file if not yaml_data: - print("Error: empty yaml file") - raise ValueError + raise RuntimeError("Empty yaml file, aborting.") if not yaml_data.get("services"): - print("Error: no services found") - raise ValueError + raise RuntimeError("No services found, aborting.") # parse services data into Service objects services_data = yaml_data["services"] From ecdc76a2a1fcce9b89d5d8a336219ced47c1a711 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:38:42 +0800 Subject: [PATCH 45/93] style: separate different tests --- tests/test_extends.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_extends.py b/tests/test_extends.py index 8b28cd1..a655be7 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -3,14 +3,24 @@ import pytest from compose_viz.extends import Extends -def test_extend_init(): +def test_extend_init_normal(): try: Extends(service_name="frontend", from_file="tests/in/000001.yaml") + + assert True + except Exception as e: + assert False, e + + +def test_extend_init_without_from_file(): + try: Extends(service_name="frontend") assert True except Exception as e: assert False, e + +def test_extend_init_without_service_name(): with pytest.raises(TypeError): Extends(from_file="tests/in/000001.yaml") # type: ignore From 5349155622910a4614336144d042f05771d85a23 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:41:08 +0800 Subject: [PATCH 46/93] chore: add type annotations in tests --- tests/test_cli.py | 2 +- tests/test_extends.py | 6 +++--- tests/test_parse_file.py | 2 +- tests/test_parser.py | 6 +++--- tests/test_service.py | 2 +- tests/test_version.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index fea0981..45a640d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -76,7 +76,7 @@ runner = CliRunner() "111111", ], ) -def test_cli(file_number: str): +def test_cli(file_number: str) -> None: input_path = f"tests/in/{file_number}.yaml" output_path = f"{file_number}.png" result = runner.invoke(cli.app, ["-o", output_path, input_path]) diff --git a/tests/test_extends.py b/tests/test_extends.py index a655be7..ef8f755 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -3,7 +3,7 @@ import pytest from compose_viz.extends import Extends -def test_extend_init_normal(): +def test_extend_init_normal() -> None: try: Extends(service_name="frontend", from_file="tests/in/000001.yaml") @@ -12,7 +12,7 @@ def test_extend_init_normal(): assert False, e -def test_extend_init_without_from_file(): +def test_extend_init_without_from_file() -> None: try: Extends(service_name="frontend") @@ -21,6 +21,6 @@ def test_extend_init_without_from_file(): assert False, e -def test_extend_init_without_service_name(): +def test_extend_init_without_service_name() -> None: with pytest.raises(TypeError): Extends(from_file="tests/in/000001.yaml") # type: ignore diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 1c6150c..f91249d 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1537,7 +1537,7 @@ from compose_viz.service import Service ), ], ) -def test_parse_file(test_input, expected): +def test_parse_file(test_input: str, expected: Compose) -> None: parser = Parser() actual = parser.parse(test_input) diff --git a/tests/test_parser.py b/tests/test_parser.py index e28a9f9..e541a70 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,16 +3,16 @@ import pytest from compose_viz.parser import Parser -def test_parser_error_parsing_file(): +def test_parser_error_parsing_file() -> None: with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/in/invalid.yaml'.*"): Parser().parse("tests/in/invalid.yaml") -def test_parser_invalid_yaml(): +def test_parser_invalid_yaml() -> None: with pytest.raises(RuntimeError, match=r"Empty yaml file, aborting."): Parser().parse("tests/in/000000.yaml") -def test_parser_no_services_found(): +def test_parser_no_services_found() -> None: with pytest.raises(RuntimeError, match=r"No services found, aborting."): Parser().parse("tests/in/no-services.yaml") diff --git a/tests/test_service.py b/tests/test_service.py index 4006e2d..223f471 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -4,7 +4,7 @@ from compose_viz.extends import Extends from compose_viz.service import Service -def test_service_init(): +def test_service_init() -> None: with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): Service(name="frontend") diff --git a/tests/test_version.py b/tests/test_version.py index a3bd0eb..27ecc67 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -5,7 +5,7 @@ from compose_viz import __app_name__, __version__, cli runner = CliRunner() -def test_version(): +def test_version() -> None: result = runner.invoke(cli.app, ["--version"]) assert result.exit_code == 0 From 82e79514447258f4e1ed64b3fdba6267431d082c Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:43:45 +0800 Subject: [PATCH 47/93] test: change ValueError to AttributeError in Service initialization --- tests/test_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_service.py b/tests/test_service.py index 223f471..1f56b33 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -5,11 +5,13 @@ from compose_viz.service import Service def test_service_init() -> None: - with pytest.raises(ValueError, match=r"Both image and extends are not defined in service 'frontend', aborting."): + with pytest.raises( + AttributeError, match=r"Both image and extends are not defined in service 'frontend', aborting." + ): Service(name="frontend") with pytest.raises( - ValueError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." + AttributeError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." ): Service( name="frontend", image="image", extends=Extends(service_name="frontend", from_file="tests/in/000001.yaml") From b091f416990b3bc6c36760a6ee12345ea92c4059 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 00:44:18 +0800 Subject: [PATCH 48/93] feat: implement AttributeError in Service initialization --- compose_viz/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose_viz/service.py b/compose_viz/service.py index 336bab9..b8cb99a 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -18,10 +18,10 @@ class Service: self._name = name if image is None and extends is None: - raise ValueError(f"Both image and extends are not defined in service '{name}', aborting.") + raise AttributeError(f"Both image and extends are not defined in service '{name}', aborting.") if image is not None and extends is not None: - raise ValueError(f"Only one of image and extends can be defined in service '{name}', aborting.") + raise AttributeError(f"Only one of image and extends can be defined in service '{name}', aborting.") self._image = image self._ports = ports From 524f608fe55ebf6ee11acb116ee143b452e80649 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 15:29:37 +0800 Subject: [PATCH 49/93] feat: show pytest coverage in ci --- .github/workflows/ci.yml | 2 +- poetry.lock | 78 +++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d7a2d..912e441 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,4 +106,4 @@ jobs: - name: Run Pytest run: | - poetry run python -m pytest --tb=short + poetry run python -m pytest --cov=compose_viz tests/ --tb=short diff --git a/poetry.lock b/poetry.lock index bf2bd9d..b4a11f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -47,6 +47,20 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.3.3" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "distlib" version = "0.3.4" @@ -198,6 +212,21 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + [[package]] name = "pyyaml" version = "6.0" @@ -291,7 +320,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "e4dfedc159ef4b0088a5694e59b21c62b16ed06dca7ef14f7fc422b6db7b414f" +content-hash = "4d670a3b1f70618579557a2b2d831bc0ec95d69e47120665db819400fb3b4682" [metadata.files] atomicwrites = [ @@ -314,6 +343,49 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-6.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df32ee0f4935a101e4b9a5f07b617d884a531ed5666671ff6ac66d2e8e8246d8"}, + {file = "coverage-6.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75b5dbffc334e0beb4f6c503fb95e6d422770fd2d1b40a64898ea26d6c02742d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:114944e6061b68a801c5da5427b9173a0dd9d32cd5fcc18a13de90352843737d"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab88a01cd180b5640ccc9c47232e31924d5f9967ab7edd7e5c91c68eee47a69"}, + {file = "coverage-6.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad8f9068f5972a46d50fe5f32c09d6ee11da69c560fcb1b4c3baea246ca4109b"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4cd696aa712e6cd16898d63cf66139dc70d998f8121ab558f0e1936396dbc579"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c1a9942e282cc9d3ed522cd3e3cab081149b27ea3bda72d6f61f84eaf88c1a63"}, + {file = "coverage-6.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c06455121a089252b5943ea682187a4e0a5cf0a3fb980eb8e7ce394b144430a9"}, + {file = "coverage-6.3.3-cp310-cp310-win32.whl", hash = "sha256:cb5311d6ccbd22578c80028c5e292a7ab9adb91bd62c1982087fad75abe2e63d"}, + {file = "coverage-6.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:6d4a6f30f611e657495cc81a07ff7aa8cd949144e7667c5d3e680d73ba7a70e4"}, + {file = "coverage-6.3.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:79bf405432428e989cad7b8bc60581963238f7645ae8a404f5dce90236cc0293"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:338c417613f15596af9eb7a39353b60abec9d8ce1080aedba5ecee6a5d85f8d3"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db094a6a4ae6329ed322a8973f83630b12715654c197dd392410400a5bfa1a73"}, + {file = "coverage-6.3.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1414e8b124611bf4df8d77215bd32cba6e3425da8ce9c1f1046149615e3a9a31"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:93b16b08f94c92cab88073ffd185070cdcb29f1b98df8b28e6649145b7f2c90d"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fbc86ae8cc129c801e7baaafe3addf3c8d49c9c1597c44bdf2d78139707c3c62"}, + {file = "coverage-6.3.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b5ba058610e8289a07db2a57bce45a1793ec0d3d11db28c047aae2aa1a832572"}, + {file = "coverage-6.3.3-cp37-cp37m-win32.whl", hash = "sha256:8329635c0781927a2c6ae068461e19674c564e05b86736ab8eb29c420ee7dc20"}, + {file = "coverage-6.3.3-cp37-cp37m-win_amd64.whl", hash = "sha256:e5af1feee71099ae2e3b086ec04f57f9950e1be9ecf6c420696fea7977b84738"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e814a4a5a1d95223b08cdb0f4f57029e8eab22ffdbae2f97107aeef28554517e"}, + {file = "coverage-6.3.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61f4fbf3633cb0713437291b8848634ea97f89c7e849c2be17a665611e433f53"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3401b0d2ed9f726fadbfa35102e00d1b3547b73772a1de5508ef3bdbcb36afe7"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8586b177b4407f988731eb7f41967415b2197f35e2a6ee1a9b9b561f6323c8e9"}, + {file = "coverage-6.3.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892e7fe32191960da559a14536768a62e83e87bbb867e1b9c643e7e0fbce2579"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:afb03f981fadb5aed1ac6e3dd34f0488e1a0875623d557b6fad09b97a942b38a"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cbe91bc84be4e5ef0b1480d15c7b18e29c73bdfa33e07d3725da7d18e1b0aff2"}, + {file = "coverage-6.3.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:91502bf27cbd5c83c95cfea291ef387469f2387508645602e1ca0fd8a4ba7548"}, + {file = "coverage-6.3.3-cp38-cp38-win32.whl", hash = "sha256:c488db059848702aff30aa1d90ef87928d4e72e4f00717343800546fdbff0a94"}, + {file = "coverage-6.3.3-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6534fcdfb5c503affb6b1130db7b5bfc8a0f77fa34880146f7a5c117987d0"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc692c9ee18f0dd3214843779ba6b275ee4bb9b9a5745ba64265bce911aefd1a"}, + {file = "coverage-6.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:462105283de203df8de58a68c1bb4ba2a8a164097c2379f664fa81d6baf94b81"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc972d829ad5ef4d4c5fcabd2bbe2add84ce8236f64ba1c0c72185da3a273130"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06f54765cdbce99901871d50fe9f41d58213f18e98b170a30ca34f47de7dd5e8"}, + {file = "coverage-6.3.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7835f76a081787f0ca62a53504361b3869840a1620049b56d803a8cb3a9eeea3"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6f5fee77ec3384b934797f1873758f796dfb4f167e1296dc00f8b2e023ce6ee9"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:baa8be8aba3dd1e976e68677be68a960a633a6d44c325757aefaa4d66175050f"}, + {file = "coverage-6.3.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4d06380e777dd6b35ee936f333d55b53dc4a8271036ff884c909cf6e94be8b6c"}, + {file = "coverage-6.3.3-cp39-cp39-win32.whl", hash = "sha256:f8cabc5fd0091976ab7b020f5708335033e422de25e20ddf9416bdce2b7e07d8"}, + {file = "coverage-6.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c9441d57b0963cf8340268ad62fc83de61f1613034b79c2b1053046af0c5284"}, + {file = "coverage-6.3.3-pp36.pp37.pp38-none-any.whl", hash = "sha256:d522f1dc49127eab0bfbba4e90fa068ecff0899bbf61bf4065c790ddd6c177fe"}, + {file = "coverage-6.3.3.tar.gz", hash = "sha256:2781c43bffbbec2b8867376d4d61916f5e9c4cc168232528562a61d1b4b01879"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -366,6 +438,10 @@ pytest = [ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, diff --git a/pyproject.toml b/pyproject.toml index 512740c..95718f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ graphviz = "^0.20" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pre-commit = "^2.19.0" +pytest-cov = "^3.0.0" [build-system] requires = ["poetry-core>=1.0.0"] From 85d8b02a8b91c950e0d3aecfc46a821443ced0b1 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 15:37:49 +0800 Subject: [PATCH 50/93] fix: missing dev dependency --- poetry.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index b4a11f3..6043af8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -320,7 +320,7 @@ testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "4d670a3b1f70618579557a2b2d831bc0ec95d69e47120665db819400fb3b4682" +content-hash = "e1b68a4c83f398e841e6f38823ea18f3cb27b0b251689d0398ae39c03ddc4a47" [metadata.files] atomicwrites = [ diff --git a/pyproject.toml b/pyproject.toml index 95718f8..888fddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ graphviz = "^0.20" [tool.poetry.dev-dependencies] pytest = "^7.1.2" pre-commit = "^2.19.0" +coverage = "^6.3.3" pytest-cov = "^3.0.0" [build-system] From 4ca486bc2568ad04dd747eb3310cff64e624be63 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 16:03:05 +0800 Subject: [PATCH 51/93] chore: remove unused method --- compose_viz/compose.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/compose_viz/compose.py b/compose_viz/compose.py index 0734342..03e8f74 100644 --- a/compose_viz/compose.py +++ b/compose_viz/compose.py @@ -7,9 +7,6 @@ class Compose: def __init__(self, services: List[Service]) -> None: self._services = services - def extract_networks(self) -> List[str]: - raise NotImplementedError - @property def services(self): return self._services From 77db4f578e6a2f0d10afa0651a003cbd063a5d6a Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Thu, 19 May 2022 16:06:16 +0800 Subject: [PATCH 52/93] test: add test_module.py --- tests/test_module.py | 5 +++++ tests/test_parse_file.py | 7 ++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/test_module.py diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 0000000..136b7a9 --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,5 @@ +import os + + +def test_module(): + assert os.system("python -m compose_viz") == 0 diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index f91249d..56f8b21 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -1551,4 +1551,9 @@ def test_parse_file(test_input: str, expected: Compose) -> None: assert actual_service.volumes == expected_service.volumes assert actual_service.depends_on == expected_service.depends_on assert actual_service.links == expected_service.links - assert actual_service.extends == expected_service.extends + + assert (actual_service.extends is not None) == (expected_service.extends is not None) + + if (actual_service.extends is not None) and (expected_service.extends is not None): + assert actual_service.extends.service_name == expected_service.extends.service_name + assert actual_service.extends.from_file == expected_service.extends.from_file From 5eae1907a0b1ec27468a2d5c50e20fc6ba3556a5 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 14:32:07 +0800 Subject: [PATCH 53/93] chore: implement service_ewtends parse --- compose_viz/parser.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index fe90d56..85ea484 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -3,7 +3,7 @@ from typing import List, Optional from ruamel.yaml import YAML from compose_viz.compose import Compose, Service - +from compose_viz.extends import Extends class Parser: def __init__(self): @@ -30,12 +30,12 @@ class Parser: services = [] for service, service_name in zip(services_data.values(), services_data.keys()): - # print("name: {}".format(service_name)) + print("name: {}".format(service_name)) service_image: Optional[str] = None if service.get("image"): service_image = service["image"] - # print("image: {}".format(service_image)) + print("image: {}".format(service_image)) service_networks: List[str] = [] if service.get("networks"): @@ -43,13 +43,25 @@ class Parser: service_networks = service["networks"] else: service_networks = list(service["networks"].keys()) - # print("networks: {}".format(service_networks)) + print("networks: {}".format(service_networks)) + + service_image: Optional[str] = None + if service.get("image"): + service_image = service["image"] + print("image: {}".format(service_image)) + + service_extends: Optional[Extends] = None + if service.get("extends"): + service_extends = Extends(service_name=service["extends"]["service"]) + print("extends: {}".format(service_extends)) + services.append( Service( name=service_name, image=service_image, networks=service_networks, + extends=service_extends ) ) From de762d79b4623f6cddda24a3b74e547c6fab9539 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 15:38:59 +0800 Subject: [PATCH 54/93] fix: wrong test expect value --- tests/test_parse_file.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 56f8b21..338ff26 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -41,7 +41,7 @@ from compose_viz.service import Service ), Service( name="common", - extends=Extends(service_name="frontend"), + extends=Extends(service_name="base"), ), Service( name="cli", @@ -596,13 +596,13 @@ from compose_viz.service import Service Service( name="monitoring", image="awesome/monitoring", + volumes=["db-data"], + depends_on=["backend"], ), Service( name="backend", - volumes=["db-data"], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=["8000:5010"], + image="awesome/backend", + ports=["8000:5001"], ), ] ), From 9f9733bec97485635ccd4961ab8b6ceb30a5e856 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 17:25:56 +0800 Subject: [PATCH 55/93] test: add test_volume.py --- tests/test_extends.py | 10 ++++++---- tests/test_volume.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 tests/test_volume.py diff --git a/tests/test_extends.py b/tests/test_extends.py index ef8f755..0bc6a0c 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -5,18 +5,20 @@ from compose_viz.extends import Extends def test_extend_init_normal() -> None: try: - Extends(service_name="frontend", from_file="tests/in/000001.yaml") + e = Extends(service_name="frontend", from_file="tests/in/000001.yaml") - assert True + assert e.service_name == "frontend" + assert e.from_file == "tests/in/000001.yaml" except Exception as e: assert False, e def test_extend_init_without_from_file() -> None: try: - Extends(service_name="frontend") + e = Extends(service_name="frontend") - assert True + assert e.service_name == "frontend" + assert e.from_file is None except Exception as e: assert False, e diff --git a/tests/test_volume.py b/tests/test_volume.py new file mode 100644 index 0000000..a58b56e --- /dev/null +++ b/tests/test_volume.py @@ -0,0 +1,23 @@ +from compose_viz.volume import Volume, VolumeType + + +def test_extend_init_normal() -> None: + try: + v = Volume(source="./foo", target="./bar") + + assert v.source == "./foo" + assert v.target == "./bar" + assert v.type == VolumeType.volume + except Exception as e: + assert False, e + + +def test_extend_with_type() -> None: + try: + v = Volume(source="./foo", target="./bar", type=VolumeType.bind) + + assert v.source == "./foo" + assert v.target == "./bar" + assert v.type == VolumeType.bind + except Exception as e: + assert False, e From d5a9bf3f722d7d0a23bbc9c208903f36492f600a Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 17:41:26 +0800 Subject: [PATCH 56/93] feat: implement Volume --- compose_viz/graph.py | 4 +- compose_viz/service.py | 3 +- compose_viz/volume.py | 26 ++++ tests/test_parse_file.py | 292 ++++++++++++++++++++++++++++++++++----- 4 files changed, 288 insertions(+), 37 deletions(-) create mode 100644 compose_viz/volume.py diff --git a/compose_viz/graph.py b/compose_viz/graph.py index 09761cf..ec75c34 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -61,8 +61,8 @@ class Graph: self.add_vertex("net#" + network, "network") self.add_edge(service.name, "net#" + network, "links") for volume in service.volumes: - self.add_vertex(volume, "volume") - self.add_edge(service.name, volume, "links") + self.add_vertex(volume.source, "volume") + self.add_edge(service.name, volume.source, "links") for port in service.ports: self.add_vertex(port, "port") self.add_edge(service.name, port, "ports") diff --git a/compose_viz/service.py b/compose_viz/service.py index b8cb99a..4e47d92 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -1,6 +1,7 @@ from typing import List, Optional from compose_viz.extends import Extends +from compose_viz.volume import Volume class Service: @@ -10,7 +11,7 @@ class Service: image: Optional[str] = None, ports: List[str] = [], networks: List[str] = [], - volumes: List[str] = [], + volumes: List[Volume] = [], depends_on: List[str] = [], links: List[str] = [], extends: Optional[Extends] = None, diff --git a/compose_viz/volume.py b/compose_viz/volume.py new file mode 100644 index 0000000..d90faa9 --- /dev/null +++ b/compose_viz/volume.py @@ -0,0 +1,26 @@ +from enum import Enum + + +class VolumeType(str, Enum): + volume = "volume" + bind = "bind" + tmpfs = "tmpfs" + + +class Volume: + def __init__(self, source: str, target: str, type: VolumeType = VolumeType.volume): + self._source = source + self._target = target + self._type = type + + @property + def source(self): + return self._source + + @property + def target(self): + return self._target + + @property + def type(self): + return self._type diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 338ff26..751d267 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -4,6 +4,7 @@ from compose_viz.compose import Compose from compose_viz.extends import Extends from compose_viz.parser import Parser from compose_viz.service import Service +from compose_viz.volume import Volume, VolumeType @pytest.mark.parametrize( @@ -345,7 +346,14 @@ from compose_viz.service import Service Service( name="backend", image="awesome/backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ), ] ), @@ -368,7 +376,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ), ] ), @@ -380,12 +395,12 @@ from compose_viz.service import Service Service( name="common", image="busybox", - volumes=["common-volume"], + volumes=[Volume(source="common-volume", target="/var/lib/backup/data:rw")], ), Service( name="cli", extends=Extends(service_name="common"), - volumes=["cli-volume"], + volumes=[Volume(source="cli-volume", target="/var/lib/backup/data:ro")], ), ] ), @@ -407,7 +422,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="monitoring"), ), ] @@ -420,7 +442,14 @@ from compose_viz.service import Service Service( name="backend", image="awesome/backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ports=["8000:5000"], ), ] @@ -444,7 +473,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ports=["8000:5000"], ), ] @@ -464,7 +500,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="monitoring"), ports=["8000:5000"], ), @@ -488,7 +531,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="monitoring"), ports=["8000:5000"], ), @@ -507,7 +557,14 @@ from compose_viz.service import Service name="monitoring", image="awesome/monitoring", depends_on=["backend"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ), Service( name="backend", @@ -534,7 +591,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], ), ] @@ -554,7 +618,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), ), @@ -578,7 +649,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), ), @@ -596,7 +674,14 @@ from compose_viz.service import Service Service( name="monitoring", image="awesome/monitoring", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["backend"], ), Service( @@ -625,7 +710,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], ports=["8000:5010"], ), @@ -646,7 +738,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), ports=["8000:5010"], @@ -671,7 +770,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="monitoring"), ports=["8000:5010"], @@ -1106,7 +1212,14 @@ from compose_viz.service import Service Service( name="monitoring", image="awesome/monitoring", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], links=["db:database"], ), Service( @@ -1138,7 +1251,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], links=["db:database"], ), Service( @@ -1162,7 +1282,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="frontend"), links=["db:database"], ), @@ -1190,7 +1317,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="frontend"), links=["db:database"], ), @@ -1216,7 +1350,14 @@ from compose_viz.service import Service Service( name="backend", image="awesome/backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ports=["8000:5010"], links=["db:database"], ), @@ -1245,7 +1386,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], ports=["8000:5010"], links=["db:database"], ), @@ -1270,7 +1418,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="frontend"), ports=["8000:5010"], links=["db:database"], @@ -1299,7 +1454,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], extends=Extends(service_name="frontend"), ports=["8000:5010"], links=["db:database"], @@ -1323,7 +1485,14 @@ from compose_viz.service import Service name="monitoring", image="awesome/monitoring", depends_on=["backend"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], links=["db:database"], ), Service( @@ -1355,7 +1524,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], links=["db:database"], ), @@ -1380,7 +1556,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), links=["db:database"], @@ -1409,7 +1592,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), links=["db:database"], @@ -1436,7 +1626,14 @@ from compose_viz.service import Service Service( name="backend", image="awesome/backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], ports=["8000:5010"], links=["db:database"], @@ -1466,7 +1663,14 @@ from compose_viz.service import Service name="backend", image="awesome/backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], ports=["8000:5010"], links=["db:database"], @@ -1492,7 +1696,14 @@ from compose_viz.service import Service ), Service( name="backend", - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), ports=["8000:5010"], @@ -1522,7 +1733,14 @@ from compose_viz.service import Service Service( name="backend", networks=["back-tier", "admin"], - volumes=["db-data"], + volumes=[ + Volume(source="db-data", target="/data"), + Volume( + source="/var/run/postgres/postgres.sock", + target="/var/run/postgres/postgres.sock", + type=VolumeType.bind, + ), + ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), ports=["8000:5010"], @@ -1548,7 +1766,13 @@ def test_parse_file(test_input: str, expected: Compose) -> None: assert actual_service.image == expected_service.image assert actual_service.ports == expected_service.ports assert actual_service.networks == expected_service.networks - assert actual_service.volumes == expected_service.volumes + + assert len(actual_service.volumes) == len(expected_service.volumes) + for actual_volume, expected_volume in zip(actual_service.volumes, expected_service.volumes): + assert actual_volume.source == expected_volume.source + assert actual_volume.target == expected_volume.target + assert actual_volume.type == expected_volume.type + assert actual_service.depends_on == expected_service.depends_on assert actual_service.links == expected_service.links From 2911dde5880c8cd79b9d35d40c2ceed2f5f176ec Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 17:47:35 +0800 Subject: [PATCH 57/93] chore: apply pre-commit hooks --- compose_viz/parser.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 85ea484..bfac019 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -5,6 +5,7 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends + class Parser: def __init__(self): pass @@ -44,7 +45,7 @@ class Parser: else: service_networks = list(service["networks"].keys()) print("networks: {}".format(service_networks)) - + service_image: Optional[str] = None if service.get("image"): service_image = service["image"] @@ -54,15 +55,9 @@ class Parser: if service.get("extends"): service_extends = Extends(service_name=service["extends"]["service"]) print("extends: {}".format(service_extends)) - services.append( - Service( - name=service_name, - image=service_image, - networks=service_networks, - extends=service_extends - ) + Service(name=service_name, image=service_image, networks=service_networks, extends=service_extends) ) # create Compose object From 6a8be74675254c5660b8c355356f15893b17b9d9 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 15:52:11 +0800 Subject: [PATCH 58/93] feat: add build from image path --- compose_viz/parser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index bfac019..d3a9afa 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -37,6 +37,9 @@ class Parser: if service.get("image"): service_image = service["image"] print("image: {}".format(service_image)) + elif service.get("build"): + service_image = "build from " + service["build"] + print("image: {}".format(service_image)) service_networks: List[str] = [] if service.get("networks"): From daaeba4ee07ae9aa7ebdd5a873faad8e27eb78f8 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 16:08:35 +0800 Subject: [PATCH 59/93] chore: implement service ports parse --- compose_viz/parser.py | 58 +++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index d3a9afa..b46cb9f 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -18,28 +18,31 @@ class Parser: yaml_data = yaml.load(f) except Exception as e: raise RuntimeError(f"Error parsing file '{file_path}': {e}") - # validate the yaml file if not yaml_data: raise RuntimeError("Empty yaml file, aborting.") - if not yaml_data.get("services"): raise RuntimeError("No services found, aborting.") # parse services data into Service objects services_data = yaml_data["services"] + services = self.parse_service_data(yaml_data["services"]) + + # create Compose object + compose = Compose(services) + + return compose + + def parse_service_data(self, services_yaml_data: List): services = [] - - for service, service_name in zip(services_data.values(), services_data.keys()): - print("name: {}".format(service_name)) - + for service, service_name in zip(services_yaml_data.values(), services_yaml_data.keys()): + service_image: Optional[str] = None if service.get("image"): service_image = service["image"] - print("image: {}".format(service_image)) elif service.get("build"): service_image = "build from " + service["build"] - print("image: {}".format(service_image)) + service_networks: List[str] = [] if service.get("networks"): @@ -47,23 +50,40 @@ class Parser: service_networks = service["networks"] else: service_networks = list(service["networks"].keys()) - print("networks: {}".format(service_networks)) - - service_image: Optional[str] = None - if service.get("image"): - service_image = service["image"] - print("image: {}".format(service_image)) + service_extends: Optional[Extends] = None if service.get("extends"): service_extends = Extends(service_name=service["extends"]["service"]) - print("extends: {}".format(service_extends)) + + + service_ports: List[str] = [] + if service.get("ports"): + service_ports = service["ports"] + + service_depends_on: List[str] = [] + if service.get("depends_on"): + service_depends_on = service["depends_on"] services.append( - Service(name=service_name, image=service_image, networks=service_networks, extends=service_extends) + Service( + name=service_name, + image=service_image, + networks=service_networks, + extends=service_extends, + ports=service_ports, + depends_on=service_depends_on, + ) ) + # Service print debug + #print("--------------------") + #print("Service name: {}".format(service_name)) + #print("image: {}".format(service_image)) + #print("networks: {}".format(service_networks)) + #print("image: {}".format(service_image)) + #print("extends: {}".format(service_extends)) + #print("ports: {}".format(service_ports)) + #print("depends: {}".format(service_depends_on)) - # create Compose object - compose = Compose(services) - return compose + return services From b877dad590797233e66e9985342524d6afe56110 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 16:33:04 +0800 Subject: [PATCH 60/93] fix: fix 001110 test case error --- tests/test_parse_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 751d267..8524e58 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -309,7 +309,7 @@ from compose_viz.volume import Volume, VolumeType ), Service( name="backend", - image="awesome/backend", + extends=Extends(service_name="frontend"), ports=["8000:5001"], ), ] From 78d7d3af28b04d0df1a16f5b83d71ae34b4ec2b2 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 18:07:30 +0800 Subject: [PATCH 61/93] chore: refactor parser parse --- compose_viz/parser.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index b46cb9f..8a711cf 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -5,6 +5,15 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends +class service_parse_rule: + def __init__( + self, + name: str, parse_path: List[str], + target: List[str] + ) -> None: + self.name = name + self.parse_path = parse_path + self.target = target class Parser: def __init__(self): From 2b22de5193ecbbb83e8fc742785103732f7e7800 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 18:17:37 +0800 Subject: [PATCH 62/93] chore: delete service_parse_rule class --- compose_viz/parser.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 8a711cf..b46cb9f 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -5,15 +5,6 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends -class service_parse_rule: - def __init__( - self, - name: str, parse_path: List[str], - target: List[str] - ) -> None: - self.name = name - self.parse_path = parse_path - self.target = target class Parser: def __init__(self): From 114f778727b055febcc274bd34defc3e6595af91 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 21:57:18 +0800 Subject: [PATCH 63/93] feat: implment Parser parse service volumes --- compose_viz/parser.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index b46cb9f..b9959fa 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -4,7 +4,7 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends - +from compose_viz.volume import Volume, VolumeType class Parser: def __init__(self): @@ -65,6 +65,22 @@ class Parser: if service.get("depends_on"): service_depends_on = service["depends_on"] + service_volumes: List[Volume] = [] + if service.get("volumes"): + for volume_data in service["volumes"]: + if type(volume_data) is dict: + volume_source = volume_data["source"] + volume_target = volume_data["target"] + volume_type = VolumeType[volume_data["type"]] + service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) + elif type(volume_data) is str: + spilt_data = volume_data.split(":",1) + volume_source = spilt_data[0] + volume_target = spilt_data[1] + service_volumes.append(Volume(source=volume_source, target=volume_target)) + + + services.append( Service( name=service_name, @@ -73,6 +89,7 @@ class Parser: extends=service_extends, ports=service_ports, depends_on=service_depends_on, + volumes=service_volumes, ) ) # Service print debug From 87422471be9fe76ba4b039aaff962d75d1270d51 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 22:00:38 +0800 Subject: [PATCH 64/93] feat: implment Parser parse link --- compose_viz/parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index b9959fa..04a40be 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -79,7 +79,9 @@ class Parser: volume_target = spilt_data[1] service_volumes.append(Volume(source=volume_source, target=volume_target)) - + service_links: List[str] = [] + if service.get("links"): + service_links = service["links"] services.append( Service( @@ -90,6 +92,7 @@ class Parser: ports=service_ports, depends_on=service_depends_on, volumes=service_volumes, + links=service_links ) ) # Service print debug From 56d8c6aaa7e32630283b9300e542aca1d6a5f7d0 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 22:13:48 +0800 Subject: [PATCH 65/93] fix: fix 011100 test case error --- tests/test_parse_file.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 8524e58..f9f63ad 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -670,10 +670,12 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", + ports=["8000:5000"], ), Service( name="monitoring", image="awesome/monitoring", + ports=["8000:5010"], volumes=[ Volume(source="db-data", target="/data"), Volume( From 603b9cb299eede7b4ae056479fc3bef42eafa396 Mon Sep 17 00:00:00 2001 From: uccu Date: Sat, 21 May 2022 22:14:23 +0800 Subject: [PATCH 66/93] fix: fix 011111 test case error --- tests/test_parse_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index f9f63ad..c3305d5 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -781,7 +781,7 @@ from compose_viz.volume import Volume, VolumeType ), ], depends_on=["monitoring"], - extends=Extends(service_name="monitoring"), + extends=Extends(service_name="frontend"), ports=["8000:5010"], ), ] From a58e2c3af070262b9ff2d161e7137c1ed585cf21 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 22:32:59 +0800 Subject: [PATCH 67/93] chore: apply pre-commit hooks --- compose_viz/parser.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 04a40be..2f74c93 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Dict, List, Optional from ruamel.yaml import YAML @@ -6,6 +6,7 @@ from compose_viz.compose import Compose, Service from compose_viz.extends import Extends from compose_viz.volume import Volume, VolumeType + class Parser: def __init__(self): pass @@ -25,7 +26,6 @@ class Parser: raise RuntimeError("No services found, aborting.") # parse services data into Service objects - services_data = yaml_data["services"] services = self.parse_service_data(yaml_data["services"]) # create Compose object @@ -33,16 +33,15 @@ class Parser: return compose - def parse_service_data(self, services_yaml_data: List): - services = [] + def parse_service_data(self, services_yaml_data: Dict[str, dict]) -> List[Service]: + services: List[Service] = [] for service, service_name in zip(services_yaml_data.values(), services_yaml_data.keys()): - + service_image: Optional[str] = None if service.get("image"): service_image = service["image"] elif service.get("build"): service_image = "build from " + service["build"] - service_networks: List[str] = [] if service.get("networks"): @@ -50,12 +49,10 @@ class Parser: service_networks = service["networks"] else: service_networks = list(service["networks"].keys()) - service_extends: Optional[Extends] = None if service.get("extends"): service_extends = Extends(service_name=service["extends"]["service"]) - service_ports: List[str] = [] if service.get("ports"): @@ -74,7 +71,7 @@ class Parser: volume_type = VolumeType[volume_data["type"]] service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) elif type(volume_data) is str: - spilt_data = volume_data.split(":",1) + spilt_data = volume_data.split(":", 1) volume_source = spilt_data[0] volume_target = spilt_data[1] service_volumes.append(Volume(source=volume_source, target=volume_target)) @@ -92,18 +89,17 @@ class Parser: ports=service_ports, depends_on=service_depends_on, volumes=service_volumes, - links=service_links + links=service_links, ) ) # Service print debug - #print("--------------------") - #print("Service name: {}".format(service_name)) - #print("image: {}".format(service_image)) - #print("networks: {}".format(service_networks)) - #print("image: {}".format(service_image)) - #print("extends: {}".format(service_extends)) - #print("ports: {}".format(service_ports)) - #print("depends: {}".format(service_depends_on)) - + # print("--------------------") + # print("Service name: {}".format(service_name)) + # print("image: {}".format(service_image)) + # print("networks: {}".format(service_networks)) + # print("image: {}".format(service_image)) + # print("extends: {}".format(service_extends)) + # print("ports: {}".format(service_ports)) + # print("depends: {}".format(service_depends_on)) return services From 5d090fca4508a1ee36ffe0b14d6b5e0ab35c65a4 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 23:00:20 +0800 Subject: [PATCH 68/93] test: add test_port.py --- tests/test_port.py | 23 +++++++++++++++++++++++ tests/test_volume.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/test_port.py diff --git a/tests/test_port.py b/tests/test_port.py new file mode 100644 index 0000000..c7baf39 --- /dev/null +++ b/tests/test_port.py @@ -0,0 +1,23 @@ +from compose_viz.port import Port, Protocol + + +def test_port_init_normal() -> None: + try: + p = Port(host_port="8080", container_port="80") + + assert p.host_port == "8080" + assert p.container_port == "80" + assert p.protocol == Protocol.tcp + except Exception as e: + assert False, e + + +def test_port_with_protocol() -> None: + try: + p = Port(host_port="8080", container_port="80", protocol=Protocol.udp) + + assert p.host_port == "8080" + assert p.container_port == "80" + assert p.protocol == Protocol.udp + except Exception as e: + assert False, e diff --git a/tests/test_volume.py b/tests/test_volume.py index a58b56e..8081ae3 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -1,7 +1,7 @@ from compose_viz.volume import Volume, VolumeType -def test_extend_init_normal() -> None: +def test_volume_init_normal() -> None: try: v = Volume(source="./foo", target="./bar") @@ -12,7 +12,7 @@ def test_extend_init_normal() -> None: assert False, e -def test_extend_with_type() -> None: +def test_volume_with_type() -> None: try: v = Volume(source="./foo", target="./bar", type=VolumeType.bind) From 2b9ddaa86570934d43c3587993275daacc32403b Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sat, 21 May 2022 23:19:01 +0800 Subject: [PATCH 69/93] feat: implement Port --- compose_viz/graph.py | 4 +- compose_viz/parser.py | 9 +- compose_viz/port.py | 25 ++++++ compose_viz/service.py | 3 +- tests/test_parse_file.py | 177 +++++++++++++++++++++++++++++---------- 5 files changed, 171 insertions(+), 47 deletions(-) create mode 100644 compose_viz/port.py diff --git a/compose_viz/graph.py b/compose_viz/graph.py index ec75c34..1e71be4 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -64,8 +64,8 @@ class Graph: self.add_vertex(volume.source, "volume") self.add_edge(service.name, volume.source, "links") for port in service.ports: - self.add_vertex(port, "port") - self.add_edge(service.name, port, "ports") + self.add_vertex(port.host_port, "port") + self.add_edge(service.name, port.host_port, "ports") for depends_on in service.depends_on: self.dot.edge(depends_on, service.name, "depends_on") diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 2f74c93..3d94b31 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -4,6 +4,7 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends +from compose_viz.port import Port from compose_viz.volume import Volume, VolumeType @@ -86,7 +87,13 @@ class Parser: image=service_image, networks=service_networks, extends=service_extends, - ports=service_ports, + ports=[ + Port( + # TODO: not implemented yet + host_port=service_ports[0], + container_port=service_ports[0], + ) + ], depends_on=service_depends_on, volumes=service_volumes, links=service_links, diff --git a/compose_viz/port.py b/compose_viz/port.py new file mode 100644 index 0000000..c2b1646 --- /dev/null +++ b/compose_viz/port.py @@ -0,0 +1,25 @@ +from enum import Enum + + +class Protocol(str, Enum): + tcp = "tcp" + udp = "udp" + + +class Port: + def __init__(self, host_port: str, container_port: str, protocol: Protocol = Protocol.tcp): + self._host_port = host_port + self._container_port = container_port + self._protocol = protocol + + @property + def host_port(self): + return self._host_port + + @property + def container_port(self): + return self._container_port + + @property + def protocol(self): + return self._protocol diff --git a/compose_viz/service.py b/compose_viz/service.py index 4e47d92..c238576 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -1,6 +1,7 @@ from typing import List, Optional from compose_viz.extends import Extends +from compose_viz.port import Port from compose_viz.volume import Volume @@ -9,7 +10,7 @@ class Service: self, name: str, image: Optional[str] = None, - ports: List[str] = [], + ports: List[Port] = [], networks: List[str] = [], volumes: List[Volume] = [], depends_on: List[str] = [], diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index c3305d5..7dfe071 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -3,6 +3,7 @@ import pytest from compose_viz.compose import Compose from compose_viz.extends import Extends from compose_viz.parser import Parser +from compose_viz.port import Port from compose_viz.service import Service from compose_viz.volume import Volume, VolumeType @@ -80,7 +81,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="web", image="build from .", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), Service( name="redis", @@ -96,19 +99,25 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], networks=["front-tier", "back-tier"], ), Service( name="monitoring", image="awesome/monitoring", - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], networks=["admin"], ), Service( name="backend", image="awesome/backend", - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], networks=["back-tier", "admin"], ), ] @@ -121,7 +130,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), Service( name="monitoring", @@ -130,7 +141,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="backend", extends=Extends(service_name="frontend"), - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], ), ] ), @@ -142,7 +155,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], networks=["front-tier", "back-tier"], ), Service( @@ -152,7 +167,9 @@ from compose_viz.volume import Volume, VolumeType ), Service( name="backend", - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], networks=["back-tier", "admin"], extends=Extends(service_name="frontend"), ), @@ -252,18 +269,24 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), Service( name="monitoring", image="awesome/monitoring", depends_on=["backend"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), Service( name="backend", image="awesome/backend", - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], ), ] ), @@ -282,7 +305,9 @@ from compose_viz.volume import Volume, VolumeType image="awesome/monitoring", networks=["admin"], depends_on=["backend"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), Service( name="backend", @@ -299,18 +324,24 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), Service( name="monitoring", depends_on=["backend"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), Service( name="backend", extends=Extends(service_name="frontend"), - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], ), ] ), @@ -329,7 +360,9 @@ from compose_viz.volume import Volume, VolumeType networks=["admin"], depends_on=["backend"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), Service( name="backend", @@ -450,7 +483,9 @@ from compose_viz.volume import Volume, VolumeType type=VolumeType.bind, ), ], - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), ] ), @@ -481,7 +516,9 @@ from compose_viz.volume import Volume, VolumeType type=VolumeType.bind, ), ], - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), ] ), @@ -509,7 +546,9 @@ from compose_viz.volume import Volume, VolumeType ), ], extends=Extends(service_name="monitoring"), - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), ] ), @@ -540,7 +579,9 @@ from compose_viz.volume import Volume, VolumeType ), ], extends=Extends(service_name="monitoring"), - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), ] ), @@ -670,12 +711,16 @@ from compose_viz.volume import Volume, VolumeType Service( name="frontend", image="awesome/webapp", - ports=["8000:5000"], + ports=[ + Port(host_port="8000", container_port="5000"), + ], ), Service( name="monitoring", image="awesome/monitoring", - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], volumes=[ Volume(source="db-data", target="/data"), Volume( @@ -689,7 +734,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="backend", image="awesome/backend", - ports=["8000:5001"], + ports=[ + Port(host_port="8000", container_port="5001"), + ], ), ] ), @@ -721,7 +768,9 @@ from compose_viz.volume import Volume, VolumeType ), ], depends_on=["monitoring"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), ] ), @@ -750,7 +799,9 @@ from compose_viz.volume import Volume, VolumeType ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), ] ), @@ -782,7 +833,9 @@ from compose_viz.volume import Volume, VolumeType ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], ), ] ), @@ -896,7 +949,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="backend", image="awesome/backend", - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -924,7 +979,9 @@ from compose_viz.volume import Volume, VolumeType name="backend", image="awesome/backend", networks=["back-tier", "admin"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -949,7 +1006,9 @@ from compose_viz.volume import Volume, VolumeType Service( name="backend", extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -977,7 +1036,9 @@ from compose_viz.volume import Volume, VolumeType name="backend", networks=["back-tier", "admin"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1109,7 +1170,9 @@ from compose_viz.volume import Volume, VolumeType name="backend", image="awesome/backend", depends_on=["monitoring"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1138,7 +1201,9 @@ from compose_viz.volume import Volume, VolumeType image="awesome/backend", networks=["back-tier", "admin"], depends_on=["monitoring"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1164,7 +1229,9 @@ from compose_viz.volume import Volume, VolumeType name="backend", depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1193,7 +1260,9 @@ from compose_viz.volume import Volume, VolumeType networks=["back-tier", "admin"], depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1360,7 +1429,9 @@ from compose_viz.volume import Volume, VolumeType type=VolumeType.bind, ), ], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1396,7 +1467,9 @@ from compose_viz.volume import Volume, VolumeType type=VolumeType.bind, ), ], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1429,7 +1502,9 @@ from compose_viz.volume import Volume, VolumeType ), ], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1465,7 +1540,9 @@ from compose_viz.volume import Volume, VolumeType ), ], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1637,7 +1714,9 @@ from compose_viz.volume import Volume, VolumeType ), ], depends_on=["monitoring"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1674,7 +1753,9 @@ from compose_viz.volume import Volume, VolumeType ), ], depends_on=["monitoring"], - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1708,7 +1789,9 @@ from compose_viz.volume import Volume, VolumeType ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1745,7 +1828,9 @@ from compose_viz.volume import Volume, VolumeType ], depends_on=["monitoring"], extends=Extends(service_name="frontend"), - ports=["8000:5010"], + ports=[ + Port(host_port="8000", container_port="5010"), + ], links=["db:database"], ), Service( @@ -1766,7 +1851,13 @@ def test_parse_file(test_input: str, expected: Compose) -> None: for actual_service, expected_service in zip(actual.services, expected.services): assert actual_service.name == expected_service.name assert actual_service.image == expected_service.image - assert actual_service.ports == expected_service.ports + + assert len(actual_service.ports) == len(expected_service.ports) + for actual_port, expected_port in zip(actual_service.ports, expected_service.ports): + assert actual_port.host_port == expected_port.host_port + assert actual_port.container_port == expected_port.container_port + assert actual_port.protocol == expected_port.protocol + assert actual_service.networks == expected_service.networks assert len(actual_service.volumes) == len(expected_service.volumes) From 1c96d6f9f21c35c195bccbd13b49308007ecae9b Mon Sep 17 00:00:00 2001 From: uccu Date: Sun, 22 May 2022 01:50:07 +0800 Subject: [PATCH 70/93] chore: implement parser port parse --- compose_viz/parser.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 3d94b31..649e437 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -57,7 +57,13 @@ class Parser: service_ports: List[str] = [] if service.get("ports"): - service_ports = service["ports"] + if type(service["ports"]) is list: + for port_data in service["ports"]: + if not port_data.contains(":"): + raise RuntimeError("Invalid ports input, aborting.") + spilt_data = port_data.split(":", 1) + service_ports.append(Port(host_port=spilt_data[0], + container_port=spilt_data[1])) service_depends_on: List[str] = [] if service.get("depends_on"): @@ -87,13 +93,7 @@ class Parser: image=service_image, networks=service_networks, extends=service_extends, - ports=[ - Port( - # TODO: not implemented yet - host_port=service_ports[0], - container_port=service_ports[0], - ) - ], + ports=service_ports, depends_on=service_depends_on, volumes=service_volumes, links=service_links, From 473033d3f5e7c034aa7659be12abfd6779e346e2 Mon Sep 17 00:00:00 2001 From: uccu Date: Sun, 22 May 2022 01:54:41 +0800 Subject: [PATCH 71/93] :fix fix parser port parse error --- compose_viz/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 649e437..0b327e6 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -59,7 +59,7 @@ class Parser: if service.get("ports"): if type(service["ports"]) is list: for port_data in service["ports"]: - if not port_data.contains(":"): + if ':' not in port_data: raise RuntimeError("Invalid ports input, aborting.") spilt_data = port_data.split(":", 1) service_ports.append(Port(host_port=spilt_data[0], From cd36de0a5f70ab515b9c943d2720ec6bfd6ca101 Mon Sep 17 00:00:00 2001 From: uccu Date: Sun, 22 May 2022 01:57:21 +0800 Subject: [PATCH 72/93] chore: parser add some value checking --- compose_viz/parser.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 0b327e6..345600b 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -48,14 +48,15 @@ class Parser: if service.get("networks"): if type(service["networks"]) is list: service_networks = service["networks"] - else: + elif type(service["networks"]) is dict: service_networks = list(service["networks"].keys()) service_extends: Optional[Extends] = None if service.get("extends"): - service_extends = Extends(service_name=service["extends"]["service"]) + if service["extends"].get("service"): + service_extends = Extends(service_name=service["extends"]["service"]) - service_ports: List[str] = [] + service_ports: List[Port] = [] if service.get("ports"): if type(service["ports"]) is list: for port_data in service["ports"]: @@ -73,15 +74,23 @@ class Parser: if service.get("volumes"): for volume_data in service["volumes"]: if type(volume_data) is dict: - volume_source = volume_data["source"] - volume_target = volume_data["target"] - volume_type = VolumeType[volume_data["type"]] - service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) + volume_source: str = None + volume_target: str = None + volume_type: VolumeType.volume = None + if volume_data.get("source"): + volume_source = volume_data["source"] + if volume_data.get("target"): + volume_target = volume_data["target"] + if volume_data.get("type"): + volume_type = VolumeType[volume_data["type"]] + service_volumes.append(Volume(source=volume_source, + target=volume_target, + type=volume_type)) elif type(volume_data) is str: + if ':' not in volume_data: + raise RuntimeError("Invalid volume input, aborting.") spilt_data = volume_data.split(":", 1) - volume_source = spilt_data[0] - volume_target = spilt_data[1] - service_volumes.append(Volume(source=volume_source, target=volume_target)) + service_volumes.append(Volume(source=spilt_data[0], target=spilt_data[1])) service_links: List[str] = [] if service.get("links"): From 31839b7aa6c7e974d7efe0a259fb484a18d784bd Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Sun, 22 May 2022 12:34:18 +0800 Subject: [PATCH 73/93] feat: re-implement parser with compose spec --- compose_viz/parser.py | 126 ++++++++++++++++++++++++++++++++------- compose_viz/volume.py | 17 +++++- tests/test_parse_file.py | 8 ++- tests/test_volume.py | 16 ++++- 4 files changed, 139 insertions(+), 28 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 345600b..608ef85 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -1,11 +1,12 @@ +import re from typing import Dict, List, Optional from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends -from compose_viz.port import Port -from compose_viz.volume import Volume, VolumeType +from compose_viz.port import Port, Protocol +from compose_viz.volume import AccessMode, Volume, VolumeType class Parser: @@ -58,13 +59,87 @@ class Parser: service_ports: List[Port] = [] if service.get("ports"): - if type(service["ports"]) is list: - for port_data in service["ports"]: - if ':' not in port_data: - raise RuntimeError("Invalid ports input, aborting.") - spilt_data = port_data.split(":", 1) - service_ports.append(Port(host_port=spilt_data[0], - container_port=spilt_data[1])) + assert type(service["ports"]) is list + for port_data in service["ports"]: + if type(port_data) is dict: + # define a nested function to limit variable scope + def long_syntax(): + assert port_data["target"] + + container_port: str = port_data["target"] + host_port: str = "" + protocol: Protocol = Protocol.tcp + + if port_data.get("host_port"): + host_port = port_data["host_port"] + else: + host_port = container_port + + if port_data.get("host_ip"): + host_ip = str(port_data["host_ip"]) + host_port = f"{host_ip}:{host_port}" + + if port_data.get("protocol"): + protocol = Protocol(port_data["protocol"]) + + assert host_port, "Error parsing port, aborting." + + service_ports.append( + Port( + host_port=host_port, + container_port=container_port, + protocol=protocol, + ) + ) + + long_syntax() + elif type(port_data) is str: + # ports that needs to parse using regex: + # - "3000" + # - "3000-3005" + # - "8000:8000" + # - "9090-9091:8080-8081" + # - "49100:22" + # - "127.0.0.1:8001:8001" + # - "127.0.0.1:5000-5010:5000-5010" + # - "6060:6060/udp" + + def short_syntax(): + regex = r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P\d+(\-\d+)?):)?((?P\d+(\-\d+)?))?(/(?P\w+))?" # noqa: E501 + match = re.match(regex, port_data) + if match: + host_ip: Optional[str] = match.group("host_ip") + host_port: Optional[str] = match.group("host_port") + container_port: Optional[str] = match.group("container_port") + protocol: Optional[str] = match.group("protocol") + + assert container_port, "Invalid port format, aborting." + + if container_port and not host_port: + host_port = container_port + + if host_ip: + host_port = f"{host_ip}{host_port}" + + assert host_port, "Error while parsing port, aborting." + + if protocol: + service_ports.append( + Port( + host_port=host_port, + container_port=container_port, + protocol=Protocol[protocol], + ) + ) + else: + service_ports.append( + Port( + host_port=host_port, + container_port=container_port, + ) + ) + + short_syntax() service_depends_on: List[str] = [] if service.get("depends_on"): @@ -74,23 +149,28 @@ class Parser: if service.get("volumes"): for volume_data in service["volumes"]: if type(volume_data) is dict: - volume_source: str = None - volume_target: str = None - volume_type: VolumeType.volume = None - if volume_data.get("source"): - volume_source = volume_data["source"] - if volume_data.get("target"): - volume_target = volume_data["target"] + assert volume_data["source"] and volume_data["target"], "Invalid volume input, aborting." + + volume_source: str = volume_data["source"] + volume_target: str = volume_data["target"] + volume_type: VolumeType = VolumeType.volume + if volume_data.get("type"): volume_type = VolumeType[volume_data["type"]] - service_volumes.append(Volume(source=volume_source, - target=volume_target, - type=volume_type)) + + service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) elif type(volume_data) is str: - if ':' not in volume_data: - raise RuntimeError("Invalid volume input, aborting.") - spilt_data = volume_data.split(":", 1) - service_volumes.append(Volume(source=spilt_data[0], target=spilt_data[1])) + assert ":" in volume_data, "Invalid volume input, aborting." + + spilt_data = volume_data.split(":") + if len(spilt_data) == 2: + service_volumes.append(Volume(source=spilt_data[0], target=spilt_data[1])) + elif len(spilt_data) == 3: + service_volumes.append( + Volume( + source=spilt_data[0], target=spilt_data[1], access_mode=AccessMode[spilt_data[2]] + ) + ) service_links: List[str] = [] if service.get("links"): diff --git a/compose_viz/volume.py b/compose_viz/volume.py index d90faa9..f86d203 100644 --- a/compose_viz/volume.py +++ b/compose_viz/volume.py @@ -5,13 +5,24 @@ class VolumeType(str, Enum): volume = "volume" bind = "bind" tmpfs = "tmpfs" + npipe = "npipe" + + +class AccessMode(str, Enum): + rw = "rw" + ro = "ro" + z = "z" + Z = "Z" class Volume: - def __init__(self, source: str, target: str, type: VolumeType = VolumeType.volume): + def __init__( + self, source: str, target: str, type: VolumeType = VolumeType.volume, access_mode: AccessMode = AccessMode.rw + ): self._source = source self._target = target self._type = type + self._access_mode = access_mode @property def source(self): @@ -24,3 +35,7 @@ class Volume: @property def type(self): return self._type + + @property + def access_mode(self): + return self._access_mode diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 7dfe071..8dc3dbf 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -5,7 +5,7 @@ from compose_viz.extends import Extends from compose_viz.parser import Parser from compose_viz.port import Port from compose_viz.service import Service -from compose_viz.volume import Volume, VolumeType +from compose_viz.volume import AccessMode, Volume, VolumeType @pytest.mark.parametrize( @@ -428,12 +428,14 @@ from compose_viz.volume import Volume, VolumeType Service( name="common", image="busybox", - volumes=[Volume(source="common-volume", target="/var/lib/backup/data:rw")], + volumes=[ + Volume(source="common-volume", target="/var/lib/backup/data", access_mode=AccessMode.rw) + ], ), Service( name="cli", extends=Extends(service_name="common"), - volumes=[Volume(source="cli-volume", target="/var/lib/backup/data:ro")], + volumes=[Volume(source="cli-volume", target="/var/lib/backup/data", access_mode=AccessMode.ro)], ), ] ), diff --git a/tests/test_volume.py b/tests/test_volume.py index 8081ae3..d1d4cac 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -1,4 +1,4 @@ -from compose_viz.volume import Volume, VolumeType +from compose_viz.volume import AccessMode, Volume, VolumeType def test_volume_init_normal() -> None: @@ -8,6 +8,7 @@ def test_volume_init_normal() -> None: assert v.source == "./foo" assert v.target == "./bar" assert v.type == VolumeType.volume + assert v.access_mode == AccessMode.rw except Exception as e: assert False, e @@ -19,5 +20,18 @@ def test_volume_with_type() -> None: assert v.source == "./foo" assert v.target == "./bar" assert v.type == VolumeType.bind + assert v.access_mode == AccessMode.rw + except Exception as e: + assert False, e + + +def test_volume_with_access_mode() -> None: + try: + v = Volume(source="./foo", target="./bar", access_mode=AccessMode.ro) + + assert v.source == "./foo" + assert v.target == "./bar" + assert v.type == VolumeType.volume + assert v.access_mode == AccessMode.ro except Exception as e: assert False, e From 914827e928848fda68ceb4b6d1d12b273255c3f2 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Mon, 23 May 2022 02:13:57 +0800 Subject: [PATCH 74/93] chore: delete unused docker-compose.yaml --- tests/in/docker-compose.yaml | 48 ------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 tests/in/docker-compose.yaml diff --git a/tests/in/docker-compose.yaml b/tests/in/docker-compose.yaml deleted file mode 100644 index 68e98a8..0000000 --- a/tests/in/docker-compose.yaml +++ /dev/null @@ -1,48 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: From 4424aae3ae1adf177e82a52db679279e63e60ee6 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Mon, 23 May 2022 02:41:00 +0800 Subject: [PATCH 75/93] test: update test files to test ports and volumes in compose spec --- tests/in/000100.yaml | 2 +- tests/in/000101.yaml | 9 ++++++--- tests/in/000110.yaml | 2 +- tests/in/010000.yaml | 6 +----- tests/test_parse_file.py | 12 ++++++------ 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/in/000100.yaml b/tests/in/000100.yaml index c68c007..058aa18 100644 --- a/tests/in/000100.yaml +++ b/tests/in/000100.yaml @@ -3,6 +3,6 @@ services: web: build: . ports: - - "8000:5000" + - "8080" redis: image: "redis:alpine" diff --git a/tests/in/000101.yaml b/tests/in/000101.yaml index 6eb5b7f..a92a5f1 100644 --- a/tests/in/000101.yaml +++ b/tests/in/000101.yaml @@ -2,7 +2,10 @@ services: frontend: image: awesome/webapp ports: - - "8000:5000" + - target: 80 + host_ip: 127.0.0.1 + published: 8080 + protocol: udp networks: - front-tier - back-tier @@ -10,14 +13,14 @@ services: monitoring: image: awesome/monitoring ports: - - "8000:5001" + - "127.0.0.1:8081:5001" networks: - admin backend: image: awesome/backend ports: - - "8000:5010" + - "8000:5010/udp" networks: back-tier: aliases: diff --git a/tests/in/000110.yaml b/tests/in/000110.yaml index d67632a..ff82c1c 100644 --- a/tests/in/000110.yaml +++ b/tests/in/000110.yaml @@ -2,7 +2,7 @@ services: frontend: image: awesome/webapp ports: - - "8000:5000" + - target: 80 monitoring: extends: diff --git a/tests/in/010000.yaml b/tests/in/010000.yaml index 2b61286..1040482 100644 --- a/tests/in/010000.yaml +++ b/tests/in/010000.yaml @@ -2,11 +2,7 @@ services: backend: image: awesome/backend volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true + - "db-data:/data" - type: bind source: /var/run/postgres/postgres.sock target: /var/run/postgres/postgres.sock diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index 8dc3dbf..e4fe9c7 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -3,7 +3,7 @@ import pytest from compose_viz.compose import Compose from compose_viz.extends import Extends from compose_viz.parser import Parser -from compose_viz.port import Port +from compose_viz.port import Port, Protocol from compose_viz.service import Service from compose_viz.volume import AccessMode, Volume, VolumeType @@ -82,7 +82,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType name="web", image="build from .", ports=[ - Port(host_port="8000", container_port="5000"), + Port(host_port="8080", container_port="8080"), ], ), Service( @@ -100,7 +100,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType name="frontend", image="awesome/webapp", ports=[ - Port(host_port="8000", container_port="5000"), + Port(host_port="127.0.0.1:8080", container_port="80", protocol=Protocol.udp), ], networks=["front-tier", "back-tier"], ), @@ -108,7 +108,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType name="monitoring", image="awesome/monitoring", ports=[ - Port(host_port="8000", container_port="5001"), + Port(host_port="127.0.0.1:8081", container_port="5001"), ], networks=["admin"], ), @@ -116,7 +116,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType name="backend", image="awesome/backend", ports=[ - Port(host_port="8000", container_port="5010"), + Port(host_port="8000", container_port="5010", protocol=Protocol.udp), ], networks=["back-tier", "admin"], ), @@ -131,7 +131,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType name="frontend", image="awesome/webapp", ports=[ - Port(host_port="8000", container_port="5000"), + Port(host_port="80", container_port="80"), ], ), Service( From 876e32bc810f25e16d29a53eccb37ea664497f97 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Mon, 23 May 2022 02:41:35 +0800 Subject: [PATCH 76/93] fix: fix parser to fit spec --- compose_viz/parser.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 608ef85..ad5e325 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -64,14 +64,15 @@ class Parser: if type(port_data) is dict: # define a nested function to limit variable scope def long_syntax(): + assert type(port_data) is dict assert port_data["target"] - container_port: str = port_data["target"] + container_port: str = str(port_data["target"]) host_port: str = "" protocol: Protocol = Protocol.tcp - if port_data.get("host_port"): - host_port = port_data["host_port"] + if port_data.get("published"): + host_port = str(port_data["published"]) else: host_port = container_port @@ -80,7 +81,7 @@ class Parser: host_port = f"{host_ip}:{host_port}" if port_data.get("protocol"): - protocol = Protocol(port_data["protocol"]) + protocol = Protocol[str(port_data["protocol"])] assert host_port, "Error parsing port, aborting." @@ -105,6 +106,7 @@ class Parser: # - "6060:6060/udp" def short_syntax(): + assert type(port_data) is str regex = r"(?P\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P\d+(\-\d+)?):)?((?P\d+(\-\d+)?))?(/(?P\w+))?" # noqa: E501 match = re.match(regex, port_data) if match: @@ -151,12 +153,12 @@ class Parser: if type(volume_data) is dict: assert volume_data["source"] and volume_data["target"], "Invalid volume input, aborting." - volume_source: str = volume_data["source"] - volume_target: str = volume_data["target"] + volume_source: str = str(volume_data["source"]) + volume_target: str = str(volume_data["target"]) volume_type: VolumeType = VolumeType.volume if volume_data.get("type"): - volume_type = VolumeType[volume_data["type"]] + volume_type = VolumeType[str(volume_data["type"])] service_volumes.append(Volume(source=volume_source, target=volume_target, type=volume_type)) elif type(volume_data) is str: From 9503c55f765c73306373b0dff541ede355b27400 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Mon, 23 May 2022 23:23:54 +0800 Subject: [PATCH 77/93] test: add depends_on long syntax --- tests/in/001000.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/in/001000.yaml b/tests/in/001000.yaml index f64c323..36d2b3e 100644 --- a/tests/in/001000.yaml +++ b/tests/in/001000.yaml @@ -2,8 +2,10 @@ services: web: build: . depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_started redis: image: redis db: From a5b0fa507808a143f13208476c56a306c5ad52dd Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Mon, 23 May 2022 23:24:20 +0800 Subject: [PATCH 78/93] feat: implement depends_on long syntax --- compose_viz/parser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index ad5e325..0f7a5a3 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -145,10 +145,15 @@ class Parser: service_depends_on: List[str] = [] if service.get("depends_on"): - service_depends_on = service["depends_on"] + if type(service["depends_on"]) is list: + for depends_on in service["depends_on"]: + service_depends_on.append(str(depends_on)) + elif type(service["depends_on"]) is dict: + service_depends_on = list(service["depends_on"].keys()) service_volumes: List[Volume] = [] if service.get("volumes"): + assert type(service["volumes"]) is list for volume_data in service["volumes"]: if type(volume_data) is dict: assert volume_data["source"] and volume_data["target"], "Invalid volume input, aborting." From 2c692f3ab3bebc3edc9d9d379e2f4d48eae3d938 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 24 May 2022 16:02:47 +0800 Subject: [PATCH 79/93] refactor: test structure --- .github/workflows/ci.yml | 66 - .pre-commit-config.yaml | 2 +- compose_viz/parser.py | 28 +- compose_viz/port.py | 3 +- compose_viz/service.py | 7 - tests/in/000010.yaml | 10 - tests/in/000011.yaml | 28 - tests/in/000100.yaml | 8 - tests/in/000101.yaml | 35 - tests/in/000110.yaml | 15 - tests/in/000111.yaml | 32 - tests/in/001000.yaml | 12 - tests/in/001001.yaml | 29 - tests/in/001010.yaml | 11 - tests/in/001011.yaml | 30 - tests/in/001100.yaml | 17 - tests/in/001101.yaml | 30 - tests/in/001110.yaml | 19 - tests/in/001111.yaml | 31 - tests/in/010000.yaml | 11 - tests/in/010001.yaml | 37 - tests/in/010011.yaml | 38 - tests/in/010100.yaml | 16 - tests/in/010101.yaml | 39 - tests/in/010110.yaml | 23 - tests/in/010111.yaml | 40 - tests/in/011000.yaml | 22 - tests/in/011001.yaml | 40 - tests/in/011010.yaml | 25 - tests/in/011011.yaml | 41 - tests/in/011100.yaml | 28 - tests/in/011101.yaml | 42 - tests/in/011110.yaml | 29 - tests/in/011111.yaml | 43 - tests/in/100001.yaml | 31 - tests/in/100010.yaml | 15 - tests/in/100011.yaml | 32 - tests/in/100100.yaml | 16 - tests/in/100101.yaml | 33 - tests/in/100110.yaml | 17 - tests/in/100111.yaml | 34 - tests/in/101000.yaml | 16 - tests/in/101001.yaml | 33 - tests/in/101010.yaml | 17 - tests/in/101011.yaml | 34 - tests/in/101100.yaml | 18 - tests/in/101101.yaml | 35 - tests/in/101110.yaml | 20 - tests/in/101111.yaml | 36 - tests/in/110000.yaml | 24 - tests/in/110001.yaml | 42 - tests/in/110010.yaml | 27 - tests/in/110011.yaml | 43 - tests/in/110100.yaml | 28 - tests/in/110101.yaml | 44 - tests/in/110110.yaml | 29 - tests/in/110111.yaml | 45 - tests/in/111000.yaml | 26 - tests/in/111001.yaml | 44 - tests/in/111010.yaml | 29 - tests/in/111011.yaml | 45 - tests/in/111100.yaml | 30 - tests/in/111101.yaml | 46 - tests/in/111110.yaml | 30 - tests/in/111111.yaml | 47 - tests/test_cli.py | 78 +- tests/test_extends.py | 6 +- tests/test_parse_file.py | 1872 ++--------------- tests/test_parser.py | 8 +- tests/test_port.py | 2 +- tests/test_service.py | 18 - tests/ymls/builds/docker-compose.yml | 12 + tests/ymls/depends_on/docker-compose.yml | 19 + tests/ymls/extends/docker-compose.yml | 14 + .../links/docker-compose.yml} | 8 +- .../networks/docker-compose.yml} | 4 +- .../{in/000000.yaml => ymls/others/empty.yml} | 0 .../invalid.yaml => ymls/others/invalid.yml} | 0 .../others/no-services.yml} | 0 tests/ymls/ports/docker-compose.yml | 18 + .../volumes/docker-compose.yml} | 10 + 81 files changed, 252 insertions(+), 3670 deletions(-) delete mode 100644 tests/in/000010.yaml delete mode 100644 tests/in/000011.yaml delete mode 100644 tests/in/000100.yaml delete mode 100644 tests/in/000101.yaml delete mode 100644 tests/in/000110.yaml delete mode 100644 tests/in/000111.yaml delete mode 100644 tests/in/001000.yaml delete mode 100644 tests/in/001001.yaml delete mode 100644 tests/in/001010.yaml delete mode 100644 tests/in/001011.yaml delete mode 100644 tests/in/001100.yaml delete mode 100644 tests/in/001101.yaml delete mode 100644 tests/in/001110.yaml delete mode 100644 tests/in/001111.yaml delete mode 100644 tests/in/010000.yaml delete mode 100644 tests/in/010001.yaml delete mode 100644 tests/in/010011.yaml delete mode 100644 tests/in/010100.yaml delete mode 100644 tests/in/010101.yaml delete mode 100644 tests/in/010110.yaml delete mode 100644 tests/in/010111.yaml delete mode 100644 tests/in/011000.yaml delete mode 100644 tests/in/011001.yaml delete mode 100644 tests/in/011010.yaml delete mode 100644 tests/in/011011.yaml delete mode 100644 tests/in/011100.yaml delete mode 100644 tests/in/011101.yaml delete mode 100644 tests/in/011110.yaml delete mode 100644 tests/in/011111.yaml delete mode 100644 tests/in/100001.yaml delete mode 100644 tests/in/100010.yaml delete mode 100644 tests/in/100011.yaml delete mode 100644 tests/in/100100.yaml delete mode 100644 tests/in/100101.yaml delete mode 100644 tests/in/100110.yaml delete mode 100644 tests/in/100111.yaml delete mode 100644 tests/in/101000.yaml delete mode 100644 tests/in/101001.yaml delete mode 100644 tests/in/101010.yaml delete mode 100644 tests/in/101011.yaml delete mode 100644 tests/in/101100.yaml delete mode 100644 tests/in/101101.yaml delete mode 100644 tests/in/101110.yaml delete mode 100644 tests/in/101111.yaml delete mode 100644 tests/in/110000.yaml delete mode 100644 tests/in/110001.yaml delete mode 100644 tests/in/110010.yaml delete mode 100644 tests/in/110011.yaml delete mode 100644 tests/in/110100.yaml delete mode 100644 tests/in/110101.yaml delete mode 100644 tests/in/110110.yaml delete mode 100644 tests/in/110111.yaml delete mode 100644 tests/in/111000.yaml delete mode 100644 tests/in/111001.yaml delete mode 100644 tests/in/111010.yaml delete mode 100644 tests/in/111011.yaml delete mode 100644 tests/in/111100.yaml delete mode 100644 tests/in/111101.yaml delete mode 100644 tests/in/111110.yaml delete mode 100644 tests/in/111111.yaml delete mode 100644 tests/test_service.py create mode 100644 tests/ymls/builds/docker-compose.yml create mode 100644 tests/ymls/depends_on/docker-compose.yml create mode 100644 tests/ymls/extends/docker-compose.yml rename tests/{in/100000.yaml => ymls/links/docker-compose.yml} (53%) rename tests/{in/000001.yaml => ymls/networks/docker-compose.yml} (88%) rename tests/{in/000000.yaml => ymls/others/empty.yml} (100%) rename tests/{in/invalid.yaml => ymls/others/invalid.yml} (100%) rename tests/{in/no-services.yaml => ymls/others/no-services.yml} (100%) create mode 100644 tests/ymls/ports/docker-compose.yml rename tests/{in/010010.yaml => ymls/volumes/docker-compose.yml} (53%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 912e441..9d6b488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,72 +25,6 @@ jobs: with: python-version: '3.10.4' - - name: Validate Test Files - run: | - docker-compose -f tests/in/000001.yaml config -q - docker-compose -f tests/in/000010.yaml config -q - docker-compose -f tests/in/000011.yaml config -q - docker-compose -f tests/in/000100.yaml config -q - docker-compose -f tests/in/000101.yaml config -q - docker-compose -f tests/in/000110.yaml config -q - docker-compose -f tests/in/000111.yaml config -q - docker-compose -f tests/in/001000.yaml config -q - docker-compose -f tests/in/001001.yaml config -q - docker-compose -f tests/in/001010.yaml config -q - docker-compose -f tests/in/001011.yaml config -q - docker-compose -f tests/in/001100.yaml config -q - docker-compose -f tests/in/001101.yaml config -q - docker-compose -f tests/in/001110.yaml config -q - docker-compose -f tests/in/001111.yaml config -q - docker-compose -f tests/in/010000.yaml config -q - docker-compose -f tests/in/010001.yaml config -q - docker-compose -f tests/in/010010.yaml config -q - docker-compose -f tests/in/010011.yaml config -q - docker-compose -f tests/in/010100.yaml config -q - docker-compose -f tests/in/010101.yaml config -q - docker-compose -f tests/in/010110.yaml config -q - docker-compose -f tests/in/010111.yaml config -q - docker-compose -f tests/in/011000.yaml config -q - docker-compose -f tests/in/011001.yaml config -q - docker-compose -f tests/in/011010.yaml config -q - docker-compose -f tests/in/011011.yaml config -q - docker-compose -f tests/in/011100.yaml config -q - docker-compose -f tests/in/011101.yaml config -q - docker-compose -f tests/in/011110.yaml config -q - docker-compose -f tests/in/011111.yaml config -q - docker-compose -f tests/in/100000.yaml config -q - docker-compose -f tests/in/100001.yaml config -q - docker-compose -f tests/in/100010.yaml config -q - docker-compose -f tests/in/100011.yaml config -q - docker-compose -f tests/in/100100.yaml config -q - docker-compose -f tests/in/100101.yaml config -q - docker-compose -f tests/in/100110.yaml config -q - docker-compose -f tests/in/100111.yaml config -q - docker-compose -f tests/in/101000.yaml config -q - docker-compose -f tests/in/101001.yaml config -q - docker-compose -f tests/in/101010.yaml config -q - docker-compose -f tests/in/101011.yaml config -q - docker-compose -f tests/in/101100.yaml config -q - docker-compose -f tests/in/101101.yaml config -q - docker-compose -f tests/in/101110.yaml config -q - docker-compose -f tests/in/101111.yaml config -q - docker-compose -f tests/in/110000.yaml config -q - docker-compose -f tests/in/110001.yaml config -q - docker-compose -f tests/in/110010.yaml config -q - docker-compose -f tests/in/110011.yaml config -q - docker-compose -f tests/in/110100.yaml config -q - docker-compose -f tests/in/110101.yaml config -q - docker-compose -f tests/in/110110.yaml config -q - docker-compose -f tests/in/110111.yaml config -q - docker-compose -f tests/in/111000.yaml config -q - docker-compose -f tests/in/111001.yaml config -q - docker-compose -f tests/in/111010.yaml config -q - docker-compose -f tests/in/111011.yaml config -q - docker-compose -f tests/in/111100.yaml config -q - docker-compose -f tests/in/111101.yaml config -q - docker-compose -f tests/in/111110.yaml config -q - docker-compose -f tests/in/111111.yaml config -q - - name: Setup Poetry uses: Gr1N/setup-poetry@v7 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29b731d..4999367 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: | (?x)^( README.md| LICENSE| - tests/in/invalid.yaml + tests/ymls/ ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 0f7a5a3..37e8b37 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -40,10 +40,14 @@ class Parser: for service, service_name in zip(services_yaml_data.values(), services_yaml_data.keys()): service_image: Optional[str] = None - if service.get("image"): + if service.get("build"): + if type(service["build"]) is str: + service_image = "build from " + service["build"] + elif type(service["build"]) is dict: + assert service["build"].get("context"), "Missing build context, aborting." + service_image = "build from " + str(service["build"]["context"]) + elif service.get("image"): service_image = service["image"] - elif service.get("build"): - service_image = "build from " + service["build"] service_networks: List[str] = [] if service.get("networks"): @@ -54,8 +58,16 @@ class Parser: service_extends: Optional[Extends] = None if service.get("extends"): - if service["extends"].get("service"): - service_extends = Extends(service_name=service["extends"]["service"]) + assert type(service["extends"]) is dict, "Invalid extends format, aborting." + assert service["extends"]["service"], "Missing extends service, aborting." + extend_service_name = str(service["extends"]["service"]) + + extend_from_file: Optional[str] = None + if service["extends"].get("file"): + assert service["extends"]["file"], "Missing extends file, aborting." + extend_from_file = str(service["extends"]["file"]) + + service_extends = Extends(service_name=extend_service_name, from_file=extend_from_file) service_ports: List[Port] = [] if service.get("ports"): @@ -69,7 +81,7 @@ class Parser: container_port: str = str(port_data["target"]) host_port: str = "" - protocol: Protocol = Protocol.tcp + protocol: Protocol = Protocol.any if port_data.get("published"): host_port = str(port_data["published"]) @@ -79,6 +91,8 @@ class Parser: if port_data.get("host_ip"): host_ip = str(port_data["host_ip"]) host_port = f"{host_ip}:{host_port}" + else: + host_port = f"0.0.0.0:{host_port}" if port_data.get("protocol"): protocol = Protocol[str(port_data["protocol"])] @@ -122,6 +136,8 @@ class Parser: if host_ip: host_port = f"{host_ip}{host_port}" + else: + host_port = f"0.0.0.0:{host_port}" assert host_port, "Error while parsing port, aborting." diff --git a/compose_viz/port.py b/compose_viz/port.py index c2b1646..d64fb5c 100644 --- a/compose_viz/port.py +++ b/compose_viz/port.py @@ -4,10 +4,11 @@ from enum import Enum class Protocol(str, Enum): tcp = "tcp" udp = "udp" + any = "any" class Port: - def __init__(self, host_port: str, container_port: str, protocol: Protocol = Protocol.tcp): + def __init__(self, host_port: str, container_port: str, protocol: Protocol = Protocol.any): self._host_port = host_port self._container_port = container_port self._protocol = protocol diff --git a/compose_viz/service.py b/compose_viz/service.py index c238576..b1bf84b 100644 --- a/compose_viz/service.py +++ b/compose_viz/service.py @@ -18,13 +18,6 @@ class Service: extends: Optional[Extends] = None, ) -> None: self._name = name - - if image is None and extends is None: - raise AttributeError(f"Both image and extends are not defined in service '{name}', aborting.") - - if image is not None and extends is not None: - raise AttributeError(f"Only one of image and extends can be defined in service '{name}', aborting.") - self._image = image self._ports = ports self._networks = networks diff --git a/tests/in/000010.yaml b/tests/in/000010.yaml deleted file mode 100644 index 377b994..0000000 --- a/tests/in/000010.yaml +++ /dev/null @@ -1,10 +0,0 @@ -services: - base: - image: busybox - user: root - common: - extends: - service: base - cli: - extends: - service: common diff --git a/tests/in/000011.yaml b/tests/in/000011.yaml deleted file mode 100644 index d17efbe..0000000 --- a/tests/in/000011.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - networks: - - admin - extends: - service: frontend - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - extends: - service: frontend - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/000100.yaml b/tests/in/000100.yaml deleted file mode 100644 index 058aa18..0000000 --- a/tests/in/000100.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "3.9" -services: - web: - build: . - ports: - - "8080" - redis: - image: "redis:alpine" diff --git a/tests/in/000101.yaml b/tests/in/000101.yaml deleted file mode 100644 index a92a5f1..0000000 --- a/tests/in/000101.yaml +++ /dev/null @@ -1,35 +0,0 @@ -services: - frontend: - image: awesome/webapp - ports: - - target: 80 - host_ip: 127.0.0.1 - published: 8080 - protocol: udp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - ports: - - "127.0.0.1:8081:5001" - networks: - - admin - - backend: - image: awesome/backend - ports: - - "8000:5010/udp" - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/000110.yaml b/tests/in/000110.yaml deleted file mode 100644 index ff82c1c..0000000 --- a/tests/in/000110.yaml +++ /dev/null @@ -1,15 +0,0 @@ -services: - frontend: - image: awesome/webapp - ports: - - target: 80 - - monitoring: - extends: - service: frontend - - backend: - extends: - service: frontend - ports: - - "8000:5001" diff --git a/tests/in/000111.yaml b/tests/in/000111.yaml deleted file mode 100644 index c17fb18..0000000 --- a/tests/in/000111.yaml +++ /dev/null @@ -1,32 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - ports: - - "8000:5000" - - monitoring: - networks: - - admin - extends: - service: frontend - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - extends: - service: frontend - ports: - - "8000:5001" - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/001000.yaml b/tests/in/001000.yaml deleted file mode 100644 index 36d2b3e..0000000 --- a/tests/in/001000.yaml +++ /dev/null @@ -1,12 +0,0 @@ -services: - web: - build: . - depends_on: - db: - condition: service_healthy - redis: - condition: service_started - redis: - image: redis - db: - image: postgres diff --git a/tests/in/001001.yaml b/tests/in/001001.yaml deleted file mode 100644 index 1e176a6..0000000 --- a/tests/in/001001.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - depends_on: - - monitoring - - backend - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/001010.yaml b/tests/in/001010.yaml deleted file mode 100644 index 62f96e4..0000000 --- a/tests/in/001010.yaml +++ /dev/null @@ -1,11 +0,0 @@ -services: - web: - depends_on: - - db - - redis - extends: - service: redis - redis: - image: redis - db: - image: postgres diff --git a/tests/in/001011.yaml b/tests/in/001011.yaml deleted file mode 100644 index f91c6b5..0000000 --- a/tests/in/001011.yaml +++ /dev/null @@ -1,30 +0,0 @@ -services: - frontend: - networks: - - front-tier - - back-tier - depends_on: - - monitoring - - backend - extends: - service: backend - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/001100.yaml b/tests/in/001100.yaml deleted file mode 100644 index ef3d14c..0000000 --- a/tests/in/001100.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - frontend: - image: awesome/webapp - ports: - - "8000:5000" - - monitoring: - image: awesome/monitoring - depends_on: - - backend - ports: - - "8000:5010" - - backend: - image: awesome/backend - ports: - - "8000:5001" diff --git a/tests/in/001101.yaml b/tests/in/001101.yaml deleted file mode 100644 index 04483a2..0000000 --- a/tests/in/001101.yaml +++ /dev/null @@ -1,30 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - depends_on: - - backend - ports: - - "8000:5010" - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/001110.yaml b/tests/in/001110.yaml deleted file mode 100644 index 75981f6..0000000 --- a/tests/in/001110.yaml +++ /dev/null @@ -1,19 +0,0 @@ -services: - frontend: - image: awesome/webapp - ports: - - "8000:5000" - - monitoring: - depends_on: - - backend - extends: - service: frontend - ports: - - "8000:5010" - - backend: - extends: - service: frontend - ports: - - "8000:5001" diff --git a/tests/in/001111.yaml b/tests/in/001111.yaml deleted file mode 100644 index e328460..0000000 --- a/tests/in/001111.yaml +++ /dev/null @@ -1,31 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - networks: - - admin - depends_on: - - backend - extends: - service: frontend - ports: - - "8000:5010" - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/010000.yaml b/tests/in/010000.yaml deleted file mode 100644 index 1040482..0000000 --- a/tests/in/010000.yaml +++ /dev/null @@ -1,11 +0,0 @@ -services: - backend: - image: awesome/backend - volumes: - - "db-data:/data" - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - -volumes: - db-data: diff --git a/tests/in/010001.yaml b/tests/in/010001.yaml deleted file mode 100644 index 4361504..0000000 --- a/tests/in/010001.yaml +++ /dev/null @@ -1,37 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/010011.yaml b/tests/in/010011.yaml deleted file mode 100644 index 6337778..0000000 --- a/tests/in/010011.yaml +++ /dev/null @@ -1,38 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: monitoring - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/010100.yaml b/tests/in/010100.yaml deleted file mode 100644 index a601ccd..0000000 --- a/tests/in/010100.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - backend: - image: awesome/backend - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - ports: - - "8000:5000" -volumes: - db-data: diff --git a/tests/in/010101.yaml b/tests/in/010101.yaml deleted file mode 100644 index 098a6a9..0000000 --- a/tests/in/010101.yaml +++ /dev/null @@ -1,39 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - ports: - - "8000:5000" - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/010110.yaml b/tests/in/010110.yaml deleted file mode 100644 index 2727cf4..0000000 --- a/tests/in/010110.yaml +++ /dev/null @@ -1,23 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: monitoring - ports: - - "8000:5000" -volumes: - db-data: diff --git a/tests/in/010111.yaml b/tests/in/010111.yaml deleted file mode 100644 index 19fb865..0000000 --- a/tests/in/010111.yaml +++ /dev/null @@ -1,40 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: monitoring - ports: - - "8000:5000" - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/011000.yaml b/tests/in/011000.yaml deleted file mode 100644 index 0e31582..0000000 --- a/tests/in/011000.yaml +++ /dev/null @@ -1,22 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - depends_on: - - backend - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - - backend: - image: awesome/backend -volumes: - db-data: diff --git a/tests/in/011001.yaml b/tests/in/011001.yaml deleted file mode 100644 index 495facc..0000000 --- a/tests/in/011001.yaml +++ /dev/null @@ -1,40 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/011010.yaml b/tests/in/011010.yaml deleted file mode 100644 index 4578dc1..0000000 --- a/tests/in/011010.yaml +++ /dev/null @@ -1,25 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - -volumes: - db-data: diff --git a/tests/in/011011.yaml b/tests/in/011011.yaml deleted file mode 100644 index a263f0b..0000000 --- a/tests/in/011011.yaml +++ /dev/null @@ -1,41 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/011100.yaml b/tests/in/011100.yaml deleted file mode 100644 index 2bb322e..0000000 --- a/tests/in/011100.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - frontend: - image: awesome/webapp - ports: - - "8000:5000" - - monitoring: - image: awesome/monitoring - depends_on: - - backend - ports: - - "8000:5010" - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - - backend: - image: awesome/backend - ports: - - "8000:5001" -volumes: - db-data: diff --git a/tests/in/011101.yaml b/tests/in/011101.yaml deleted file mode 100644 index 8f366a2..0000000 --- a/tests/in/011101.yaml +++ /dev/null @@ -1,42 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - ports: - - "8000:5010" - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/011110.yaml b/tests/in/011110.yaml deleted file mode 100644 index d0553d2..0000000 --- a/tests/in/011110.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - frontend: - image: awesome/webapp - - - monitoring: - image: awesome/monitoring - - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - -volumes: - db-data: diff --git a/tests/in/011111.yaml b/tests/in/011111.yaml deleted file mode 100644 index 34389f4..0000000 --- a/tests/in/011111.yaml +++ /dev/null @@ -1,43 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/100001.yaml b/tests/in/100001.yaml deleted file mode 100644 index 2c130d1..0000000 --- a/tests/in/100001.yaml +++ /dev/null @@ -1,31 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/100010.yaml b/tests/in/100010.yaml deleted file mode 100644 index b7d9de1..0000000 --- a/tests/in/100010.yaml +++ /dev/null @@ -1,15 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - extends: - service: frontend - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/100011.yaml b/tests/in/100011.yaml deleted file mode 100644 index 7c4be01..0000000 --- a/tests/in/100011.yaml +++ /dev/null @@ -1,32 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/100100.yaml b/tests/in/100100.yaml deleted file mode 100644 index e3b8a94..0000000 --- a/tests/in/100100.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - image: awesome/backend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/100101.yaml b/tests/in/100101.yaml deleted file mode 100644 index a576314..0000000 --- a/tests/in/100101.yaml +++ /dev/null @@ -1,33 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/100110.yaml b/tests/in/100110.yaml deleted file mode 100644 index 612d73f..0000000 --- a/tests/in/100110.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/100111.yaml b/tests/in/100111.yaml deleted file mode 100644 index 4e1ca01..0000000 --- a/tests/in/100111.yaml +++ /dev/null @@ -1,34 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/101000.yaml b/tests/in/101000.yaml deleted file mode 100644 index 8f52814..0000000 --- a/tests/in/101000.yaml +++ /dev/null @@ -1,16 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - image: awesome/backend - depends_on: - - monitoring - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/101001.yaml b/tests/in/101001.yaml deleted file mode 100644 index 5fbf3d9..0000000 --- a/tests/in/101001.yaml +++ /dev/null @@ -1,33 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - depends_on: - - monitoring - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/101010.yaml b/tests/in/101010.yaml deleted file mode 100644 index 1fe5809..0000000 --- a/tests/in/101010.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - depends_on: - - monitoring - links: - - "db:database" - extends: - service: frontend - db: - image: postgres diff --git a/tests/in/101011.yaml b/tests/in/101011.yaml deleted file mode 100644 index f882bcd..0000000 --- a/tests/in/101011.yaml +++ /dev/null @@ -1,34 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - depends_on: - - monitoring - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/101100.yaml b/tests/in/101100.yaml deleted file mode 100644 index a7f17fe..0000000 --- a/tests/in/101100.yaml +++ /dev/null @@ -1,18 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - image: awesome/backend - depends_on: - - monitoring - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/101101.yaml b/tests/in/101101.yaml deleted file mode 100644 index dc00820..0000000 --- a/tests/in/101101.yaml +++ /dev/null @@ -1,35 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - depends_on: - - monitoring - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/101110.yaml b/tests/in/101110.yaml deleted file mode 100644 index e6cb223..0000000 --- a/tests/in/101110.yaml +++ /dev/null @@ -1,20 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - - backend: - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres diff --git a/tests/in/101111.yaml b/tests/in/101111.yaml deleted file mode 100644 index b17b0d4..0000000 --- a/tests/in/101111.yaml +++ /dev/null @@ -1,36 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: diff --git a/tests/in/110000.yaml b/tests/in/110000.yaml deleted file mode 100644 index f79c07a..0000000 --- a/tests/in/110000.yaml +++ /dev/null @@ -1,24 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - links: - - "db:database" - - backend: - image: awesome/backend - db: - image: postgres -volumes: - db-data: diff --git a/tests/in/110001.yaml b/tests/in/110001.yaml deleted file mode 100644 index dab8541..0000000 --- a/tests/in/110001.yaml +++ /dev/null @@ -1,42 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/110010.yaml b/tests/in/110010.yaml deleted file mode 100644 index 8dcf22e..0000000 --- a/tests/in/110010.yaml +++ /dev/null @@ -1,27 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/110011.yaml b/tests/in/110011.yaml deleted file mode 100644 index 2725ee8..0000000 --- a/tests/in/110011.yaml +++ /dev/null @@ -1,43 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/110100.yaml b/tests/in/110100.yaml deleted file mode 100644 index ac0fc6c..0000000 --- a/tests/in/110100.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - image: awesome/backend - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/110101.yaml b/tests/in/110101.yaml deleted file mode 100644 index 48b9d1d..0000000 --- a/tests/in/110101.yaml +++ /dev/null @@ -1,44 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/110110.yaml b/tests/in/110110.yaml deleted file mode 100644 index f9abebe..0000000 --- a/tests/in/110110.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/110111.yaml b/tests/in/110111.yaml deleted file mode 100644 index 76e6a4f..0000000 --- a/tests/in/110111.yaml +++ /dev/null @@ -1,45 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/111000.yaml b/tests/in/111000.yaml deleted file mode 100644 index bdcd442..0000000 --- a/tests/in/111000.yaml +++ /dev/null @@ -1,26 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - depends_on: - - backend - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - links: - - "db:database" - - backend: - image: awesome/backend - db: - image: postgres -volumes: - db-data: diff --git a/tests/in/111001.yaml b/tests/in/111001.yaml deleted file mode 100644 index 7586730..0000000 --- a/tests/in/111001.yaml +++ /dev/null @@ -1,44 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/111010.yaml b/tests/in/111010.yaml deleted file mode 100644 index 7591564..0000000 --- a/tests/in/111010.yaml +++ /dev/null @@ -1,29 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/111011.yaml b/tests/in/111011.yaml deleted file mode 100644 index 3714d62..0000000 --- a/tests/in/111011.yaml +++ /dev/null @@ -1,45 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/111100.yaml b/tests/in/111100.yaml deleted file mode 100644 index 9a52b90..0000000 --- a/tests/in/111100.yaml +++ /dev/null @@ -1,30 +0,0 @@ -services: - frontend: - image: awesome/webapp - - monitoring: - image: awesome/monitoring - - - backend: - image: awesome/backend - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/111101.yaml b/tests/in/111101.yaml deleted file mode 100644 index 4d6bead..0000000 --- a/tests/in/111101.yaml +++ /dev/null @@ -1,46 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - image: awesome/backend - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/in/111110.yaml b/tests/in/111110.yaml deleted file mode 100644 index 540c9de..0000000 --- a/tests/in/111110.yaml +++ /dev/null @@ -1,30 +0,0 @@ -services: - frontend: - image: awesome/webapp - monitoring: - image: awesome/monitoring - - - backend: - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -volumes: - db-data: diff --git a/tests/in/111111.yaml b/tests/in/111111.yaml deleted file mode 100644 index 2ed41b9..0000000 --- a/tests/in/111111.yaml +++ /dev/null @@ -1,47 +0,0 @@ -services: - frontend: - image: awesome/webapp - networks: - - front-tier - - back-tier - - monitoring: - image: awesome/monitoring - networks: - - admin - - - backend: - networks: - back-tier: - aliases: - - database - admin: - aliases: - - mysql - volumes: - - type: volume - source: db-data - target: /data - volume: - nocopy: true - - type: bind - source: /var/run/postgres/postgres.sock - target: /var/run/postgres/postgres.sock - depends_on: - - monitoring - extends: - service: frontend - ports: - - "8000:5010" - links: - - "db:database" - db: - image: postgres - -networks: - front-tier: - back-tier: - admin: -volumes: - db-data: diff --git a/tests/test_cli.py b/tests/test_cli.py index 45a640d..5b1ae39 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,76 +9,20 @@ runner = CliRunner() @pytest.mark.parametrize( - "file_number", + "test_file_path", [ - "000001", - "000010", - "000011", - "000100", - "000101", - "000110", - "000111", - "001000", - "001001", - "001010", - "001011", - "001100", - "001101", - "001110", - "001111", - "010000", - "010001", - "010010", - "010011", - "010100", - "010101", - "010110", - "010111", - "011000", - "011001", - "011010", - "011011", - "011100", - "011101", - "011110", - "011111", - "100000", - "100001", - "100010", - "100011", - "100100", - "100101", - "100110", - "100111", - "101000", - "101001", - "101010", - "101011", - "101100", - "101101", - "101110", - "101111", - "110000", - "110001", - "110010", - "110011", - "110100", - "110101", - "110110", - "110111", - "111000", - "111001", - "111010", - "111011", - "111100", - "111101", - "111110", - "111111", + "builds/docker-compose", + "depends_on/docker-compose", + "extends/docker-compose", + "links/docker-compose", + "networks/docker-compose", + "ports/docker-compose", + "volumes/docker-compose", ], ) -def test_cli(file_number: str) -> None: - input_path = f"tests/in/{file_number}.yaml" - output_path = f"{file_number}.png" +def test_cli(test_file_path: str) -> None: + input_path = f"tests/ymls/{test_file_path}.yml" + output_path = "compose-viz-test.png" result = runner.invoke(cli.app, ["-o", output_path, input_path]) assert result.exit_code == 0 diff --git a/tests/test_extends.py b/tests/test_extends.py index 0bc6a0c..ad7e4c5 100644 --- a/tests/test_extends.py +++ b/tests/test_extends.py @@ -5,10 +5,10 @@ from compose_viz.extends import Extends def test_extend_init_normal() -> None: try: - e = Extends(service_name="frontend", from_file="tests/in/000001.yaml") + e = Extends(service_name="frontend", from_file="tests/ymls/others/empty.yaml") assert e.service_name == "frontend" - assert e.from_file == "tests/in/000001.yaml" + assert e.from_file == "tests/ymls/others/empty.yaml" except Exception as e: assert False, e @@ -25,4 +25,4 @@ def test_extend_init_without_from_file() -> None: def test_extend_init_without_service_name() -> None: with pytest.raises(TypeError): - Extends(from_file="tests/in/000001.yaml") # type: ignore + Extends(from_file="tests/ymls/others/empty.yaml") # type: ignore diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index e4fe9c7..e760c9e 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -9,378 +9,190 @@ from compose_viz.volume import AccessMode, Volume, VolumeType @pytest.mark.parametrize( - "test_input, expected", + "test_file_path, expected", [ ( - "tests/in/000001.yaml", + "builds/docker-compose", Compose( - [ + services=[ Service( name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], + image="build from ./frontend", ), Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], + name="backend", + image="build from backend", + ), + ], + ), + ), + ( + "depends_on/docker-compose", + Compose( + services=[ + Service( + name="frontend", + image="awesome/frontend", + depends_on=[ + "db", + "redis", + ], ), Service( name="backend", image="awesome/backend", - networks=["back-tier", "admin"], + depends_on=[ + "db", + "redis", + ], ), - ] + Service( + name="db", + image="mysql", + ), + Service( + name="redis", + image="redis", + ), + ], ), ), ( - "tests/in/000010.yaml", + "extends/docker-compose", Compose( - [ + services=[ Service( name="base", - image="busybox", + image="alpine:latest", ), Service( - name="common", - extends=Extends(service_name="base"), + name="derive_from_base", + image="alpine:edge", + extends=Extends( + service_name="base", + ), ), Service( - name="cli", - extends=Extends(service_name="common"), + name="derive_from_file", + extends=Extends( + service_name="web", + from_file="web.yml", + ), ), - ] + ], ), ), ( - "tests/in/000011.yaml", + "links/docker-compose", Compose( - [ + services=[ Service( name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - networks=["admin"], - extends=Extends(service_name="frontend"), - ), - Service( - name="backend", - networks=["back-tier", "admin"], - extends=Extends(service_name="frontend"), - ), - ] - ), - ), - ( - "tests/in/000100.yaml", - Compose( - [ - Service( - name="web", - image="build from .", - ports=[ - Port(host_port="8080", container_port="8080"), + image="awesome/frontend", + links=[ + "db:database", ], ), - Service( - name="redis", - image="redis:alpine", - ), - ] - ), - ), - ( - "tests/in/000101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="127.0.0.1:8080", container_port="80", protocol=Protocol.udp), - ], - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - ports=[ - Port(host_port="127.0.0.1:8081", container_port="5001"), - ], - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - ports=[ - Port(host_port="8000", container_port="5010", protocol=Protocol.udp), - ], - networks=["back-tier", "admin"], - ), - ] - ), - ), - ( - "tests/in/000110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="80", container_port="80"), - ], - ), - Service( - name="monitoring", - extends=Extends(service_name="frontend"), - ), - Service( - name="backend", - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5001"), - ], - ), - ] - ), - ), - ( - "tests/in/000111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="8000", container_port="5000"), - ], - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - networks=["admin"], - extends=Extends(service_name="frontend"), - ), - Service( - name="backend", - ports=[ - Port(host_port="8000", container_port="5001"), - ], - networks=["back-tier", "admin"], - extends=Extends(service_name="frontend"), - ), - ] - ), - ), - ( - "tests/in/001000.yaml", - Compose( - [ - Service( - name="web", - image="build from .", - depends_on=["db", "redis"], - ), - Service( - name="redis", - image="redis", - ), Service( name="db", - image="postgres", + image="mysql", ), - ] + ], ), ), ( - "tests/in/001001.yaml", + "networks/docker-compose", Compose( - [ + services=[ Service( name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - depends_on=["monitoring", "backend"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - ), - ] - ), - ), - ( - "tests/in/001010.yaml", - Compose( - [ - Service( - name="web", - depends_on=["db", "redis"], - extends=Extends(service_name="redis"), - ), - Service( - name="redis", - image="redis", - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/001011.yaml", - Compose( - [ - Service( - name="frontend", - networks=["front-tier", "back-tier"], - depends_on=["monitoring", "backend"], - extends=Extends(service_name="backend"), - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - ), - ] - ), - ), - ( - "tests/in/001100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="8000", container_port="5000"), + image="awesome/frontend", + networks=[ + "front-tier", + "back-tier", ], ), Service( name="monitoring", image="awesome/monitoring", - depends_on=["backend"], - ports=[ - Port(host_port="8000", container_port="5010"), + networks=[ + "admin", ], ), Service( name="backend", image="awesome/backend", - ports=[ - Port(host_port="8000", container_port="5001"), + networks=[ + "back-tier", + "admin", ], ), - ] + ], ), ), ( - "tests/in/001101.yaml", + "ports/docker-compose", Compose( - [ + services=[ Service( name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - depends_on=["backend"], + image="awesome/frontend", ports=[ - Port(host_port="8000", container_port="5010"), + Port( + host_port="0.0.0.0:3000", + container_port="3000", + ), + Port( + host_port="0.0.0.0:3000-3005", + container_port="3000-3005", + ), + Port( + host_port="0.0.0.0:9090-9091", + container_port="8080-8081", + ), + Port( + host_port="0.0.0.0:49100", + container_port="22", + ), + Port( + host_port="127.0.0.1:8001", + container_port="8001", + ), + Port( + host_port="127.0.0.1:5000-5010", + container_port="5000-5010", + ), + Port( + host_port="0.0.0.0:6060", + container_port="6060", + protocol=Protocol.udp, + ), + Port( + host_port="127.0.0.1:8080", + container_port="80", + protocol=Protocol.tcp, + ), + Port( + host_port="0.0.0.0:443", + container_port="443", + ), ], ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - ), - ] + ], ), ), ( - "tests/in/001110.yaml", + "volumes/docker-compose", Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - Service( - name="monitoring", - depends_on=["backend"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - ), - Service( - name="backend", - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5001"), - ], - ), - ] - ), - ), - ( - "tests/in/001111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - networks=["admin"], - depends_on=["backend"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - ), - ] - ), - ), - ( - "tests/in/010000.yaml", - Compose( - [ + services=[ Service( name="backend", image="awesome/backend", volumes=[ - Volume(source="db-data", target="/data"), + Volume( + source="./data", + target="/data", + ), Volume( source="/var/run/postgres/postgres.sock", target="/var/run/postgres/postgres.sock", @@ -388,1465 +200,37 @@ from compose_viz.volume import AccessMode, Volume, VolumeType ), ], ), - ] - ), - ), - ( - "tests/in/010001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - ), - ] - ), - ), - ( - "tests/in/010010.yaml", - Compose( - [ Service( name="common", image="busybox", volumes=[ - Volume(source="common-volume", target="/var/lib/backup/data", access_mode=AccessMode.rw) + Volume( + source="common-volume", + target="/var/lib/backup/data", + ), ], ), Service( name="cli", - extends=Extends(service_name="common"), - volumes=[Volume(source="cli-volume", target="/var/lib/backup/data", access_mode=AccessMode.ro)], - ), - ] - ), - ), - ( - "tests/in/010011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], + extends=Extends( + service_name="common", + ), volumes=[ - Volume(source="db-data", target="/data"), Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="monitoring"), - ), - ] - ), - ), - ( - "tests/in/010100.yaml", - Compose( - [ - Service( - name="backend", - image="awesome/backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - ] - ), - ), - ( - "tests/in/010101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - ] - ), - ), - ( - "tests/in/010110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="monitoring"), - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - ] - ), - ), - ( - "tests/in/010111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="monitoring"), - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - ] - ), - ), - ( - "tests/in/011000.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - depends_on=["backend"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, + source="cli-volume", + target="/var/lib/backup/data", + access_mode=AccessMode.ro, ), ], ), - Service( - name="backend", - image="awesome/backend", - ), - ] - ), - ), - ( - "tests/in/011001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - ), - ] - ), - ), - ( - "tests/in/011010.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ), - ] - ), - ), - ( - "tests/in/011011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ), - ] - ), - ), - ( - "tests/in/011100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ports=[ - Port(host_port="8000", container_port="5000"), - ], - ), - Service( - name="monitoring", - image="awesome/monitoring", - ports=[ - Port(host_port="8000", container_port="5010"), - ], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["backend"], - ), - Service( - name="backend", - image="awesome/backend", - ports=[ - Port(host_port="8000", container_port="5001"), - ], - ), - ] - ), - ), - ( - "tests/in/011101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - ), - ] - ), - ), - ( - "tests/in/011110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - ), - ] - ), - ), - ( - "tests/in/011111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - ), - ] - ), - ), - ( - "tests/in/100000.yaml", - Compose( - [ - Service( - name="web", - image="build from .", - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100010.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - image="awesome/backend", - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/100111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101000.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - image="awesome/backend", - depends_on=["monitoring"], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - depends_on=["monitoring"], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101010.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - image="awesome/backend", - depends_on=["monitoring"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - depends_on=["monitoring"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/101111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110000.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - links=["db:database"], - ), - Service( - name="backend", - image="awesome/backend", - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110010.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - image="awesome/backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/110111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111000.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - depends_on=["backend"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - links=["db:database"], - ), - Service( - name="backend", - image="awesome/backend", - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111001.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111010.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111011.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111100.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - image="awesome/backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111101.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - image="awesome/backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111110.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - ), - Service( - name="monitoring", - image="awesome/monitoring", - ), - Service( - name="backend", - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] - ), - ), - ( - "tests/in/111111.yaml", - Compose( - [ - Service( - name="frontend", - image="awesome/webapp", - networks=["front-tier", "back-tier"], - ), - Service( - name="monitoring", - image="awesome/monitoring", - networks=["admin"], - ), - Service( - name="backend", - networks=["back-tier", "admin"], - volumes=[ - Volume(source="db-data", target="/data"), - Volume( - source="/var/run/postgres/postgres.sock", - target="/var/run/postgres/postgres.sock", - type=VolumeType.bind, - ), - ], - depends_on=["monitoring"], - extends=Extends(service_name="frontend"), - ports=[ - Port(host_port="8000", container_port="5010"), - ], - links=["db:database"], - ), - Service( - name="db", - image="postgres", - ), - ] + ], ), ), ], ) -def test_parse_file(test_input: str, expected: Compose) -> None: +def test_parse_file(test_file_path: str, expected: Compose) -> None: parser = Parser() - actual = parser.parse(test_input) + actual = parser.parse(f"tests/ymls/{test_file_path}.yml") assert len(actual.services) == len(expected.services) diff --git a/tests/test_parser.py b/tests/test_parser.py index e541a70..0e3d33f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -4,15 +4,15 @@ from compose_viz.parser import Parser def test_parser_error_parsing_file() -> None: - with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/in/invalid.yaml'.*"): - Parser().parse("tests/in/invalid.yaml") + with pytest.raises(RuntimeError, match=r"Error parsing file 'tests/ymls/others/invalid.yml'.*"): + Parser().parse("tests/ymls/others/invalid.yml") def test_parser_invalid_yaml() -> None: with pytest.raises(RuntimeError, match=r"Empty yaml file, aborting."): - Parser().parse("tests/in/000000.yaml") + Parser().parse("tests/ymls/others/empty.yml") def test_parser_no_services_found() -> None: with pytest.raises(RuntimeError, match=r"No services found, aborting."): - Parser().parse("tests/in/no-services.yaml") + Parser().parse("tests/ymls/others/no-services.yml") diff --git a/tests/test_port.py b/tests/test_port.py index c7baf39..159c92c 100644 --- a/tests/test_port.py +++ b/tests/test_port.py @@ -7,7 +7,7 @@ def test_port_init_normal() -> None: assert p.host_port == "8080" assert p.container_port == "80" - assert p.protocol == Protocol.tcp + assert p.protocol == Protocol.any except Exception as e: assert False, e diff --git a/tests/test_service.py b/tests/test_service.py deleted file mode 100644 index 1f56b33..0000000 --- a/tests/test_service.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from compose_viz.extends import Extends -from compose_viz.service import Service - - -def test_service_init() -> None: - with pytest.raises( - AttributeError, match=r"Both image and extends are not defined in service 'frontend', aborting." - ): - Service(name="frontend") - - with pytest.raises( - AttributeError, match=r"Only one of image and extends can be defined in service 'frontend', aborting." - ): - Service( - name="frontend", image="image", extends=Extends(service_name="frontend", from_file="tests/in/000001.yaml") - ) diff --git a/tests/ymls/builds/docker-compose.yml b/tests/ymls/builds/docker-compose.yml new file mode 100644 index 0000000..e55248f --- /dev/null +++ b/tests/ymls/builds/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.9" + +services: + frontend: + image: awesome/frontend + build: ./frontend + + backend: + image: awesome/backend + build: + context: backend + dockerfile: ../backend.Dockerfile \ No newline at end of file diff --git a/tests/ymls/depends_on/docker-compose.yml b/tests/ymls/depends_on/docker-compose.yml new file mode 100644 index 0000000..e949719 --- /dev/null +++ b/tests/ymls/depends_on/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.9" + +services: + frontend: + image: awesome/frontend + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + backend: + image: awesome/backend + depends_on: + - db + - redis + db: + image: mysql + redis: + image: redis diff --git a/tests/ymls/extends/docker-compose.yml b/tests/ymls/extends/docker-compose.yml new file mode 100644 index 0000000..149b396 --- /dev/null +++ b/tests/ymls/extends/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.9" + +services: + base: + image: alpine:latest + tty: true + derive_from_base: + image: alpine:edge + extends: + service: base + derive_from_file: + extends: + file: web.yml + service: web \ No newline at end of file diff --git a/tests/in/100000.yaml b/tests/ymls/links/docker-compose.yml similarity index 53% rename from tests/in/100000.yaml rename to tests/ymls/links/docker-compose.yml index 7ea23ec..56fcc07 100644 --- a/tests/in/100000.yaml +++ b/tests/ymls/links/docker-compose.yml @@ -1,9 +1,9 @@ version: "3.9" -services: - web: - build: . +services: + frontend: + image: awesome/frontend links: - "db:database" db: - image: postgres + image: mysql \ No newline at end of file diff --git a/tests/in/000001.yaml b/tests/ymls/networks/docker-compose.yml similarity index 88% rename from tests/in/000001.yaml rename to tests/ymls/networks/docker-compose.yml index b6e30ec..9210e39 100644 --- a/tests/in/000001.yaml +++ b/tests/ymls/networks/docker-compose.yml @@ -1,6 +1,8 @@ +version: "3.9" + services: frontend: - image: awesome/webapp + image: awesome/frontend networks: - front-tier - back-tier diff --git a/tests/in/000000.yaml b/tests/ymls/others/empty.yml similarity index 100% rename from tests/in/000000.yaml rename to tests/ymls/others/empty.yml diff --git a/tests/in/invalid.yaml b/tests/ymls/others/invalid.yml similarity index 100% rename from tests/in/invalid.yaml rename to tests/ymls/others/invalid.yml diff --git a/tests/in/no-services.yaml b/tests/ymls/others/no-services.yml similarity index 100% rename from tests/in/no-services.yaml rename to tests/ymls/others/no-services.yml diff --git a/tests/ymls/ports/docker-compose.yml b/tests/ymls/ports/docker-compose.yml new file mode 100644 index 0000000..642a975 --- /dev/null +++ b/tests/ymls/ports/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" + +services: + frontend: + image: awesome/frontend + ports: + - "3000" + - "3000-3005" + - "9090-9091:8080-8081" + - "49100:22" + - "127.0.0.1:8001:8001" + - "127.0.0.1:5000-5010:5000-5010" + - "6060:6060/udp" + - target: 80 + host_ip: 127.0.0.1 + published: 8080 + protocol: tcp + - target: 443 diff --git a/tests/in/010010.yaml b/tests/ymls/volumes/docker-compose.yml similarity index 53% rename from tests/in/010010.yaml rename to tests/ymls/volumes/docker-compose.yml index 9294993..dd004c1 100644 --- a/tests/in/010010.yaml +++ b/tests/ymls/volumes/docker-compose.yml @@ -1,4 +1,13 @@ +version: "3.9" + services: + backend: + image: awesome/backend + volumes: + - "./data:/data" + - type: bind + source: /var/run/postgres/postgres.sock + target: /var/run/postgres/postgres.sock common: image: busybox volumes: @@ -8,6 +17,7 @@ services: service: common volumes: - cli-volume:/var/lib/backup/data:ro + volumes: common-volume: cli-volume: From 81324f7f14dff5231140ab47447004d9cb303f6c Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 24 May 2022 17:06:31 +0800 Subject: [PATCH 80/93] chore: update volume access_mode to fit compose-spec --- compose_viz/parser.py | 6 ++---- compose_viz/volume.py | 11 +---------- tests/test_parse_file.py | 4 ++-- tests/test_volume.py | 10 +++++----- tests/ymls/volumes/docker-compose.yml | 2 +- 5 files changed, 11 insertions(+), 22 deletions(-) diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 37e8b37..2440bce 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -6,7 +6,7 @@ from ruamel.yaml import YAML from compose_viz.compose import Compose, Service from compose_viz.extends import Extends from compose_viz.port import Port, Protocol -from compose_viz.volume import AccessMode, Volume, VolumeType +from compose_viz.volume import Volume, VolumeType class Parser: @@ -190,9 +190,7 @@ class Parser: service_volumes.append(Volume(source=spilt_data[0], target=spilt_data[1])) elif len(spilt_data) == 3: service_volumes.append( - Volume( - source=spilt_data[0], target=spilt_data[1], access_mode=AccessMode[spilt_data[2]] - ) + Volume(source=spilt_data[0], target=spilt_data[1], access_mode=spilt_data[2]) ) service_links: List[str] = [] diff --git a/compose_viz/volume.py b/compose_viz/volume.py index f86d203..192ed97 100644 --- a/compose_viz/volume.py +++ b/compose_viz/volume.py @@ -8,17 +8,8 @@ class VolumeType(str, Enum): npipe = "npipe" -class AccessMode(str, Enum): - rw = "rw" - ro = "ro" - z = "z" - Z = "Z" - - class Volume: - def __init__( - self, source: str, target: str, type: VolumeType = VolumeType.volume, access_mode: AccessMode = AccessMode.rw - ): + def __init__(self, source: str, target: str, type: VolumeType = VolumeType.volume, access_mode: str = "rw"): self._source = source self._target = target self._type = type diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index e760c9e..d624a43 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -5,7 +5,7 @@ from compose_viz.extends import Extends from compose_viz.parser import Parser from compose_viz.port import Port, Protocol from compose_viz.service import Service -from compose_viz.volume import AccessMode, Volume, VolumeType +from compose_viz.volume import Volume, VolumeType @pytest.mark.parametrize( @@ -219,7 +219,7 @@ from compose_viz.volume import AccessMode, Volume, VolumeType Volume( source="cli-volume", target="/var/lib/backup/data", - access_mode=AccessMode.ro, + access_mode="ro,z", ), ], ), diff --git a/tests/test_volume.py b/tests/test_volume.py index d1d4cac..b3651ec 100644 --- a/tests/test_volume.py +++ b/tests/test_volume.py @@ -1,4 +1,4 @@ -from compose_viz.volume import AccessMode, Volume, VolumeType +from compose_viz.volume import Volume, VolumeType def test_volume_init_normal() -> None: @@ -8,7 +8,7 @@ def test_volume_init_normal() -> None: assert v.source == "./foo" assert v.target == "./bar" assert v.type == VolumeType.volume - assert v.access_mode == AccessMode.rw + assert v.access_mode == "rw" except Exception as e: assert False, e @@ -20,18 +20,18 @@ def test_volume_with_type() -> None: assert v.source == "./foo" assert v.target == "./bar" assert v.type == VolumeType.bind - assert v.access_mode == AccessMode.rw + assert v.access_mode == "rw" except Exception as e: assert False, e def test_volume_with_access_mode() -> None: try: - v = Volume(source="./foo", target="./bar", access_mode=AccessMode.ro) + v = Volume(source="./foo", target="./bar", access_mode="ro,z") assert v.source == "./foo" assert v.target == "./bar" assert v.type == VolumeType.volume - assert v.access_mode == AccessMode.ro + assert v.access_mode == "ro,z" except Exception as e: assert False, e diff --git a/tests/ymls/volumes/docker-compose.yml b/tests/ymls/volumes/docker-compose.yml index dd004c1..4edb77c 100644 --- a/tests/ymls/volumes/docker-compose.yml +++ b/tests/ymls/volumes/docker-compose.yml @@ -11,7 +11,7 @@ services: common: image: busybox volumes: - - common-volume:/var/lib/backup/data:rw + - common-volume:/var/lib/backup/data:rw,z cli: extends: service: common From 64d4804ed8b0775763e058ffbee3fbdcdfe56819 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 24 May 2022 19:38:32 +0800 Subject: [PATCH 81/93] ci: validate test files using docker compose config --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d6b488..bcfde75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,16 @@ jobs: sudo apt-get update sudo apt-get install -y graphviz + - name: Validate Test Files + run: | + docker compose -f tests/ymls/builds/docker-compose.yml config -q + docker compose -f tests/ymls/depends_on/docker-compose.yml config -q + docker compose -f tests/ymls/extends/docker-compose.yml config -q + docker compose -f tests/ymls/links/docker-compose.yml config -q + docker compose -f tests/ymls/networks/docker-compose.yml config -q + docker compose -f tests/ymls/ports/docker-compose.yml config -q + docker compose -f tests/ymls/volumes/docker-compose.yml config -q + - name: Setup Python 3.10.4 uses: actions/setup-python@v3 with: From f686cb53e9dd54698cd29ca5b7491872664b4118 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Tue, 24 May 2022 19:49:39 +0800 Subject: [PATCH 82/93] chore: add missing tests/ymls/extends/web.yml --- tests/ymls/extends/web.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/ymls/extends/web.yml diff --git a/tests/ymls/extends/web.yml b/tests/ymls/extends/web.yml new file mode 100644 index 0000000..1452125 --- /dev/null +++ b/tests/ymls/extends/web.yml @@ -0,0 +1,5 @@ +version: "3.9" + +services: + web: + image: awesome/web \ No newline at end of file From 922fc14fae87a31193b9e9913c977627a233b758 Mon Sep 17 00:00:00 2001 From: Chuan Ou Yang Date: Wed, 25 May 2022 13:16:56 +0800 Subject: [PATCH 83/93] chore: update full support graph renderer --- compose_viz/graph.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/compose_viz/graph.py b/compose_viz/graph.py index 1e71be4..992716f 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -32,10 +32,16 @@ def apply_edge_style(type) -> dict: }, "volumes": { "style": "dashed", + "dir": "both", }, "depends_on": { "style": "dotted", }, + "extends":{ + "dir": "both", + "arrowhead": "inv", + "arrowtail": "dot", + } } return style[type] @@ -48,25 +54,35 @@ class Graph: self.compose = compose self.filename = filename - def add_vertex(self, name: str, type: str) -> None: - self.dot.node(name, **apply_vertex_style(type)) + def validate_name(self, name: str) -> str: + # graphviz does not allow ':' in node name + transTable = name.maketrans({":": ""}) + return name.translate(transTable) - def add_edge(self, head: str, tail: str, type: str) -> None: - self.dot.edge(head, tail, **apply_edge_style(type)) + def add_vertex(self, name: str, type: str, lable: str = None) -> None: + self.dot.node(self.validate_name(name), lable, **apply_vertex_style(type)) + + def add_edge(self, head: str, tail: str, type: str, lable: str = None) -> None: + self.dot.edge(self.validate_name(head), self.validate_name(tail), lable, **apply_edge_style(type)) def render(self, format: str, cleanup: bool = True) -> None: for service in self.compose.services: - self.add_vertex(service.name, "service") + if service.image is not None: + self.add_vertex(service.name, "service", lable=f"{service.name}\n({service.image})") + if service.extends is not None: + self.add_edge(service.extends.service_name, service.name, "extends") for network in service.networks: - self.add_vertex("net#" + network, "network") - self.add_edge(service.name, "net#" + network, "links") + self.add_vertex(network, "network", lable=f"net:{network}") + self.add_edge(service.name, network, "links") for volume in service.volumes: self.add_vertex(volume.source, "volume") - self.add_edge(service.name, volume.source, "links") + self.add_edge(service.name, volume.source, "volumes", lable=volume.target) for port in service.ports: - self.add_vertex(port.host_port, "port") - self.add_edge(service.name, port.host_port, "ports") + self.add_vertex(port.host_port, "port", lable=port.host_port) + self.add_edge(port.host_port, service.name, "ports", lable=port.container_port) + for link in service.links: + self.add_edge(link.split(":")[0], service.name, "links", link.split(":")[1]) for depends_on in service.depends_on: - self.dot.edge(depends_on, service.name, "depends_on") + self.add_edge(service.name, depends_on, "depends_on") self.dot.render(outfile=self.filename, format=format, cleanup=cleanup) From 3d0a608896deb310e110e40df5acefce1b5de386 Mon Sep 17 00:00:00 2001 From: Chuan Ou Yang Date: Wed, 25 May 2022 16:18:53 +0800 Subject: [PATCH 84/93] fix: change port edge style & create extend vertex --- compose_viz/graph.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose_viz/graph.py b/compose_viz/graph.py index 992716f..c686e3c 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -26,6 +26,7 @@ def apply_edge_style(type) -> dict: style = { "ports": { "style": "solid", + "dir": "both", }, "links": { "style": "solid", @@ -70,6 +71,7 @@ class Graph: if service.image is not None: self.add_vertex(service.name, "service", lable=f"{service.name}\n({service.image})") if service.extends is not None: + self.add_vertex(service.name, "service", lable=f"{service.name}\n") self.add_edge(service.extends.service_name, service.name, "extends") for network in service.networks: self.add_vertex(network, "network", lable=f"net:{network}") From e8ed141c09f7a88b97a6825a47d58dc7c5af3abf Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 17:16:10 +0800 Subject: [PATCH 85/93] chore: apply pre-commit hooks --- .pre-commit-config.yaml | 2 +- compose_viz/graph.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4999367..9b26d29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: | (?x)^( README.md| LICENSE| - tests/ymls/ + tests/ymls/others/ ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/compose_viz/graph.py b/compose_viz/graph.py index c686e3c..15516ca 100644 --- a/compose_viz/graph.py +++ b/compose_viz/graph.py @@ -1,3 +1,5 @@ +from typing import Optional + import graphviz from compose_viz.compose import Compose @@ -38,11 +40,11 @@ def apply_edge_style(type) -> dict: "depends_on": { "style": "dotted", }, - "extends":{ + "extends": { "dir": "both", "arrowhead": "inv", "arrowtail": "dot", - } + }, } return style[type] @@ -60,10 +62,10 @@ class Graph: transTable = name.maketrans({":": ""}) return name.translate(transTable) - def add_vertex(self, name: str, type: str, lable: str = None) -> None: + def add_vertex(self, name: str, type: str, lable: Optional[str] = None) -> None: self.dot.node(self.validate_name(name), lable, **apply_vertex_style(type)) - def add_edge(self, head: str, tail: str, type: str, lable: str = None) -> None: + def add_edge(self, head: str, tail: str, type: str, lable: Optional[str] = None) -> None: self.dot.edge(self.validate_name(head), self.validate_name(tail), lable, **apply_edge_style(type)) def render(self, format: str, cleanup: bool = True) -> None: From a71f82572c6bceb0e425567b68967e6c24279dd5 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 17:16:38 +0800 Subject: [PATCH 86/93] chore: add examples --- .github/workflows/ci.yml | 1 + .gitignore | 2 + .../full-stack-node-app/docker-compose.yml | 59 +++++++++++++++++++ tests/test_cli.py | 18 +++--- tests/ymls/builds/docker-compose.yml | 2 +- tests/ymls/extends/docker-compose.yml | 2 +- tests/ymls/extends/web.yml | 2 +- tests/ymls/links/docker-compose.yml | 2 +- 8 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 examples/full-stack-node-app/docker-compose.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcfde75..0636ebf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: docker compose -f tests/ymls/networks/docker-compose.yml config -q docker compose -f tests/ymls/ports/docker-compose.yml config -q docker compose -f tests/ymls/volumes/docker-compose.yml config -q + docker compose -f examples/full-stack-node-app/docker-compose.yml config -q - name: Setup Python 3.10.4 uses: actions/setup-python@v3 diff --git a/.gitignore b/.gitignore index 5d7a886..14acc5b 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +*.png diff --git a/examples/full-stack-node-app/docker-compose.yml b/examples/full-stack-node-app/docker-compose.yml new file mode 100644 index 0000000..c3602de --- /dev/null +++ b/examples/full-stack-node-app/docker-compose.yml @@ -0,0 +1,59 @@ +version: "3.9" + +services: + node: + build: + context: . + dockerfile: Dockerfile.node + + api: + image: "awesome/api" + extends: + service: node + build: + args: + PACKAGE_PATH: api + WORKING_DIR: /usr/src/ + expose: + - 8000 + ports: + - 8000:8000 + environment: + - NODE_ENV=development + volumes: + - ./api:/usr/src + depends_on: + - postgres + - adminer + command: ["npm", "start"] + + frontend: + extends: + service: node + build: + args: + PACKAGE_PATH: frontend + WORKING_DIR: /usr/src/ + expose: + - 3000 + ports: + - 3000:3000 + environment: + - REACT_APP_ENV=development + - BACKEND=api:8000 + - HTTPS=true + - NODE_PATH=/usr/src/ + volumes: + - ./frontend:/usr/src + depends_on: + - api + command: ["npm", "start"] + + postgres: + image: postgres + restart: always + + adminer: + image: adminer + ports: + - 8080:8080 diff --git a/tests/test_cli.py b/tests/test_cli.py index 5b1ae39..7c28d72 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,17 +11,19 @@ runner = CliRunner() @pytest.mark.parametrize( "test_file_path", [ - "builds/docker-compose", - "depends_on/docker-compose", - "extends/docker-compose", - "links/docker-compose", - "networks/docker-compose", - "ports/docker-compose", - "volumes/docker-compose", + "tests/ymls/builds/docker-compose.yml", + "tests/ymls/depends_on/docker-compose.yml", + "tests/ymls/extends/docker-compose.yml", + "tests/ymls/links/docker-compose.yml", + "tests/ymls/networks/docker-compose.yml", + "tests/ymls/others/docker-compose.yml", + "tests/ymls/ports/docker-compose.yml", + "tests/ymls/volumes/docker-compose.yml", + "examples/full-stack-node-app/docker-compose.yml", ], ) def test_cli(test_file_path: str) -> None: - input_path = f"tests/ymls/{test_file_path}.yml" + input_path = f"{test_file_path}" output_path = "compose-viz-test.png" result = runner.invoke(cli.app, ["-o", output_path, input_path]) diff --git a/tests/ymls/builds/docker-compose.yml b/tests/ymls/builds/docker-compose.yml index e55248f..322a355 100644 --- a/tests/ymls/builds/docker-compose.yml +++ b/tests/ymls/builds/docker-compose.yml @@ -9,4 +9,4 @@ services: image: awesome/backend build: context: backend - dockerfile: ../backend.Dockerfile \ No newline at end of file + dockerfile: ../backend.Dockerfile diff --git a/tests/ymls/extends/docker-compose.yml b/tests/ymls/extends/docker-compose.yml index 149b396..caf2a1e 100644 --- a/tests/ymls/extends/docker-compose.yml +++ b/tests/ymls/extends/docker-compose.yml @@ -11,4 +11,4 @@ services: derive_from_file: extends: file: web.yml - service: web \ No newline at end of file + service: web diff --git a/tests/ymls/extends/web.yml b/tests/ymls/extends/web.yml index 1452125..1676371 100644 --- a/tests/ymls/extends/web.yml +++ b/tests/ymls/extends/web.yml @@ -2,4 +2,4 @@ version: "3.9" services: web: - image: awesome/web \ No newline at end of file + image: awesome/web diff --git a/tests/ymls/links/docker-compose.yml b/tests/ymls/links/docker-compose.yml index 56fcc07..cac78e9 100644 --- a/tests/ymls/links/docker-compose.yml +++ b/tests/ymls/links/docker-compose.yml @@ -6,4 +6,4 @@ services: links: - "db:database" db: - image: mysql \ No newline at end of file + image: mysql From 469f1f61225acf9a33c46a26e47cf1cc169239bf Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 17:46:53 +0800 Subject: [PATCH 87/93] chore: add new examples --- .github/workflows/ci.yml | 1 + tests/test_cli.py | 2 +- tests/test_parse_file.py | 8 ++++++-- tests/ymls/builds/docker-compose.yml | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0636ebf..1577053 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: docker compose -f tests/ymls/ports/docker-compose.yml config -q docker compose -f tests/ymls/volumes/docker-compose.yml config -q docker compose -f examples/full-stack-node-app/docker-compose.yml config -q + docker compose -f examples/non-normative/docker-compose.yml config -q - name: Setup Python 3.10.4 uses: actions/setup-python@v3 diff --git a/tests/test_cli.py b/tests/test_cli.py index 7c28d72..bfcdbcd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -16,10 +16,10 @@ runner = CliRunner() "tests/ymls/extends/docker-compose.yml", "tests/ymls/links/docker-compose.yml", "tests/ymls/networks/docker-compose.yml", - "tests/ymls/others/docker-compose.yml", "tests/ymls/ports/docker-compose.yml", "tests/ymls/volumes/docker-compose.yml", "examples/full-stack-node-app/docker-compose.yml", + "examples/non-normative/docker-compose.yml", ], ) def test_cli(test_file_path: str) -> None: diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py index d624a43..f384789 100644 --- a/tests/test_parse_file.py +++ b/tests/test_parse_file.py @@ -17,11 +17,15 @@ from compose_viz.volume import Volume, VolumeType services=[ Service( name="frontend", - image="build from ./frontend", + image="build from './frontend', image: awesome/frontend", ), Service( name="backend", - image="build from backend", + image="build from 'backend' using '../backend.Dockerfile'", + ), + Service( + name="db", + image="build from './db'", ), ], ), diff --git a/tests/ymls/builds/docker-compose.yml b/tests/ymls/builds/docker-compose.yml index 322a355..fff4866 100644 --- a/tests/ymls/builds/docker-compose.yml +++ b/tests/ymls/builds/docker-compose.yml @@ -6,7 +6,10 @@ services: build: ./frontend backend: - image: awesome/backend build: context: backend dockerfile: ../backend.Dockerfile + + db: + build: + context: ./db From cbb1f0575d6f9ba60f95440b7ee7f012d0665c3a Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 17:47:23 +0800 Subject: [PATCH 88/93] chore: implement new image parsing rule --- compose_viz/parser.py | 17 +++++-- .../full-stack-node-app/docker-compose.yml | 31 ++++++++---- examples/full-stack-node-app/postgres.yml | 5 ++ examples/non-normative/docker-compose.yml | 47 +++++++++++++++++++ 4 files changed, 86 insertions(+), 14 deletions(-) create mode 100644 examples/full-stack-node-app/postgres.yml create mode 100644 examples/non-normative/docker-compose.yml diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 2440bce..757d3ed 100644 --- a/compose_viz/parser.py +++ b/compose_viz/parser.py @@ -42,12 +42,19 @@ class Parser: service_image: Optional[str] = None if service.get("build"): if type(service["build"]) is str: - service_image = "build from " + service["build"] + service_image = f"build from '{service['build']}'" elif type(service["build"]) is dict: - assert service["build"].get("context"), "Missing build context, aborting." - service_image = "build from " + str(service["build"]["context"]) - elif service.get("image"): - service_image = service["image"] + if service["build"].get("context") and service["build"].get("dockerfile"): + service_image = ( + f"build from '{service['build']['context']}' using '{service['build']['dockerfile']}'" + ) + elif service["build"].get("context"): + service_image = f"build from '{service['build']['context']}'" + if service.get("image"): + if service_image: + service_image += ", image: " + service["image"] + else: + service_image = service["image"] service_networks: List[str] = [] if service.get("networks"): diff --git a/examples/full-stack-node-app/docker-compose.yml b/examples/full-stack-node-app/docker-compose.yml index c3602de..5e5387f 100644 --- a/examples/full-stack-node-app/docker-compose.yml +++ b/examples/full-stack-node-app/docker-compose.yml @@ -23,8 +23,9 @@ services: volumes: - ./api:/usr/src depends_on: - - postgres + - db - adminer + - redis command: ["npm", "start"] frontend: @@ -38,22 +39,34 @@ services: - 3000 ports: - 3000:3000 - environment: - - REACT_APP_ENV=development - - BACKEND=api:8000 - - HTTPS=true - - NODE_PATH=/usr/src/ volumes: - ./frontend:/usr/src depends_on: - api command: ["npm", "start"] - postgres: - image: postgres + db: + image: "awesome/db" + extends: + service: postgres + from: postgres.yml restart: always + volumes: + - "db-data:/data" + - type: bind + source: /var/run/postgres/postgres.sock + target: /var/run/postgres/postgres.sock + + redis: + image: "awesome/redis" + restart: always + expose: + - 6379 adminer: - image: adminer + image: "awesome/adminer" ports: - 8080:8080 + + volumes: + db-data: diff --git a/examples/full-stack-node-app/postgres.yml b/examples/full-stack-node-app/postgres.yml new file mode 100644 index 0000000..cb6c45e --- /dev/null +++ b/examples/full-stack-node-app/postgres.yml @@ -0,0 +1,5 @@ +version: "3.9" + +services: + postgres: + image: awesome/postgres diff --git a/examples/non-normative/docker-compose.yml b/examples/non-normative/docker-compose.yml new file mode 100644 index 0000000..2ed41b9 --- /dev/null +++ b/examples/non-normative/docker-compose.yml @@ -0,0 +1,47 @@ +services: + frontend: + image: awesome/webapp + networks: + - front-tier + - back-tier + + monitoring: + image: awesome/monitoring + networks: + - admin + + + backend: + networks: + back-tier: + aliases: + - database + admin: + aliases: + - mysql + volumes: + - type: volume + source: db-data + target: /data + volume: + nocopy: true + - type: bind + source: /var/run/postgres/postgres.sock + target: /var/run/postgres/postgres.sock + depends_on: + - monitoring + extends: + service: frontend + ports: + - "8000:5010" + links: + - "db:database" + db: + image: postgres + +networks: + front-tier: + back-tier: + admin: +volumes: + db-data: From f34ff42be3f913aad5d2f0b381ab14076b9d03c0 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 17:55:52 +0800 Subject: [PATCH 89/93] chore: add cpv results in examples --- .gitignore | 1 + examples/full-stack-node-app/compose-viz.png | Bin 0 -> 94328 bytes .../full-stack-node-app/docker-compose.yml | 19 ++++++++++++++++-- examples/non-normative/compose-viz.png | Bin 0 -> 48849 bytes 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 examples/full-stack-node-app/compose-viz.png create mode 100644 examples/non-normative/compose-viz.png diff --git a/.gitignore b/.gitignore index 14acc5b..1fdfab5 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ #.idea/ *.png +!examples/**/*.png diff --git a/examples/full-stack-node-app/compose-viz.png b/examples/full-stack-node-app/compose-viz.png new file mode 100644 index 0000000000000000000000000000000000000000..1b0976bad91a138522d389a7c86d8055f27a2c8b GIT binary patch literal 94328 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 z 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

      b7A`K$&H*vjZU6EoSYDjmmEW9*JQb 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 z%0AjS$7LqH-nNYhY3gyH<;OA%W}Cc_;fx3ip*_GVI=i}NL(+xmSbjsNrMMxd1oav+ z)dmAdit9k;cY6PkL2LRlSy}cKK5(pHhdE_28tE`!W(6$GdhV|rM8JfM=@KaYZ=9jM z7ZEt`AgGBQ_KC)}#5y3$TRJu|BlO{M97|XqPhnKK7-#}Ae(-hcNVSB8lXULxtRgfX zQHhRd>5n)9GMyZzaF;&N(o$7$T3J?gLCpr5>;jD>d_~Tq4w`Y*#b5!*2nix`lZh)Y zp7_K;ZMTr>rs=y>>$AdWlKf#H^`E(z4}aax_dv)<&EU{3fn#`rN{Sk}5IPM{(yUJi z=eJkZ;&+zR%S@vX5_{qf24Lm26E0hx$dd*K;KFh5Ff ztkHogno=7YEnFP`Ee)qER$eL8_*A4_WJhww4b?xm1!!8yfyF0Jw2p9M6nV%!x3lo9 z444Zy*1bTOsL}~vuT{qewha(A3~VXl`T4Npq3rptyAW{2)5^;E!otEC+1YoK{8VFL z>tKeTB`!o#($786iFYyzbjWp&bWXPS!UVt_qMJO=6@`7E7fMU2PVW2_M_Bw1? zGAAPe9)xTod~c>9vd{%PsJMwZLiRKyI@B$%rnU&^`eG<1(u;pB!fE3VqX_^ld$nGz zutd?i9zx13Gg0PQ3f2XB%jbaB{7B*YADm>Cf$MSeYW3ob_r?1H!L%qWssthVFIvfa@a<_ z(&f8M&Ibz}Qg0Q7JreQ-m(Gz?T-VCDu9IJ&{*e;6;<295IS0KPmim3@^|kJAY^m|Mrtb;W~AJ ztsn-#RZ2@@bPKnlC^_8-2rL3ph=J8SSXjbPP2y9!fdDAsQBzjKtthFDr5jj|`h@1? zVqF1ALQo@1Hi#;$&`i=Xe34z1XJ(WT7GwqF30WPwK#|ocf+B-1&4I0PFcQKeCK zh;XTiv5?7rLr*+e4O8-p?7Py92ani=HipcIxM;nf;zEZ1JM=4zLZOb8kW!jL_V?PX^izqt|O^m0-XRg zAc5L|$_sU41*SkPJh$hG+cvt0HTNssX3H<|Hq5hU0VzxQDh)bK)QFfHXP9 z@rg&^-l?vkG5mD?dhC}FrE|g;XbytO^shD4QG&$|Yq?K87`50JbjYQ1PT7l}&i5kS||A zl0Az6PAERhkuS&nh_xmoQqAZE$d0jv;~pYMA(H^C&O5j~GEE2aEsV}8N<~n;|HS)T z3@gFZDWhv@_imtX7P>Ta4F*F4vbq2`>zk>9NM#of!t2p z`izEU+uM3Cnc|=qHT6FNs}x0n{r=$GYd534xCaE+99jJ7k=_+pj0<2HL=#&7=LCh> z-D@{R9W7Lk;^+&zgqjGLjpd@ni(w6Q$P?}NNCFf;L-mG<%xeL3QfW<=2+xi~632^z`HGXXGg;3sxv@TmedY7@vzedIP4|Ktw~m-y2q6);@6HiJQ94eB^J4 zK%(DpECbT{&Z)eaJk3#kz=tn%h|jNg?w~dsl4F3PaP=d_b%)nDC!kkHsNU$1M!%z{ zGLxJ`F(4zcEvu|d5zi&QEvv9l2FV{pz+jkHAbkoSD}V0?_qe~Zu_Zv57!0Ijhw@wp zMp&$;uYBWOY?M!wl4GfoCwcH%qND z#olNin}A>n?D7;9{RW~{w6u1N7pF+^GMXoWuB3fG()C&r{5;AuewiKlB3;IxPwKlA z&w2tL1I+XS{1r5#5v>jyJrK)nlD-d(*uBcc)yCA4&Yn!UMWQl2mTmUE^PDFFVn(b;pkMle_Wrl@$Qf-Yrj62CVFvuJHc<(h6<(uDAPsboPcSeIg`Und2g0zyD=>D#;Q zvwIx${>s}}G+3KD+0T3l)BW?48P3vl`he5nLD6LXWqFz!XV?$l)YGE{^iM=;mpiiR zP7f_~xaAcsoneVe3xH_-7#Xpku`z&4%wPb!4NAgr08N~Mdfhm1Ucw0UYkN`D5Fg7I z9I8-yf;~lES96+#Y|yd2#FS?kQ|mYO7NJ?h6<{21sLIcv#FJ#S)7l#4_3i0=wUQ3f zG)iW#8;tsTDcR;NwwO5vW2n79(v%&T_VY_hI$AJU&DEXQPDxgLm$XJApXKneryj!JcM?Wz@~NUL3h^pI8|Ds@scJj}~Gj(TnYxQVCorjux%q}?gtWgGtr zO`XjsN+AxsSP`PaGXUmzMtjS*N`wJ*`?0LyeHaabSLh1s0y7-k$4Q-w&fEcH>NzdO ze|Tf!2Q(JC#Xk+)a{Hymep<$Gj?Ba^$w1|J-ty(zt#0uYAh0W0i;bhcTRu1&o0L_V zl>Z>N^4j$3*vEU*b_yz%kM3++%U0F?&fnj^+P&*}aKasR&D-A(a#wx&pS$Ecyt>W( z5<3rT(T>4TNluPnKVch;#!S?Oym9feWyi5xX?OxExCb{h;>k6GML2{) zd?sM(38XfRfihu^86R+ds9pE!l#zX!OHQbtE|cw4x|5{)B`)#A{S*2wGE9lE`cF5G z1h1W#$GxxP%c)>p3~s*G8tcjCB)cJ5+89}4V6c}SQ=)C?9EkoC>+z{s96!H&KX8-p zwQfTuR!MW#hpZRNcE()4uHTa{hpm=1E7@fUP!Rc_I8vWt3RX4JzSj()I|rTveK`af zbw>?*4zPnp2=sF?XtOxJ{xB^2;pt+Exdl{l+C)S~R^f-60odKZxm?%ObX@*U2pmqM zjUJsmKXjVC6Rgt%q&6fQ9>C^zX(2b#XM>;G1~wyjG+sW(A*21L_3&U+D;gS32Kzd8 zysYw`^YXo71GaHe@|9g}dCixK?b6d8ta_!c+c2DL7?D|0>G8pRp5vUOCu1M}&TR`! z_`RW+9%HexJ!e|PY)K)za<0{FuaY4Wh#{YpaC_rNQKtr@KUUZz9W-WjXkne|Za?f| zxoJA`_wI>XvP|p&?dVDzEjr!W8m+WO9!Row{Oe}FRVw09fo6m|w-)zMvfh89 zhc2@7V{(A;oXgeT-G(d1Sswg;^>5Sm*vHzP#lskf67F?su%~D96-CvVo0M+VMqBu5Y~ zJDT~-<>AAJma?<*mFDgn@_pQFP@kdKFrnXI8s9cy#B6r#DHXS1%4nrjD_F+u)M5tm zG}y(TION#x)C^{%ddiqzDx!zOn^k;~b<%-Z=2yTOiao*hrCE8TZAB@iNF)0Ia=ek7 zT!J&MTiQ5&c%WIdaJ=2)C+su>kfeU&Owa_{jVI*;$iW=lgAjlzZbn2PB|9{gG+v6P zk)SJg4iHn|M>HQ+NJ!{|G%xqvAowUTeBQL?U`z|m+Ch>WejNzT;jhh!>KEDls03Bq zR9;!Bux<4wK&3cVQGxiu1`2w*Et7Lp^^W7h$S5i#EbKp|+R&OcqSr|X!`6t{Mf&m? z96O6V3QC+uAYhaf73bmy;Jh#wlOj=2mLk&w_T595bMD*w4Nwu%Ouen1+aXDWNImVl z3-gW$x6K|@hRa{+!$$+A4j0R5&{I5;V6xXj3qehetwC{j-vclNWZ;9Mxe{p6&mdh} zXjc`GtwRF>s6nNOzzJ}WV8#?td(+e+pm;PR6iD|C$h2sbf}46EZ@Ai>goH}uU^cK` zn-8dHKEvYp@lr@$zp}wE1BzZD$O29l@lsj~l9p%yn^y{!^7VdHz_fJZVGz-Y@i75t z+@kTjFv#>ne+8JfiJ*j2v**mANfA_B!hKB+vM{u29|c88zJLgMNja$Z;e|o~6Iw&g zDQ-N6rVlEi(X#?!iEgmeW|RhUGJ$1hqcQ4o3%IaM?k#E`Uu0(G6pq2lj=6#x& zi`tFp!G&C(mrkG>IUE;Hi39{nz7BdWv_%uLh3u1TuD)?<9T%z)!VrfWH*aza2%yAP zg9#4Vtf2v87GdxBvUv3EM#CBtL2l>Cj|*z+>S)LkAZh?~s3<3bl9)i(=r*rizb?OR zHQsgTkjmX4JTt=U=nAQ%Bjlzac;>Ou2yEz8$TOZwOL*bps33tWd<43sZ#7DTIY;f` zi;SO?5r7`5@SrncutO8Dh(oR9zDt@Z7zlj;*h!ZKI%O_{YCl*5_yGtodmh3)a#4j7 z9qVRbOR4{67^l2BR*KlSyzAta2Yv%=9Wj!)O?l__6v~}_^#saM1)vZb#0x$ge@v2q`1|z`b?Ig9d#Gt)e&`{kmT2o zhIx;*LZ%ED0|o!Hcs1gIAvP&|9ZHGiLaqwD?d}S2#ibwx$q^7kv8Z#IDAZSq<{0^6 zep&1`o))p=1d-5!1vEU&86dU+=0oey<}}lM^M4nRhd|9z3j7v&VWJYz6OVm%55BN7 zBM04vxMUw|)(ZmhK-)BSdM~*J^<_F*$9m!?6*qY z*2!PAkEblZ8x~=~psmn9x*Q*K`g4q%5M(})Y6n%3slwxK8QfW+zkTTzEkKsXkORpB zDCp2u*+g^A(Ja=yNetA0RpSFTxt|~AP(wJrNUvmhEdzFJOVQiT9ul=YZYk{rYfsFB z$pD`fsg{2oY7rzF%K#dg0g}Z6gEh$68!~4^P{kmEIM;*evIu@ljTb=Np-v4OUCjmw z%nX>QdhvB%Q~G*HqG<*Vp@`1J8OpP#V{AB(fjYfsIYO8jM^OmxoL%w!2m=Ay@90}jXxl|jUGH~!=JgWi@OJ`1hPk#r@FNw-8JWC|Ixy-!LeB3A&FVxA5zgoB8H5tO+aap+d*N zX=?$qUEskKK`-IiirFw^5i)SXbidg>jqIjI1n6BDOFmp%tUrGK++8%@UW%wJ3}^_^ z;|_>N&&m_oe}4baLIVSNz(WFcyWqxiaO%X~Sz%b(D=WnX)_nB(xS5^zSGGf~3hzSh zD7Dk*sk4F?Rmbq@AGGW+JCo2+90&Dh@eV!7HjbM8w$pGbk~biNc0zBg>%kjmM9rR< zg9)OIm{fq~uoTWxwgRT4@^F*25AX>x_W?l!r2|djvUa_#BnT50@|%I1ux6}WAoQi@SW=}AJ3(^QeCUHJ*zE|(#kC+Q5Pc`)rI3~6hX5Ra zF;u4Lk|wCXW4K<8XC;jyL{9QRY~FJKRo~h@hLS$!gSY5ydnC$$;DP#0S5y5L35dlk4Go zd_&|G+(skcN!T%9sUl)?W!>6{!_=-m_=Po1@T7wy0|XBgRI6Tx0c-OIdH$A>8o17U ziL0wCQ7dZu_op05$2bTK5`4sdb%nh1ItmLaUd(8jSSJ+O-c&$Tvt;*ZF=31l^T!Ah}aCxV(x(YnT1vu2G9rm-R}XZ3*6hu(Cj z$Jhh4AX|I;ATY23p0BV@5e)8MKDK@Q*QjmuZg1nU1+RdklJpj=i0vviaT~pR!;Tj} zg{s{nSAH@A8_Zk?5P|YaXqJRTM7o5zYlv>8`Juoi5hp^tv7$6{*dd z%|rG zY>q~kI+VkA@81^~__J*Y#RNtadiowGb4uux^{pSF7Y*tQ_^z~msN`WufgA)PkY^x1 zoM76D2h0VElA;KH;xcG=Gq{bAcW;Nbi#*C;Ae9a7>Nt&Q4hc33v;D(t2aa&4goL5b z6u-Zz`1C&f4g*NlG#Nj}S>@5%iZ?V%CFk7AEOFh?#sz&hyt8^FN^}+SA z(3D_XoXBW^;R?-K0%T7N7MdN+fT2U`^n;VR1%^=Eq5X*6#6?$f&Vk1e2w>)*C#+^xfqy%ruJn;y>yaiZAmMCq1+pRU3Bbw5 zVF9nyWjZtb97v<_muxtPE=T0?np^L$Tna%jYB1U+m3BLU-7NC(tZ$<52u+j8*h9z= zV99kE)nwhfdF=n$uG-}`xDKP#Y1$Ns^*Hh^M9M;bVdmy@07|&&#mum&7!!(;s{(4t z`jhGvYiU3?7*X~n)&+A-ub6@4L{rFB_SzM8kON>io&U*dn6~YIc*FW4R4yR#(bD!0 z-0-zqogk*7;W_5GIS7VyTAc!3lMZe{%_{J(IHrjF&&WtbLIk7Hefs*>4Ub*HoI45X zf2GxSg0H5oj>ca>&DwIiG6t#^gl)?XUUm)+GDRW*+1u{rJx-$QU{_c*@30rBE}&^< zwzgZbi)Bt{G|Cv>Urct!ou&R%7?W#XOVDG?JN29zZDPtDGe__NAdC|QoEasE(5Bp0 zwpR%Ry?Fs_Db*}6Dno*Wz}+LQA8%l2xCkv}aLoFE(Od17VQ_`&O3wIFf zBQJ)8H+VUEWwsyAh0IVxBK#YGI@6;^b-v~zW>%uHb~%cCR3b?-w!pQJM)+fJwOTre z4q|9=g4?|{7c|UXK&oK)4kI{`%#z#y_Xh7xh73FO+BF{ARq!%-ifjX<=mp$kX+_06 zG^N(_J)^R02`pWZWv;`t;1I@@V*x5bSB}=pBt0VkNkfT(WAZ;a3!~^>J^AJWbf599o7gB%V++&^`z#ap4sI+;KH_{S+LTL6|__x9}=iJcF zz;?Vl>L(a@|9B02xC(p7?*l?zVU(-vv3~XQXHFp+JopT>^o;nGV2)p-ki*ZHoBn-@ z_(;Zb4vxjhahfG?2$^^e|3k%jj_2j*YuDy(*^+QFxP#2zK>LW>MdYP@mO;{!c|T!7$Qj@*%t!ME z(2J3n9r^-08Dc+y*4^B9ZRnVVLUbbN91a`;OQ6O}Y^gBaDCIws*>W(&!@uFvu;}&D zq8_6olcN3Z>EF8B>$;tSJJ9{;D@+C$Ks=izo399yasaEBw3%Q*gTVh`&;KWU0m9=K z?E4^xoXf~^p@B6>xSRI`!}9zYB#0LM=&v(Mu|F!VS}wdV|JFuift4^*XV=gQjl?!a zYGPA3coO&)*;|&4Ij4w^{h&ClpM3 zP?COn6AQ`c>?hbqka&7lGQQ>BYKOKCTt5oGY*Mw!Uz7mTwGT)wDfq z)%e2y(r0Kc!q}HIB#k5(!gC3RoOc#%TYaC6B9#pd3!4oM6>c@r;^R2#Q(BZ%VNj1C zEO$LV9z#kRyhEORhZ9Aw>qMvz&* z->==&v7rvC;nXiK-?18>z!x1RxBfWsfHm0UOd*I8%5q36^xeCIKyjden@vju+3o@k zjwq!LUBn^T#B=!nMKcAWR2P!OhO_$*>+4^DSI(1`7Mi>X#Lq=)Y14;i;z}4svKW6+ z20#bjzX9mphwg4)5}rU#N1uBzp_^aFN$n$Ibv655XiY7u@urRl9)~m!i}oJ`M1;#O zXj*nOT`tFnP-L>U8PP1`hG7Y$0Fb!Oo>_WJ$>W}qu5Nc!1kj2A(4elBVfbt6KQ4}I=f<-{%8m?` zr0etXifCm}u5SjXH&jy0mN)p8{@?9i;!(iBch>yTygMj~X*ipQ7GMNu7#X*B6=}lS z0n@;W|NOFTzQT!bf~gY97|(l3Vc1gWS<-IQ{fI0xMC~3hw)w~-hEYVD0a%4CSBhcB ze7fq35;KKm%`7Z?e^50is@lE?RXkZ+ik0j*{l|-jn&1P%-Tlcsf~fs=iFG3oeB%&9 zxITA)PZ5Vr_Q?mNeba00_uJ|V-N)~5rOUFOTFhUv_CQ+G3%pje%*2IVK^Js67QQ2< zp{d38;mjo4R|G{eSw*=HR zg&BKo>mN85zbo>P1UC$A`$morObib|b*qFuiy4s@4THV!1g;vDIs=3qfy_8f@)e3k zG)a-I9n@DjZ5uL?>8Cd>kw$q;MyOOm2~{MY7*SJE*@ImIyI4PzoK!B8B?U71dJ*3h zGtV#WZ^CpEd>$E{<~>0E54kU8Ni={23?e!A0Z4gn; z-+YLQK<3o3T!l%8mTXld3=theO&XH#>$$DNVq?>Bq^I1ww+}FbJFpvZRG)Vju2ijh z@uCLs_rCw;G8nfbLV0HSY^!Z-ypM;ixkGpAYhDM>l(ckN!2*+f$2-`s;z$5k*6n=T z{d}?*_BqaxF7(r=C0&1}wR?>l6@9PT8g>_&1XObSZBQdzcZ<)-VNy3R->NTDEVCGl z-b$4)#-0l{_V$g?R5h8oKF4IsctXjJ`vY09?R<6~SX>x@%?D^{KFuj{;`iASrC_d9 z+<@B+m)f?r2dEhJAU_KtGJ6VZQ|i>GE8Ws6_&NNqrL{Hw#VYeFXqLG=;s>|o!oW@aw;7Y zuhH~nVVKwhF*;&b%XQl}q_`Fg$63cd(by2X?gM?v^2F6H$ro5vdPLX7cB zX|#8vJNtAF8-u~XSl)$`K9TRjQ2gq z8ehP`+WEtiSr85Eg>K5t-(*+TN)RE{~cv)3|14U0qr`ERrM+@p?aR95^ z4`%E^#-b0u>_0!dmtoR~8qm*L`dE0 z4@b{1kKd2i5Sin}D#9YF%DBDQQ!W;aH0Bkg0kG|ZZ*eZ=pxqE}wYop!*ZFtm zJj_aUx5ieJ;5uQ)rwqojBvr+|dI88k`{C#@L|2orS$BME3y|9=bK!8@zJm(!XR|d^x)>%HqPUTh-^8kYG`OMEIgfw_WcyJTo|UNrW*T)>%fVzCEqE&$!z3OarSnj8pMk=#-}kBdS9fp zTN?C$i5&L%Px~`MX8r0H=2pcxB5SVzr-J?;$(sBRac6UV{-sqn=WK}y4v&^B%gZR0 z;yeZ)DVO~DmC|Ik>ypjAk)hZ|v#$jA^t=BqrA1?@{H`{gM&;VGv& z+|g}j@&+fbRD(Ios#KToYu6ItkEDAOsD)BB568hlxbPb`UV0Nyms{o`C}r$Hs|g>D zPrf48dh5lIsI`qvE$Y}2tZtkf8ZFSP?_L}|Z_Fz{BjW%@cR5w_N*{AK)QU3~Y#;vm zY3HkBxFdIP!!GD*Tk7RsG@ViWZ|s`{#7y23S1qRgW17TeX$qH}Z48%_HI503Yt6K( ziOZi# zXLngIruy79A%>4J;TxDIwD#01@unMCYi0e8QYfmG!a^+&6fLd@!>(wpD(~7QO4Z^L z5|Mj0m{wI+2MSkUFR`iK&&_RIYaZ91vG}|^jNA83@6P42jR{jn5t)5NXzi7=pH^LF z&GUNy)bwqp;C^_fbvouN$PSkY+B$5K{JNs`K7ve4*PB%c`I1PD#U$HL$M3?tjHXVD z?W)H>mKhmB%5=k2$SN`_3SW9HBEqfZO&gb}Z0eR~;m^*Vk9TQag|*S)gwe5F@YuRX zj7)p`0eJ|I=OZ~->OSvz5y*fhe}M-cNjNUR47#zzxs2tRZccn)0S=W zM|cpz%tv zLmM~WRM!mKY3DE?Y-hBgr8?hQIpHj~aqPc`8#Sgs-dB&4`f6iq8_1)pufKZgWx?B^ zufCgIdTq_C531f8EVEAEZQPpQ7x8bodARCoEpOYa2VY!wYF(Wq7a?7)5jIzL5B9T; z8S?L$i#oSwL%g8c`z2KaZplZd%MbfUxv%u%3Q$zX44k%uZS^iVGV|T?VsbYb%OqJX zOFyV8UDDxz^5_)`-)EQ+FxicSo3F{1Dm9XxT=nc zNeBNzOQW@|J8ChcC(iBT*MgQH%~6+oB?Fr48?Q>mjBVXIajfJ?P8CRnN{uDuX_eOR z?OTNpl|*mjYAG|)+1)H+-x`aLT}dJ^xPQ?T}{==!eI zEfQ=!Vz%*H?aU)^eRogEygzh5!R}MGdfcf;hj~=rB6nbH2#3l2OYnWR@FIByq^)pi z!hYYucgHjSr3+2!e)zr7^tt<|U9P)q9X~m*=+=u6*J%GL`m)a^*cnyL_)C}UfQQys1la{DCJA;EsM%%JnQYxJP&4xgVa?%6i;rAAGYt zEkSzCd#}x^weQ~b?o=rI71c1lQ`Y8fjO!Hx6M?HdMoD#%E6lYP?0Vmj$CZFNs9!SV zJU9+b+Q(xVSm}n%f6+wZw+7JNud9tAT&jh#OP{kce?EK z(l?mj(fDWWg1K>(A){Few%!sIQ^IwJN6Vx?MF$Nbl7|MUhM_INPxn{q<7Ul{y>Su& zO*?irL?kW?n6hN;KYycfyzjnNa9*H3+tyjfi*87`UO4ZP$&L@~a6@5~aM_^0tF2}H zfm=>lZiMcCyT)R3>;hVNo6iVn%VYC_GQ3`RTTb+r>eK--GI=i{o&4EzJp7O zQ>E{ZUFNi9FOseK@aaaMwd{+aT?yT-ubd*)d9i%ma$*j)ejWKK|NnXhiW_UDo<_bf zotAq)(l?x2^Xu_c!b|LHa~uN|z3Pmu0UxqC8_SDzkMg&lWgW7NZ#)~F=&j*=OKsIM zyRLOwg*>XX63#OPnV-KOr2WV$XMSY`V}qTbabY4T(|+&i$yl^WCZhl}iZF7$*<{AO zn9V#JBPohTYWGvtg5wD2+((y#UXgD;*MUIw?J*zMvr^@*CX9iBg4SqBG7^hx(7)F`yFh0oa&i6VqBMnjm)>8Bf~ z;x+UG>^Am4zYX(R)BoZL`|K{$|$J~?@`+~mvnlBit>_CQbjLTlE zI-I>QWET!BALZ48M-m~d`0{hOJ5tE(zvAED_opk!UnkfSRDL1`Au$aMY@4$zjs53t z*pF0a*B>{jBCwE@uxZ|sY3}@S;lezoo*vq)&R7TfOUyD=#^E4?p$a?=_4QWhR)`cf zzW`xpzl+LqM=Hq%3x8+^vWu%BgN($q;U?}`&o_c%cl$l$?2dj~aqNSl;R+5UYm~3? z^Vd+vO2RF7fv>}Fc^L!0jps8MQ|}uLOj4oDwSq3m9f~rhu5LW4i{49H)4oZGS%jd_ z|I&OswgE6y4)ohaV=cDqLZ&`VX0x7)-27&I8I>trF=m3@0$z(FAbX+D(i^I?8Ej9G zKjD5`z$F4iO_d(ZS3pzGRlH#ZNO9hKjlWM$Oni%W&qVZbtmHu%!cWJc$a6qi=`AgW z^omf<{Q~?_v~(fYWN&=1bE{omQfjIO3O=T`b|~(c!5BG2F~0PO+k2Om67Lc&j{#v6 zd$yooP!o?zZM%EO!{f2>*$GS8Z%^h#%6jA$|Lj^E$R*p-pMkIZCDu;YYW3mCIm5@U zsemt6Et!Wmm4GbO2Kppp{|jO?OLY`i(T@b83Hag}>-DG`BZaQwV&5SsyP%SrLQ+A6 zClK_)r3?A>@f1<&QY}?b5IJ`tex98U%O#R^h2#aB?g$_|~P*crJp}_ARz%D#RYWLDGYyK@u~4!bmTbm(0$~ONon< z2ddWq93H9VXBQRx!zp&vm5k@|2#&vCskds;d1uUHOE2BKY_9L#qF-N9P}2`ftzs~I z&IaR*vq%EEhhB-=ej)r@A30zDTX?1TRi?fY|1Ag>F`6_v>7~yFP#WytFAJ)->+vkc z$O3!~e&967%h8|R!5*CR)}ioLKq~b^Y6)Y{>K|<8SDN!Z1PQYUv1 z&NKRffr0#ocl!(oql9N!FW1&D6cUqrmFMqQhvfdiz0dp_?w9!cn=m}dnO`a!v0Uia zh5%eX{qo_#E1LQB|Nju<|D#9nVh8_#%ksZZolE+=_d`2V%?|GGu$f>mRQG8rrpcTB G_x}JNIm3?t literal 0 HcmV?d00001 diff --git a/examples/full-stack-node-app/docker-compose.yml b/examples/full-stack-node-app/docker-compose.yml index 5e5387f..6dee7c9 100644 --- a/examples/full-stack-node-app/docker-compose.yml +++ b/examples/full-stack-node-app/docker-compose.yml @@ -26,6 +26,9 @@ services: - db - adminer - redis + networks: + - front-tier + - back-tier command: ["npm", "start"] frontend: @@ -43,6 +46,8 @@ services: - ./frontend:/usr/src depends_on: - api + networks: + - front-tier command: ["npm", "start"] db: @@ -51,6 +56,8 @@ services: service: postgres from: postgres.yml restart: always + networks: + - back-tier volumes: - "db-data:/data" - type: bind @@ -60,13 +67,21 @@ services: redis: image: "awesome/redis" restart: always + networks: + - back-tier expose: - 6379 adminer: image: "awesome/adminer" + networks: + - back-tier ports: - 8080:8080 - volumes: - db-data: +volumes: + db-data: + +networks: + front-tier: + back-tier: diff --git a/examples/non-normative/compose-viz.png b/examples/non-normative/compose-viz.png new file mode 100644 index 0000000000000000000000000000000000000000..805094a265529b7975634a42e80c301e55c0b705 GIT binary patch literal 48849 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%0AcL*IEeFobqR>R^KspTyNbGsLT(l-5*_Hz|qc z94%YA6x%RD>4uob-m`H%pdVe5XS4H>AF&5K8S4X23450lX<3Qu*GtX-%Mk~QTSFnX zO>hTe3PomUa8RC&dPQMaF^MpK($%+X&lI17zQ7^o$Xk%Fr;gzuC+;>3{f0Qo6UnTW zt&i7^wunM08jYDBA54CF(~rUO_YT@SIW29R;oI@5#NeKvbJ>U^g4Q3I+C0QXCaa7( zZ~Q7me<6N8oqS|WmCTN6-Nt;4n_F{$L#nH;Fuo<*;as?DYCq6A>_0Uxb=4?^oNNb> zwffYD``1l%fcfF!5iv@f5tJ=zU%L3+*hZ-k`cGAqorOir)b9^2vh`lDU{y+2;UTl8 z-Rle!(CO{KuYFt?C@Ka?<~s{H^RP9tl~{2`i#WTv!No5DmnVOAwsfr0P)kJ*I>b$- zskwO+r4vbH9nwG|8Ix!v+$yO3YzkBG0PXgc>w_f9bM}7Py;;MroA)|0$VMDBxH4)N zxck}U$`aZ8LWIC(byoorrpQY%6Uu@4R9Q`u<=s1pzmJUcgt%#Y`-f%?bb?ET$4ni- z=D4F>OW5GdvX>8nK6}PtCEvNXkv^+b8hhy3TH~~R*ryx@D9)vZQBhHW149+`k^hC( zQqy~{vR#rq*P@)y^J7zn*D7M$8f&j!9sMOMmHa0f#D!ztB*ATp-b%O?2~7>s7V`2h z_4-N_{6GG{MfvYRf7FL?oMw|JAKpqX$~!`>{j#98~XM z+qTwRpllbFeTqqSDAn|q_eI91$SKJ>$bnX#Rh=R9I6Q9NM^#b2G!l^k+rerYGN%g* z*J1!8jI}}iv{Wdxddp-91fzA+A0_;F;!*XJF>4=R&~F@BsE$bZh7~gl&MW5XIC*wY zUzpj3D#JdF;TU^Y;`*~e0XX(xzO$X$$qqw}9Ew6~_QhVqM6F%;aAMhw10s=+vXt^d zYzPjBQde{R#I(S4Y|h`@)>Md}OAZ-{xT(cn!p(Cd|Ja?b8Bx|ggG>qc7-S6<)A%yT znTC_E$%AIfhhVCW4wP4~uu_a>hZfOfpt|!zd>LWfVF!T8p}d#S8_jYvQ&aMjW4pt< zVGy7FROWqzHf%{-);~kG_+#^#c=Q}%BD~5c%1}Y*uU&CMaJvm5=^Hpb)C$BD<3Ee7 zYluxS2)W0P+uGQ8fx~W26lyLP2TT-%_jA2>l|m+BCLIHtc{X?B+jYS@qEu^A6zFDr zf-A9`c*RfWx#hWZkdk0HiMa>Fh&TS=d~gR)G}MxyEaBOHEAstao-|D;8HWv&y3U?` z8)_kLI4d)A%Y)L>I*+4Iu~n2nwzixSXGFtGa`H46A||OmH7<#OIuB-zX?2vrDw)8o z^N`M*GpFf|Q}bbF0(&WtDGY?9+tb)!%h~6an8<7}ER>-E&_x7S~ z;_8`c^{_g1jKm(wFV6|Ap{w|vXS6Ly3p_#Sg+AwCU;Y^Nx=vD%=EPfJJ9oauo)(4V zN^Nbo5Uz`FcjWDL)UBn^d)wRFS%a+o`z@!}-+~BFF=aa}MIKw1?J}bA%eBUF{*()> zDGf4%N~!EXNiHLE{rts#ejRH4IgS_4N7HC5NHZgKMsaK29xlWX$QmmsSdfP83q2L5 zu1XtdpL7?}WS)6{ zc5*s{Rddk>B}q42B9J4PCM5uHM{Oc4kor%-c{ad(u(bh2iuc|QR?Y}D5)MozH5CMf zY3OnQ(!vqW8!t}d|MypnBT&kB* Date: Wed, 25 May 2022 18:03:27 +0800 Subject: [PATCH 90/93] fix: wrong yaml keyword in example --- examples/full-stack-node-app/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/full-stack-node-app/docker-compose.yml b/examples/full-stack-node-app/docker-compose.yml index 6dee7c9..b21887f 100644 --- a/examples/full-stack-node-app/docker-compose.yml +++ b/examples/full-stack-node-app/docker-compose.yml @@ -54,7 +54,7 @@ services: image: "awesome/db" extends: service: postgres - from: postgres.yml + file: postgres.yml restart: always networks: - back-tier From 782782ee264c90d863954a48380d330d4522e197 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 21:27:10 +0800 Subject: [PATCH 91/93] chore: update README.md --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4bd58a..e09d13d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@

    • Getting Started
        +
      • Prerequisities
      • Installation
      • Usage
      • Options
      • @@ -55,7 +56,9 @@ ## About The Project -`compose-viz` is a [docker-compose](https://github.com/docker/compose)/[podman-compose](https://github.com/containers/podman-compose) graph visualization tool that allows you to gernerate graph in [DOT](https://graphviz.org/doc/info/lang.html) format or `.png`. +`compose-viz` is a compose file visualization tool that supports [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) and allows you to gernerate graph in [DOT](https://graphviz.org/doc/info/lang.html) format or `.png`. + +If you are looking for a compose file vizualization tool, and you are using one of the [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) implementations (e.g. [docker-compose](https://github.com/docker/compose)/[podman-compose](https://github.com/containers/podman-compose)), then `compose-viz` is a great choice for you.

        (back to top)

        @@ -63,6 +66,12 @@ ## Getting Started +### Prerequisities + +#### Graphviz + +If you want to generate PNG (which is the default option), you need to install [Graphviz](https://graphviz.org/download/). + ### Installation #### Using `pip` @@ -88,7 +97,9 @@ See [releases](https://github.com/compose-viz/compose-viz/releases). ### Example -`cpv -o docker-compose-viz.png docker-compose.yml` +`cpv -o .\examples\full-stack-node-app\compose-viz.png .\examples\full-stack-node-app\docker-compose.yml` + +[Here](https://github.com/compose-viz/compose-viz/blob/main/examples/full-stack-node-app/compose-viz.png) is the result.

        (back to top)

        @@ -96,7 +107,7 @@ See [releases](https://github.com/compose-viz/compose-viz/releases). ## Roadmap -- [ ] Support [podman-compose](https://github.com/containers/podman-compose). +- [ ] Support more vizualization components. See the [open issues](https://github.com/compose-viz/compose-viz/issues) for a full list of proposed features (and known issues). From f34ff17ac07342a11ffdbbc451da80ade261d427 Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 21:29:01 +0800 Subject: [PATCH 92/93] chore: update pyproject description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 888fddc..5a686d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "compose-viz" version = "0.1.0" -description = "A docker-compose/podman-compose graph visualization tool that allows you to gernerate graph in DOT format or PNG." +description = "A compose file visualization tool that supports compose-spec and allows you to gernerate graph in DOT format or PNG." authors = ["Xyphuz Wu "] readme = "README.md" license = "MIT" From 4d9685841b79025d4eabd02359a4e597a96c8ecd Mon Sep 17 00:00:00 2001 From: Xyphuz Date: Wed, 25 May 2022 21:51:56 +0800 Subject: [PATCH 93/93] chore: add new example --- examples/voting-app/compose-viz.png | Bin 0 -> 70144 bytes examples/voting-app/docker-compose.yml | 89 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 examples/voting-app/compose-viz.png create mode 100644 examples/voting-app/docker-compose.yml diff --git a/examples/voting-app/compose-viz.png b/examples/voting-app/compose-viz.png new file mode 100644 index 0000000000000000000000000000000000000000..64486c2db5c8f0760f67db2c39ed77ad117accab GIT binary patch literal 70144 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
        xz+)6e2VLX=J$^F_yqVdb5q@E}%iBDgw4tMN> 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 literal 0 HcmV?d00001 diff --git a/examples/voting-app/docker-compose.yml b/examples/voting-app/docker-compose.yml new file mode 100644 index 0000000..be21988 --- /dev/null +++ b/examples/voting-app/docker-compose.yml @@ -0,0 +1,89 @@ +# https://github.com/docker/labs/blob/master/beginner/chapters/votingapp.md + +version: "3.9" + +services: + redis: + image: redis:alpine + ports: + - "6379" + networks: + - frontend + deploy: + replicas: 2 + update_config: + parallelism: 2 + delay: 10s + restart_policy: + condition: on-failure + db: + image: postgres:9.4 + volumes: + - db-data:/var/lib/postgresql/data + networks: + - backend + deploy: + placement: + constraints: [node.role == manager] + vote: + image: dockersamples/examplevotingapp_vote:before + ports: + - 5000:80 + networks: + - frontend + depends_on: + - redis + deploy: + replicas: 2 + update_config: + parallelism: 2 + restart_policy: + condition: on-failure + result: + image: dockersamples/examplevotingapp_result:before + ports: + - 5001:80 + networks: + - backend + depends_on: + - db + deploy: + replicas: 1 + update_config: + parallelism: 2 + delay: 10s + restart_policy: + condition: on-failure + worker: + image: dockersamples/examplevotingapp_worker + networks: + - frontend + - backend + deploy: + mode: replicated + replicas: 1 + labels: [APP=VOTING] + restart_policy: + condition: on-failure + delay: 10s + max_attempts: 3 + window: 120s + placement: + constraints: [node.role == manager] + visualizer: + image: dockersamples/visualizer + ports: + - "8080:8080" + stop_grace_period: 1m30s + volumes: + - /var/run/docker.sock:/var/run/docker.sock + deploy: + placement: + constraints: [node.role == manager] + +networks: + frontend: + backend: + +volumes: + db-data: