diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1577053..9d6b488 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,18 +20,6 @@ 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
- 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
with:
diff --git a/compose_viz/graph.py b/compose_viz/graph.py
index 3f634f2..0d9a8a9 100644
--- a/compose_viz/graph.py
+++ b/compose_viz/graph.py
@@ -5,13 +5,13 @@ import graphviz
from compose_viz.models.compose import Compose
-def apply_vertex_style(type) -> dict:
+def apply_vertex_style(type: str) -> dict:
style = {
"service": {
"shape": "component",
},
"volume": {
- "shape": "folder",
+ "shape": "cylinder",
},
"network": {
"shape": "pentagon",
@@ -19,24 +19,39 @@ def apply_vertex_style(type) -> dict:
"port": {
"shape": "circle",
},
+ "env_file": {
+ "shape": "tab",
+ },
+ "porfile": {
+ "shape": "invhouse",
+ },
+ "cgroup": {
+ "shape": "diamond",
+ },
+ "device": {
+ "shape": "box3d",
+ },
}
return style[type]
-def apply_edge_style(type) -> dict:
+def apply_edge_style(type: str) -> dict:
style = {
- "ports": {
+ "exposes": {
"style": "solid",
"dir": "both",
},
"links": {
"style": "solid",
},
- "volumes": {
+ "volumes_rw": {
"style": "dashed",
"dir": "both",
},
+ "volumes_ro": {
+ "style": "dashed",
+ },
"depends_on": {
"style": "dotted",
},
@@ -45,6 +60,9 @@ def apply_edge_style(type) -> dict:
"arrowhead": "inv",
"arrowtail": "dot",
},
+ "env_file": {
+ "style": "solid",
+ },
}
return style[type]
@@ -71,19 +89,38 @@ class Graph:
def render(self, format: str, cleanup: bool = True) -> None:
for service in self.compose.services:
if service.image is not None:
- self.add_vertex(service.name, "service", lable=f"{service.name}\n({service.image})")
+ self.add_vertex(
+ service.name,
+ "service",
+ lable=f"{service.container_name if service.container_name else 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")
+ if service.cgroup_parent is not None:
+ self.add_vertex(service.cgroup_parent, "cgroup")
+ self.add_edge(service.name, service.cgroup_parent, "links")
+
for network in service.networks:
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, "volumes", lable=volume.target)
+ self.add_edge(
+ service.name,
+ volume.source,
+ "volumes_rw" if "rw" in volume.access_mode else "volumes_ro",
+ lable=volume.target,
+ )
+ for expose in service.expose:
+ self.add_vertex(expose, "port")
+ self.add_edge(expose, service.name, "exposes")
for port in service.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)
+ self.add_edge(port.host_port, service.name, "links", lable=port.container_port)
+ for env_file in service.env_file:
+ self.add_vertex(env_file, "env_file")
+ self.add_edge(env_file, service.name, "env_file")
for link in service.links:
if ":" in link:
service_name, alias = link.split(":", 1)
@@ -92,5 +129,13 @@ class Graph:
self.add_edge(link, service.name, "links")
for depends_on in service.depends_on:
self.add_edge(service.name, depends_on, "depends_on")
+ for porfile in service.profiles:
+ self.add_vertex(porfile, "porfile")
+ self.add_edge(service.name, porfile, "links")
+ for device in service.devices:
+ self.add_vertex(device.host_path, "device")
+ self.add_edge(
+ device.host_path, service.name, "exposes", f"{device.container_path}\n({device.cgroup_permissions})"
+ )
self.dot.render(outfile=f"{self.filename}.{format}", format=format, cleanup=cleanup)
diff --git a/compose_viz/models/device.py b/compose_viz/models/device.py
new file mode 100644
index 0000000..80d6b1e
--- /dev/null
+++ b/compose_viz/models/device.py
@@ -0,0 +1,20 @@
+from typing import Optional
+
+
+class Device:
+ def __init__(self, host_path: str, container_path: str, cgroup_permissions: Optional[str] = None):
+ self._host_path = host_path
+ self._container_path = container_path
+ self._cgroup_permissions = cgroup_permissions
+
+ @property
+ def host_path(self):
+ return self._host_path
+
+ @property
+ def container_path(self):
+ return self._container_path
+
+ @property
+ def cgroup_permissions(self):
+ return self._cgroup_permissions
diff --git a/compose_viz/models/service.py b/compose_viz/models/service.py
index 89740ba..8e578c2 100644
--- a/compose_viz/models/service.py
+++ b/compose_viz/models/service.py
@@ -1,5 +1,6 @@
from typing import List, Optional
+from compose_viz.models.device import Device
from compose_viz.models.extends import Extends
from compose_viz.models.port import Port
from compose_viz.models.volume import Volume
@@ -16,6 +17,12 @@ class Service:
depends_on: List[str] = [],
links: List[str] = [],
extends: Optional[Extends] = None,
+ cgroup_parent: Optional[str] = None,
+ container_name: Optional[str] = None,
+ devices: List[Device] = [],
+ env_file: List[str] = [],
+ expose: List[str] = [],
+ profiles: List[str] = [],
) -> None:
self._name = name
self._image = image
@@ -25,6 +32,12 @@ class Service:
self._depends_on = depends_on
self._links = links
self._extends = extends
+ self._cgroup_parent = cgroup_parent
+ self._container_name = container_name
+ self._devices = devices
+ self._env_file = env_file
+ self._expose = expose
+ self._profiles = profiles
@property
def name(self):
@@ -57,3 +70,27 @@ class Service:
@property
def extends(self):
return self._extends
+
+ @property
+ def cgroup_parent(self):
+ return self._cgroup_parent
+
+ @property
+ def container_name(self):
+ return self._container_name
+
+ @property
+ def devices(self):
+ return self._devices
+
+ @property
+ def env_file(self):
+ return self._env_file
+
+ @property
+ def expose(self):
+ return self._expose
+
+ @property
+ def profiles(self):
+ return self._profiles
diff --git a/compose_viz/parser.py b/compose_viz/parser.py
index 63139d4..c67186c 100644
--- a/compose_viz/parser.py
+++ b/compose_viz/parser.py
@@ -5,6 +5,7 @@ from pydantic import ValidationError
import compose_viz.spec.compose_spec as spec
from compose_viz.models.compose import Compose, Service
+from compose_viz.models.device import Device
from compose_viz.models.extends import Extends
from compose_viz.models.port import Port, Protocol
from compose_viz.models.volume import Volume, VolumeType
@@ -176,6 +177,50 @@ class Parser:
if service_data.links is not None:
service_links = service_data.links
+ cgroup_parent: Optional[str] = None
+ if service_data.cgroup_parent is not None:
+ cgroup_parent = service_data.cgroup_parent
+
+ container_name: Optional[str] = None
+ if service_data.container_name is not None:
+ container_name = service_data.container_name
+
+ env_file: List[str] = []
+ if service_data.env_file is not None:
+ if type(service_data.env_file) is spec.StringOrList:
+ if type(service_data.env_file.__root__) is spec.ListOfStrings:
+ env_file = service_data.env_file.__root__.__root__
+ elif type(service_data.env_file.__root__) is str:
+ env_file.append(service_data.env_file.__root__)
+
+ expose: List[str] = []
+ if service_data.expose is not None:
+ for port in service_data.expose:
+ expose.append(str(port))
+
+ profiles: List[str] = []
+ if service_data.profiles is not None:
+ if type(service_data.profiles) is spec.ListOfStrings:
+ profiles = service_data.profiles.__root__
+
+ devices: List[Device] = []
+ if service_data.devices is not None:
+ for device_data in service_data.devices:
+ if type(device_data) is str:
+ assert ":" in device_data, "Invalid volume input, aborting."
+
+ spilt_data = device_data.split(":")
+ if len(spilt_data) == 2:
+ devices.append(Device(host_path=spilt_data[0], container_path=spilt_data[1]))
+ elif len(spilt_data) == 3:
+ devices.append(
+ Device(
+ host_path=spilt_data[0],
+ container_path=spilt_data[1],
+ cgroup_permissions=spilt_data[2],
+ )
+ )
+
services.append(
Service(
name=service_name,
@@ -186,6 +231,12 @@ class Parser:
depends_on=service_depends_on,
volumes=service_volumes,
links=service_links,
+ cgroup_parent=cgroup_parent,
+ container_name=container_name,
+ env_file=env_file,
+ expose=expose,
+ profiles=profiles,
+ devices=devices,
)
)
diff --git a/examples/full-stack-node-app/compose-viz.png b/examples/full-stack-node-app/compose-viz.png
index 2ccd6ce..7c0f902 100644
Binary files a/examples/full-stack-node-app/compose-viz.png and b/examples/full-stack-node-app/compose-viz.png differ
diff --git a/examples/non-normative/compose-viz.png b/examples/non-normative/compose-viz.png
index 805094a..f4fdb1e 100644
Binary files a/examples/non-normative/compose-viz.png and b/examples/non-normative/compose-viz.png differ
diff --git a/examples/non-normative/docker-compose.yml b/examples/non-normative/docker-compose.yml
index 2ed41b9..8419c98 100644
--- a/examples/non-normative/docker-compose.yml
+++ b/examples/non-normative/docker-compose.yml
@@ -6,10 +6,19 @@ services:
- back-tier
monitoring:
+ env_file:
+ - a.env
+ - b.env
+ container_name: monitoring-server
image: awesome/monitoring
networks:
- admin
-
+ expose:
+ - 1234
+ profiles:
+ - tools
+ - foo
+ cgroup_parent: awesome-parent
backend:
networks:
@@ -36,8 +45,12 @@ services:
- "8000:5010"
links:
- "db:database"
+ cgroup_parent: awesome-parent
db:
image: postgres
+ devices:
+ - "/dev/ttyUSB2:/dev/ttyUSB3"
+ - "/dev/sda:/dev/xvda:rwm"
networks:
front-tier:
diff --git a/examples/voting-app/compose-viz.png b/examples/voting-app/compose-viz.png
index 64486c2..6a9fb8e 100644
Binary files a/examples/voting-app/compose-viz.png and b/examples/voting-app/compose-viz.png differ
diff --git a/examples/voting-app/compose-viz.svg b/examples/voting-app/compose-viz.svg
index f16a8ee..fb12a7a 100644
--- a/examples/voting-app/compose-viz.svg
+++ b/examples/voting-app/compose-viz.svg
@@ -38,8 +38,7 @@
0.0.0.06379->redis
-
-
+
6379
@@ -67,7 +66,8 @@
db-data
-
+
+
db-data
@@ -108,8 +108,7 @@
0.0.0.05000->vote
-
-
+
80
@@ -143,8 +142,7 @@
0.0.0.05001->result
-
-
+
80
@@ -181,7 +179,8 @@
/var/run/docker.sock
-
+
+
/var/run/docker.sock
@@ -201,8 +200,7 @@
0.0.0.08080->visualizer
-
-
+
8080
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 4e2f37e..503047b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -12,11 +12,17 @@ runner = CliRunner()
"test_file_path",
[
"tests/ymls/builds/docker-compose.yml",
+ "tests/ymls/cgroup_parent/docker-compose.yml",
+ "tests/ymls/container_name/docker-compose.yml",
"tests/ymls/depends_on/docker-compose.yml",
+ "tests/ymls/devices/docker-compose.yml",
+ "tests/ymls/env_file/docker-compose.yml",
+ "tests/ymls/expose/docker-compose.yml",
"tests/ymls/extends/docker-compose.yml",
"tests/ymls/links/docker-compose.yml",
"tests/ymls/networks/docker-compose.yml",
"tests/ymls/ports/docker-compose.yml",
+ "tests/ymls/profiles/docker-compose.yml",
"tests/ymls/volumes/docker-compose.yml",
"examples/full-stack-node-app/docker-compose.yml",
"examples/non-normative/docker-compose.yml",
diff --git a/tests/test_devices.py b/tests/test_devices.py
new file mode 100644
index 0000000..cbf0042
--- /dev/null
+++ b/tests/test_devices.py
@@ -0,0 +1,22 @@
+from compose_viz.models.device import Device
+
+
+def test_device_init_normal() -> None:
+ try:
+ d = Device(host_path="/dev/ttyUSB0", container_path="/dev/ttyUSB1")
+
+ assert d.host_path == "/dev/ttyUSB0"
+ assert d.container_path == "/dev/ttyUSB1"
+ except Exception as e:
+ assert False, e
+
+
+def test_device_with_cgroup_permissions() -> None:
+ try:
+ d = Device(host_path="/dev/sda1", container_path="/dev/xvda", cgroup_permissions="rwm")
+
+ assert d.host_path == "/dev/sda1"
+ assert d.container_path == "/dev/xvda"
+ assert d.cgroup_permissions == "rwm"
+ except Exception as e:
+ assert False, e
diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py
index 7bf3cdc..095c024 100644
--- a/tests/test_parse_file.py
+++ b/tests/test_parse_file.py
@@ -1,6 +1,7 @@
import pytest
from compose_viz.models.compose import Compose
+from compose_viz.models.device import Device
from compose_viz.models.extends import Extends
from compose_viz.models.port import Port, Protocol
from compose_viz.models.service import Service
@@ -241,6 +242,123 @@ from compose_viz.parser import Parser
],
),
),
+ (
+ "cgroup_parent/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ cgroup_parent="system",
+ ),
+ ],
+ ),
+ ),
+ (
+ "container_name/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ container_name="myfrontend",
+ ),
+ ],
+ ),
+ ),
+ (
+ "env_file/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ env_file=["a.env"],
+ ),
+ Service(
+ name="backend",
+ image="awesome/backend",
+ env_file=["b.env"],
+ ),
+ Service(
+ name="db",
+ image="awesome/db",
+ env_file=["c.env", "d.env"],
+ ),
+ ],
+ ),
+ ),
+ (
+ "expose/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ expose=["27118"],
+ ),
+ Service(
+ name="backend",
+ image="awesome/backend",
+ expose=["27017", "27018"],
+ ),
+ ],
+ ),
+ ),
+ (
+ "profiles/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ profiles=["frontend"],
+ ),
+ Service(
+ name="phpmyadmin",
+ image="phpmyadmin",
+ profiles=["debug"],
+ ),
+ Service(
+ name="db",
+ image="awesome/db",
+ profiles=["db", "sql"],
+ ),
+ ],
+ ),
+ ),
+ (
+ "devices/docker-compose",
+ Compose(
+ services=[
+ Service(
+ name="frontend",
+ image="awesome/frontend",
+ devices=[
+ Device(
+ host_path="/dev/ttyUSB0",
+ container_path="/dev/ttyUSB1",
+ )
+ ],
+ ),
+ Service(
+ name="backend",
+ image="awesome/backend",
+ devices=[
+ Device(
+ host_path="/dev/ttyUSB2",
+ container_path="/dev/ttyUSB3",
+ ),
+ Device(
+ host_path="/dev/sda",
+ container_path="/dev/xvda",
+ cgroup_permissions="rwm",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
],
)
def test_parse_file(test_file_path: str, expected: Compose) -> None:
@@ -275,3 +393,16 @@ def test_parse_file(test_file_path: str, expected: Compose) -> 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
+
+ assert actual_service.cgroup_parent == expected_service.cgroup_parent
+ assert actual_service.container_name == expected_service.container_name
+
+ assert actual_service.expose == expected_service.expose
+ assert actual_service.env_file == expected_service.env_file
+ assert actual_service.profiles == expected_service.profiles
+
+ assert len(actual_service.devices) == len(expected_service.devices)
+ for actual_device, expected_device in zip(actual_service.devices, expected_service.devices):
+ assert actual_device.host_path == expected_device.host_path
+ assert actual_device.container_path == expected_device.container_path
+ assert actual_device.cgroup_permissions == expected_device.cgroup_permissions
diff --git a/tests/ymls/cgroup_parent/docker-compose.yml b/tests/ymls/cgroup_parent/docker-compose.yml
new file mode 100644
index 0000000..6d41ad4
--- /dev/null
+++ b/tests/ymls/cgroup_parent/docker-compose.yml
@@ -0,0 +1,6 @@
+version: "3.9"
+
+services:
+ frontend:
+ image: awesome/frontend
+ cgroup_parent: "system"
diff --git a/tests/ymls/container_name/docker-compose.yml b/tests/ymls/container_name/docker-compose.yml
new file mode 100644
index 0000000..08047c2
--- /dev/null
+++ b/tests/ymls/container_name/docker-compose.yml
@@ -0,0 +1,6 @@
+version: "3.9"
+
+services:
+ frontend:
+ image: awesome/frontend
+ container_name: "myfrontend"
diff --git a/tests/ymls/devices/docker-compose.yml b/tests/ymls/devices/docker-compose.yml
new file mode 100644
index 0000000..2f810a3
--- /dev/null
+++ b/tests/ymls/devices/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3.9"
+
+services:
+ frontend:
+ image: awesome/frontend
+ devices:
+ - "/dev/ttyUSB0:/dev/ttyUSB1"
+ backend:
+ image: awesome/backend
+ devices:
+ - "/dev/ttyUSB2:/dev/ttyUSB3"
+ - "/dev/sda:/dev/xvda:rwm"
diff --git a/tests/ymls/env_file/docker-compose.yml b/tests/ymls/env_file/docker-compose.yml
new file mode 100644
index 0000000..d687153
--- /dev/null
+++ b/tests/ymls/env_file/docker-compose.yml
@@ -0,0 +1,15 @@
+version: "3.9"
+
+services:
+ frontend:
+ image: awesome/frontend
+ env_file: a.env
+ backend:
+ image: awesome/backend
+ env_file:
+ - b.env
+ db:
+ image: awesome/db
+ env_file:
+ - c.env
+ - d.env
diff --git a/tests/ymls/expose/docker-compose.yml b/tests/ymls/expose/docker-compose.yml
new file mode 100644
index 0000000..1e1bdee
--- /dev/null
+++ b/tests/ymls/expose/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3.9"
+
+services:
+ frontend:
+ image: awesome/frontend
+ expose:
+ - "27118"
+ backend:
+ image: awesome/backend
+ expose:
+ - "27017"
+ - "27018"
diff --git a/tests/ymls/profiles/docker-compose.yml b/tests/ymls/profiles/docker-compose.yml
new file mode 100644
index 0000000..39282af
--- /dev/null
+++ b/tests/ymls/profiles/docker-compose.yml
@@ -0,0 +1,14 @@
+version: "3.9"
+services:
+ frontend:
+ image: awesome/frontend
+ profiles: ["frontend"]
+ phpmyadmin:
+ image: phpmyadmin
+ profiles:
+ - debug
+ db:
+ image: awesome/db
+ profiles:
+ - db
+ - sql