diff --git a/compose_viz/parser.py b/compose_viz/parser.py index 3d94b31..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: @@ -48,16 +49,97 @@ 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"): - service_ports = service["ports"] + 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"): @@ -67,15 +149,28 @@ 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"]] + 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)) 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)) + 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"): @@ -87,13 +182,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, 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/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: 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