Compare commits

..

No commits in common. "main" and "v0.2.1" have entirely different histories.
main ... v0.2.1

35 changed files with 2209 additions and 2427 deletions

View file

@ -20,15 +20,27 @@ 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:
python-version: '3.10.4'
- name: Setup Poetry
uses: abatilo/actions-poetry@v3
uses: Gr1N/setup-poetry@v7
with:
poetry-version: 1.8.2
poetry-version: 1.1.7
- name: Install Dependencies
run: |

View file

@ -32,9 +32,9 @@ jobs:
python-version: '3.10.4'
- name: Setup Poetry
uses: abatilo/actions-poetry@v3
uses: Gr1N/setup-poetry@v7
with:
poetry-version: 1.8.2
poetry-version: 1.1.7
- run: |
poetry install --no-root
poetry build
@ -46,5 +46,4 @@ jobs:
prerelease: false
files: |
LICENSE
Dockerfile
dist/**

View file

@ -1,79 +0,0 @@
name: Update Submodules
on:
push:
branches: [ dev ]
schedule:
- cron: '0 0 * * *'
jobs:
check_submodules:
name: Check Submodules
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.check.outputs.has_changes }}
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Create new branch and push changes
run: |
git submodule update --remote
- name: Check for changes
id: check
run: |
git diff --quiet || echo "::set-output name=has_changes::true"
update_submodules:
name: Update Submodules
runs-on: ubuntu-latest
needs: [check_submodules]
if: needs.check_submodules.outputs.has_changes == 'true'
steps:
- name: Setup Python 3.10.4
uses: actions/setup-python@v3
with:
python-version: '3.10.4'
- name: Setup Poetry
uses: abatilo/actions-poetry@v3
with:
poetry-version: 1.8.2
- name: Install Dependencies
run: |
poetry install --no-root
- name: Update Submodule
run: |
datamodel-codegen --input ./compose-spec/schema/compose-spec.json --output-model-type pydantic_v2.BaseModel --field-constraints --output ./compose_viz/spec/compose_spec.py
poetry run python ./update-submodules.py
- name: Execute pre-commit
continue-on-error: true
run: |
poetry run python -m pre_commit run --all-files
- name: Push changes
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git checkout -b $GITHUB_RUN_ID
git commit -am "chore: update submodules"
git push --set-upstream origin $GITHUB_RUN_ID
- name: File PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.pulls.create({
owner: '${{ github.repository_owner }}',
repo: 'compose-viz',
head: process.env.GITHUB_RUN_ID,
base: 'main',
title: `chore: update submodules (${process.env.GITHUB_RUN_ID})`,
body: `Please add the version tag to trigger the release.`,
});

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "compose-spec"]
path = compose-spec
url = https://github.com/compose-spec/compose-spec.git

View file

@ -18,7 +18,7 @@ repos:
args:
- "--max-line-length=120"
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/psf/black

View file

@ -76,33 +76,19 @@ You need to install [Graphviz](https://graphviz.org/download/) to generate graph
#### Using `pip`
```
pip install compose-viz
```
`pip install compose-viz`
#### Using `.whl`
See [releases](https://github.com/compose-viz/compose-viz/releases).
#### Docker Image
See [wst24365888/compose-viz](https://hub.docker.com/r/wst24365888/compose-viz/tags).
### Example
This example yml is from [docker compose beginner tutorial](https://github.com/docker/labs/blob/master/beginner/chapters/votingapp.md).
```bash
cd examples/voting-app/
# using python script
cpv -m svg docker-compose.yml
# using docker image
docker run --rm -it -v $(pwd):/in wst24365888/compose-viz -m svg docker-compose.yml
# using docker image in powershell
docker run --rm -it -v ${pwd}:/in wst24365888/compose-viz -m svg docker-compose.yml
```
And this is what the result looks like:
@ -117,14 +103,12 @@ Check out the result [here](https://github.com/compose-viz/compose-viz/blob/main
### Options
| Option | Description |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-o, --output-filename FILENAME` | Output filename for the generated visualization file. [default: compose-viz] |
| `-m, --format FORMAT` | Output format for the generated visualization file. See [supported formats](https://github.com/compose-viz/compose-viz/blob/main/compose_viz/models/viz_formats.py). [default: png] |
| `-r, --root-service SERVICE_NAME` | Root of the service tree (convenient for large compose yamls) |
| `-l, --legend` | Include a legend in the visualization. |
| `-v, --version` | Show the version of compose-viz. |
| `--help` | Show help and exit. |
| Option | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-o, --output-filename` | Output filename for the generated visualization file. [default: compose-viz] |
| `-m, --format` | Output format for the generated visualization file. See [supported formats](https://github.com/compose-viz/compose-viz/blob/main/compose_viz/models/viz_formats.py). [default: png] |
| `-v, --version` | Show the version of compose-viz. |
| `--help` | Show help and exit. |
<p align="right">(<a href="#top">back to top</a>)</p>
@ -178,7 +162,7 @@ for more information.
- HSING-HAN, WU (Xyphuz)
- Mail me: xyphuzwu@gmail.com
- About me: <https://www.xyphuz.com>
- About me: <https://about.xyphuz.com>
- GitHub: <https://github.com/wst24365888>
### Project Link

@ -1 +0,0 @@
Subproject commit c9480da2ad9670c2e99126f4aad8f1ffbf6d4a9a

View file

@ -1,2 +1,2 @@
__app_name__ = "compose_viz"
__version__ = "0.3.2"
__version__ = "0.2.1"

View file

@ -36,18 +36,6 @@ def compose_viz(
"-m",
help="Output format for the generated visualization file.",
),
root_service: str = typer.Option(
None,
"--root-service",
"-r",
help="Root of the service tree (convenient for large compose yamls)",
),
include_legend: bool = typer.Option(
False,
"--legend",
"-l",
help="Include a legend in the visualization.",
),
_: Optional[bool] = typer.Option(
None,
"--version",
@ -58,12 +46,12 @@ def compose_viz(
),
) -> None:
parser = Parser()
compose = parser.parse(input_path, root_service=root_service)
compose = parser.parse(input_path)
if compose:
typer.echo(f"Successfully parsed {input_path}")
Graph(compose, output_filename, include_legend).render(format)
Graph(compose, output_filename).render(format)
raise typer.Exit()

View file

@ -3,16 +3,15 @@ from typing import Optional
import graphviz
from compose_viz.models.compose import Compose
from compose_viz.models.port import AppProtocol, Protocol
def apply_vertex_style(type: str) -> dict:
def apply_vertex_style(type) -> dict:
style = {
"service": {
"shape": "component",
},
"volume": {
"shape": "cylinder",
"shape": "folder",
},
"network": {
"shape": "pentagon",
@ -20,39 +19,24 @@ def apply_vertex_style(type: str) -> 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: str) -> dict:
def apply_edge_style(type) -> dict:
style = {
"exposes": {
"ports": {
"style": "solid",
"dir": "both",
},
"links": {
"style": "solid",
},
"volumes_rw": {
"volumes": {
"style": "dashed",
"dir": "both",
},
"volumes_ro": {
"style": "dashed",
},
"depends_on": {
"style": "dotted",
},
@ -61,67 +45,18 @@ def apply_edge_style(type: str) -> dict:
"arrowhead": "inv",
"arrowtail": "dot",
},
"env_file": {
"style": "solid",
},
}
return style[type]
class Graph:
def __init__(self, compose: Compose, filename: str, include_legend: bool) -> None:
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
if include_legend:
self.dot.attr(rankdir="LR")
with self.dot.subgraph(name="cluster_edge_") as edge:
edge.attr(label="Edge")
edge.node("line_0_l", style="invis")
edge.node("line_0_r", style="invis")
edge.edge("line_0_l", "line_0_r", label="exposes", **apply_edge_style("exposes"))
edge.node("line_1_l", style="invis")
edge.node("line_1_r", style="invis")
edge.edge("line_1_l", "line_1_r", label="links", **apply_edge_style("links"))
edge.node("line_2_l", style="invis")
edge.node("line_2_r", style="invis")
edge.edge("line_2_l", "line_2_r", label="volumes_rw", **apply_edge_style("volumes_rw"))
edge.node("line_3_l", style="invis")
edge.node("line_3_r", style="invis")
edge.edge("line_3_l", "line_3_r", label="volumes_ro", **apply_edge_style("volumes_ro"))
edge.node("line_4_l", style="invis")
edge.node("line_4_r", style="invis")
edge.edge("line_4_l", "line_4_r", label="depends_on", **apply_edge_style("depends_on"))
edge.node("line_5_l", style="invis")
edge.node("line_5_r", style="invis")
edge.edge("line_5_l", "line_5_r", label="extends", **apply_edge_style("extends"))
with self.dot.subgraph(name="cluster_node_") as node:
node.attr(label="Node")
node.node("service", shape="component", label="Service\n(image)")
node.node("volume", shape="cylinder", label="Volume")
node.node("network", shape="pentagon", label="Network")
node.node("port", shape="circle", label="Port")
node.node("env_file", shape="tab", label="Env File")
node.node("profile", shape="invhouse", label="Profile")
node.node("cgroup", shape="diamond", label="CGroupe")
node.node("device", shape="box3d", label="Device")
node.body.append("{ rank=source;service network env_file cgroup }")
self.dot.node("inv", style="invis")
self.dot.edge("inv", "network", style="invis")
self.dot.edge("port", "line_2_l", style="invis")
def validate_name(self, name: str) -> str:
# graphviz does not allow ':' in node name
transTable = name.maketrans({":": ""})
@ -136,45 +71,19 @@ 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.container_name if service.container_name else service.name}\n({service.image})",
)
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")
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_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")
self.add_edge(service.name, volume.source, "volumes", lable=volume.target)
for port in service.ports:
self.add_vertex(port.host_port, "port", lable=port.host_port)
self.add_edge(
port.host_port,
service.name,
"links",
lable=port.container_port
+ (("/" + port.protocol) if port.protocol != Protocol.any.value else "")
+ (("\n(" + port.app_protocol + ")") if port.app_protocol != AppProtocol.na.value else ""),
)
for env_file in service.env_file:
self.add_vertex(env_file, "env_file")
self.add_edge(env_file, service.name, "env_file")
self.add_edge(port.host_port, service.name, "ports", lable=port.container_port)
for link in service.links:
if ":" in link:
service_name, alias = link.split(":", 1)
@ -183,13 +92,5 @@ 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)

View file

@ -1,20 +0,0 @@
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

View file

@ -7,27 +7,11 @@ class Protocol(str, Enum):
any = "any"
class AppProtocol(str, Enum):
rest = "REST"
mqtt = "MQTT"
wbsock = "WebSocket"
http = "http"
https = "https"
na = "NA"
class Port:
def __init__(
self,
host_port: str,
container_port: str,
protocol: Protocol = Protocol.any,
app_protocol: AppProtocol = AppProtocol.na,
):
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
self._app_protocol = app_protocol
@property
def host_port(self):
@ -40,7 +24,3 @@ class Port:
@property
def protocol(self):
return self._protocol
@property
def app_protocol(self):
return self._app_protocol

View file

@ -1,6 +1,5 @@
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
@ -17,12 +16,6 @@ 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
@ -32,12 +25,6 @@ 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):
@ -70,27 +57,3 @@ 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

View file

@ -1,13 +1,12 @@
import re
from typing import Any, Dict, List, Optional, Union
from typing import List, Optional
from pydantic_yaml import parse_yaml_raw_as
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 AppProtocol, Port, Protocol
from compose_viz.models.port import Port, Protocol
from compose_viz.models.volume import Volume, VolumeType
@ -15,57 +14,26 @@ class Parser:
def __init__(self):
pass
@staticmethod
def _unwrap_depends_on(data_depends_on: Union[spec.ListOfStrings, Dict[Any, spec.DependsOn], None]) -> List[str]:
service_depends_on = []
if type(data_depends_on) is spec.ListOfStrings:
service_depends_on = data_depends_on.root
elif type(data_depends_on) is dict:
for depends_on in data_depends_on.keys():
service_depends_on.append(str(depends_on))
return service_depends_on
@staticmethod
def compile_dependencies(service_name: str, services: Dict[Any, spec.Service], file_path: str) -> List[str]:
assert service_name in services, f"Service '{service_name}' not found in given compose file: '{file_path}'"
dependencies = []
for dependency in Parser._unwrap_depends_on(services[service_name].depends_on):
if dependency:
dependencies.append(dependency)
dependencies.extend(Parser.compile_dependencies(dependency, services, file_path))
return dependencies
def parse(self, file_path: str, root_service: Optional[str] = None) -> Compose:
def parse(self, file_path: str) -> Compose:
compose_data: spec.ComposeSpecification
try:
with open(file_path, "r") as file:
file_content = file.read()
compose_data = parse_yaml_raw_as(spec.ComposeSpecification, file_content)
except Exception as e:
compose_data = spec.ComposeSpecification.parse_file(file_path)
except ValidationError as e:
raise RuntimeError(f"Error parsing file '{file_path}': {e}")
services: List[Service] = []
assert compose_data.services is not None, "No services found, aborting."
root_dependencies: List[str] = []
if root_service:
root_dependencies = Parser.compile_dependencies(root_service, compose_data.services, file_path)
root_dependencies.append(root_service)
root_dependencies = list(set(root_dependencies))
for service_name, service_data in compose_data.services.items():
service_name = str(service_name)
if root_service and service_name not in root_dependencies:
continue
service_image: Optional[str] = None
if service_data.build is not None:
if type(service_data.build) is str:
service_image = f"build from '{service_data.build}'"
elif type(service_data.build) is spec.Build:
elif type(service_data.build) is spec.BuildItem:
if service_data.build.context is not None and service_data.build.dockerfile is not None:
service_image = (
f"build from '{service_data.build.context}' using '{service_data.build.dockerfile}'"
@ -81,7 +49,7 @@ class Parser:
service_networks: List[str] = []
if service_data.networks is not None:
if type(service_data.networks) is spec.ListOfStrings:
service_networks = service_data.networks.root
service_networks = service_data.networks.__root__
elif type(service_data.networks) is dict:
service_networks = list(service_data.networks.keys())
@ -89,7 +57,7 @@ class Parser:
if service_data.extends is not None:
# https://github.com/compose-spec/compose-spec/blob/master/spec.md#extends
# The value of the extends key MUST be a dictionary.
assert type(service_data.extends) is spec.Extends
assert type(service_data.extends) is spec.Extend
service_extends = Extends(
service_name=service_data.extends.service, from_file=service_data.extends.file
)
@ -101,13 +69,12 @@ class Parser:
host_port: Optional[str] = None
container_port: Optional[str] = None
protocol: Optional[str] = None
app_protocol: Optional[str] = None
if type(port_data) is float:
container_port = str(int(port_data))
host_port = f"0.0.0.0:{container_port}"
elif type(port_data) is str:
regex = r"((?P<host_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:|(\$\{([^}]+)\}):)|:|)?((?P<host_port>\d+(\-\d+)?):)?((?P<container_port>\d+(\-\d+)?))?(/(?P<protocol>\w+))?" # noqa: E501
regex = r"(?P<host_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:)?((?P<host_port>\d+(\-\d+)?):)?((?P<container_port>\d+(\-\d+)?))?(/(?P<protocol>\w+))?" # noqa: E501
match = re.match(regex, port_data)
if match:
@ -125,18 +92,20 @@ class Parser:
host_port = f"{host_ip}{host_port}"
else:
host_port = f"0.0.0.0:{host_port}"
elif type(port_data) is spec.Ports:
elif type(port_data) is spec.Port:
assert port_data.target is not None, "Invalid port format, aborting."
if type(port_data.published) is str or type(port_data.published) is int:
host_port = str(port_data.published)
# ruamel.yaml does not parse port as int
assert type(port_data.published) is not int
if type(port_data.published) is str:
host_port = port_data.published
if type(port_data.target) is int:
container_port = str(port_data.target)
host_ip = port_data.host_ip
protocol = port_data.protocol
app_protocol = port_data.app_protocol
if container_port is not None and host_port is None:
host_port = container_port
@ -152,21 +121,21 @@ class Parser:
if protocol is None:
protocol = "any"
if app_protocol is None:
app_protocol = "na"
service_ports.append(
Port(
host_port=host_port,
container_port=container_port,
protocol=Protocol[protocol],
app_protocol=AppProtocol[app_protocol],
)
)
service_depends_on: List[str] = []
if service_data.depends_on is not None:
service_depends_on = Parser._unwrap_depends_on(service_data.depends_on)
if type(service_data.depends_on) is spec.ListOfStrings:
service_depends_on = service_data.depends_on.__root__
elif type(service_data.depends_on) is dict:
for depends_on in service_data.depends_on.keys():
service_depends_on.append(str(depends_on))
service_volumes: List[Volume] = []
if service_data.volumes is not None:
@ -185,7 +154,7 @@ class Parser:
access_mode=spilt_data[2],
)
)
elif type(volume_data) is spec.Volumes:
elif type(volume_data) is spec.ServiceVolume:
assert volume_data.target is not None, "Invalid volume input, aborting."
# https://github.com/compose-spec/compose-spec/blob/master/spec.md#long-syntax-4
@ -207,55 +176,6 @@ 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.root) is str:
env_file = [service_data.env_file.root]
elif type(service_data.env_file.root) is list:
for env_file_data in service_data.env_file.root:
if type(env_file_data) is str:
env_file.append(env_file_data)
elif type(env_file_data) is spec.EnvFilePath:
env_file.append(env_file_data.path)
else:
print(f"Invalid env_file data: {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,
@ -266,12 +186,6 @@ 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,
)
)

View file

@ -0,0 +1,813 @@
{
"$schema": "http://json-schema.org/draft/2019-09/schema#",
"id": "compose_spec.json",
"type": "object",
"title": "Compose Specification",
"description": "The Compose file is a YAML file defining a multi-containers based application.",
"properties": {
"version": {
"type": "string",
"description": "declared for backward compatibility, ignored."
},
"name": {
"type": "string",
"description": "define the Compose project name, until user defines one explicitly."
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
},
"secrets": {
"id": "#/properties/secrets",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/secret"
}
},
"additionalProperties": false
},
"configs": {
"id": "#/properties/configs",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/config"
}
},
"additionalProperties": false
}
},
"patternProperties": {"^x-": {}},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"deploy": {"$ref": "#/definitions/deployment"},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
"ssh": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"cache_from": {"type": "array", "items": {"type": "string"}},
"cache_to": {"type": "array", "items": {"type": "string"}},
"no_cache": {"type": "boolean"},
"network": {"type": "string"},
"pull": {"type": "boolean"},
"target": {"type": "string"},
"shm_size": {"type": ["integer", "string"]},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"isolation": {"type": "string"},
"secrets": {"$ref": "#/definitions/service_config_or_secret"},
"tags":{"type": "array", "items": {"type": "string"}}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
]
},
"blkio_config": {
"type": "object",
"properties": {
"device_read_bps": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_read_iops": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_write_bps": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_write_iops": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"weight": {"type": "integer"},
"weight_device": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_weight"}
}
},
"additionalProperties": false
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"configs": {"$ref": "#/definitions/service_config_or_secret"},
"container_name": {"type": "string"},
"cpu_count": {"type": "integer", "minimum": 0},
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]},
"cpu_rt_period": {"type": ["number", "string"]},
"cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"credential_spec": {
"type": "object",
"properties": {
"config": {"type": "string"},
"file": {"type": "string"},
"registry": {"type": "string"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"depends_on": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"condition": {
"type": "string",
"enum": ["service_started", "service_healthy", "service_completed_successfully"]
}
},
"required": ["condition"]
}
}
}
]
},
"device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_opt": {"type": "array","items": {"type": "string"}, "uniqueItems": true},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"extends": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"group_add": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"init": {"type": "boolean"},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number", "null"]}
}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"mem_reservation": {"type": ["string", "integer"]},
"mem_swappiness": {"type": "integer"},
"memswap_limit": {"type": ["number", "string"]},
"network_mode": {"type": "string"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"},
"link_local_ips": {"$ref": "#/definitions/list_of_strings"},
"priority": {"type": "number"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"oom_kill_disable": {"type": "boolean"},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"pid": {"type": ["string", "null"]},
"pids_limit": {"type": ["number", "string"]},
"platform": {"type": "string"},
"ports": {
"type": "array",
"items": {
"oneOf": [
{"type": "number", "format": "ports"},
{"type": "string", "format": "ports"},
{
"type": "object",
"properties": {
"mode": {"type": "string"},
"host_ip": {"type": "string"},
"target": {"type": "integer"},
"published": {"type": ["string", "integer"]},
"protocol": {"type": "string"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
]
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"profiles": {"$ref": "#/definitions/list_of_strings"},
"pull_policy": {"type": "string", "enum": [
"always", "never", "if_not_present", "build", "missing"
]},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"runtime": {
"type": "string"
},
"scale": {
"type": "integer"
},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"secrets": {"$ref": "#/definitions/service_config_or_secret"},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"storage_opt": {"type": "object"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type": "object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
]
}
}
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"required": ["type"],
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
"target": {"type": "string"},
"read_only": {"type": "boolean"},
"consistency": {"type": "string"},
"bind": {
"type": "object",
"properties": {
"propagation": {"type": "string"},
"create_host_path": {"type": "boolean"},
"selinux": {"type": "string", "enum": ["z", "Z"]}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"volume": {
"type": "object",
"properties": {
"nocopy": {"type": "boolean"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"tmpfs": {
"type": "object",
"properties": {
"size": {
"oneOf": [
{"type": "integer", "minimum": 0},
{"type": "string"}
]
},
"mode": {"type": "number"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
]
},
"uniqueItems": true
},
"volumes_from": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"working_dir": {"type": "string"}
},
"patternProperties": {"^x-": {}},
"additionalProperties": false
},
"healthcheck": {
"id": "#/definitions/healthcheck",
"type": "object",
"properties": {
"disable": {"type": "boolean"},
"interval": {"type": "string", "format": "duration"},
"retries": {"type": "number"},
"test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"timeout": {"type": "string", "format": "duration"},
"start_period": {"type": "string", "format": "duration"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"deployment": {
"id": "#/definitions/deployment",
"type": ["object", "null"],
"properties": {
"mode": {"type": "string"},
"endpoint_mode": {"type": "string"},
"replicas": {"type": "integer"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"rollback_config": {
"type": "object",
"properties": {
"parallelism": {"type": "integer"},
"delay": {"type": "string", "format": "duration"},
"failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"},
"max_failure_ratio": {"type": "number"},
"order": {"type": "string", "enum": [
"start-first", "stop-first"
]}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"update_config": {
"type": "object",
"properties": {
"parallelism": {"type": "integer"},
"delay": {"type": "string", "format": "duration"},
"failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"},
"max_failure_ratio": {"type": "number"},
"order": {"type": "string", "enum": [
"start-first", "stop-first"
]}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"resources": {
"type": "object",
"properties": {
"limits": {
"type": "object",
"properties": {
"cpus": {"type": ["number", "string"]},
"memory": {"type": "string"},
"pids": {"type": "integer"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"reservations": {
"type": "object",
"properties": {
"cpus": {"type": ["number", "string"]},
"memory": {"type": "string"},
"generic_resources": {"$ref": "#/definitions/generic_resources"},
"devices": {"$ref": "#/definitions/devices"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"restart_policy": {
"type": "object",
"properties": {
"condition": {"type": "string"},
"delay": {"type": "string", "format": "duration"},
"max_attempts": {"type": "integer"},
"window": {"type": "string", "format": "duration"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"placement": {
"type": "object",
"properties": {
"constraints": {"type": "array", "items": {"type": "string"}},
"preferences": {
"type": "array",
"items": {
"type": "object",
"properties": {
"spread": {"type": "string"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"max_replicas_per_node": {"type": "integer"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"generic_resources": {
"id": "#/definitions/generic_resources",
"type": "array",
"items": {
"type": "object",
"properties": {
"discrete_resource_spec": {
"type": "object",
"properties": {
"kind": {"type": "string"},
"value": {"type": "number"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"devices": {
"id": "#/definitions/devices",
"type": "array",
"items": {
"type": "object",
"properties": {
"capabilities": {"$ref": "#/definitions/list_of_strings"},
"count": {"type": ["string", "integer"]},
"device_ids": {"$ref": "#/definitions/list_of_strings"},
"driver":{"type": "string"},
"options":{"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"network": {
"id": "#/definitions/network",
"type": ["object", "null"],
"properties": {
"name": {"type": "string"},
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subnet": {"type": "string", "format": "subnet_ip_address"},
"ip_range": {"type": "string"},
"gateway": {"type": "string"},
"aux_addresses": {
"type": "object",
"additionalProperties": false,
"patternProperties": {"^.+$": {"type": "string"}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
},
"options": {
"type": "object",
"additionalProperties": false,
"patternProperties": {"^.+$": {"type": "string"}}
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {
"deprecated": true,
"type": "string"
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"internal": {"type": "boolean"},
"enable_ipv6": {"type": "boolean"},
"attachable": {"type": "boolean"},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"name": {"type": "string"},
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {
"deprecated": true,
"type": "string"
}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"secret": {
"id": "#/definitions/secret",
"type": "object",
"properties": {
"name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"},
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"template_driver": {"type": "string"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"config": {
"id": "#/definitions/config",
"type": "object",
"properties": {
"name": {"type": "string"},
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {
"deprecated": true,
"type": "string"
}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"},
"template_driver": {"type": "string"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "boolean", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"blkio_limit": {
"type": "object",
"properties": {
"path": {"type": "string"},
"rate": {"type": ["integer", "string"]}
},
"additionalProperties": false
},
"blkio_weight": {
"type": "object",
"properties": {
"path": {"type": "string"},
"weight": {"type": "integer"}
},
"additionalProperties": false
},
"service_config_or_secret": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
},
"additionalProperties": false,
"patternProperties": {"^x-": {}}
}
]
}
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}

View file

@ -1,24 +1,20 @@
# generated by datamodel-codegen:
# filename: compose-spec.json
# timestamp: 2024-04-27T08:31:04+00:00
# timestamp: 2022-05-27T05:44:40+00:00
from __future__ import annotations
from enum import Enum
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, ConfigDict, Field, RootModel
from pydantic import Extra, Field, conint, constr
from pydantic_yaml import YamlModel
class Cgroup(Enum):
host = "host"
private = "private"
class CredentialSpec(YamlModel):
class Config:
extra = Extra.forbid
class CredentialSpec(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
config: Optional[str] = None
file: Optional[str] = None
registry: Optional[str] = None
@ -30,42 +26,38 @@ class Condition(Enum):
service_completed_successfully = "service_completed_successfully"
class DependsOn(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
restart: Optional[bool] = None
required: Optional[bool] = True
class DependsOn(YamlModel):
class Config:
extra = Extra.forbid
condition: Condition
class Extends(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Extend(YamlModel):
class Config:
extra = Extra.forbid
service: str
file: Optional[str] = None
class Logging(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Logging(YamlModel):
class Config:
extra = Extra.forbid
driver: Optional[str] = None
options: Optional[Dict[str, Optional[Union[str, float]]]] = None
options: Optional[Dict[constr(regex=r"^.+$"), Optional[Union[str, float]]]] = None # type: ignore # noqa: F722
class Ports(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
name: Optional[str] = None
class Port(YamlModel):
class Config:
extra = Extra.forbid
mode: Optional[str] = None
host_ip: Optional[str] = None
target: Optional[int] = None
published: Optional[Union[str, int]] = None
protocol: Optional[str] = None
app_protocol: Optional[str] = None
class PullPolicy(Enum):
@ -76,44 +68,47 @@ class PullPolicy(Enum):
missing = "missing"
class Ulimit(YamlModel):
class Config:
extra = Extra.forbid
hard: int
soft: int
class Selinux(Enum):
z = "z"
Z = "Z"
class Bind(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Bind(YamlModel):
class Config:
extra = Extra.forbid
propagation: Optional[str] = None
create_host_path: Optional[bool] = None
selinux: Optional[Selinux] = None
class AdditionalVolumeOption(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class AdditionalVolumeOption(YamlModel):
class Config:
extra = Extra.forbid
nocopy: Optional[bool] = None
subpath: Optional[str] = None
class Size(RootModel[int]):
root: int = Field(..., ge=0)
class Tmpfs(YamlModel):
class Config:
extra = Extra.forbid
class Tmpfs(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
size: Optional[Union[Size, str]] = None
size: Optional[Union[conint(ge=0), str]] = None # type: ignore
mode: Optional[float] = None
class Volumes(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ServiceVolume(YamlModel):
class Config:
extra = Extra.forbid
type: str
source: Optional[str] = None
target: Optional[str] = None
@ -124,34 +119,16 @@ class Volumes(BaseModel):
tmpfs: Optional[Tmpfs] = None
class Healthcheck(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Healthcheck(YamlModel):
class Config:
extra = Extra.forbid
disable: Optional[bool] = None
interval: Optional[str] = None
retries: Optional[float] = None
test: Optional[Union[str, List[str]]] = None
timeout: Optional[str] = None
start_period: Optional[str] = None
start_interval: Optional[str] = None
class Action(Enum):
rebuild = "rebuild"
sync = "sync"
sync_restart = "sync+restart"
class WatchItem(BaseModel):
ignore: Optional[List[str]] = None
path: str
action: Action
target: Optional[str] = None
class Development(BaseModel):
watch: Optional[List[WatchItem]] = None
class Order(Enum):
@ -159,10 +136,10 @@ class Order(Enum):
stop_first = "stop-first"
class RollbackConfig(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class RollbackConfig(YamlModel):
class Config:
extra = Extra.forbid
parallelism: Optional[int] = None
delay: Optional[str] = None
failure_action: Optional[str] = None
@ -171,150 +148,146 @@ class RollbackConfig(BaseModel):
order: Optional[Order] = None
class UpdateConfig(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ConfigOrder(Enum):
start_first = "start-first"
stop_first = "stop-first"
class UpdateConfig(YamlModel):
class Config:
extra = Extra.forbid
parallelism: Optional[int] = None
delay: Optional[str] = None
failure_action: Optional[str] = None
monitor: Optional[str] = None
max_failure_ratio: Optional[float] = None
order: Optional[Order] = None
order: Optional[ConfigOrder] = None
class Limits(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Limits(YamlModel):
class Config:
extra = Extra.forbid
cpus: Optional[Union[float, str]] = None
memory: Optional[str] = None
pids: Optional[int] = None
class RestartPolicy(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class RestartPolicy(YamlModel):
class Config:
extra = Extra.forbid
condition: Optional[str] = None
delay: Optional[str] = None
max_attempts: Optional[int] = None
window: Optional[str] = None
class Preference(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Preference(YamlModel):
class Config:
extra = Extra.forbid
spread: Optional[str] = None
class Placement(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Placement(YamlModel):
class Config:
extra = Extra.forbid
constraints: Optional[List[str]] = None
preferences: Optional[List[Preference]] = None
max_replicas_per_node: Optional[int] = None
class DiscreteResourceSpec(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class DiscreteResourceSpec(YamlModel):
class Config:
extra = Extra.forbid
kind: Optional[str] = None
value: Optional[float] = None
class GenericResource(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class GenericResource(YamlModel):
class Config:
extra = Extra.forbid
discrete_resource_spec: Optional[DiscreteResourceSpec] = None
class GenericResources(RootModel[List[GenericResource]]):
root: List[GenericResource]
class GenericResources(YamlModel):
__root__: List[GenericResource]
class ConfigItem(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ConfigItem(YamlModel):
class Config:
extra = Extra.forbid
subnet: Optional[str] = None
ip_range: Optional[str] = None
gateway: Optional[str] = None
aux_addresses: Optional[Dict[str, str]] = None
aux_addresses: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722
class Ipam(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Ipam(YamlModel):
class Config:
extra = Extra.forbid
driver: Optional[str] = None
config: Optional[List[ConfigItem]] = None
options: Optional[Dict[str, str]] = None
options: Optional[Dict[constr(regex=r"^.+$"), str]] = None # type: ignore # noqa: F722
class ExternalVolumeNetwork(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ExternalNetwork(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
class ExternalConfig(BaseModel):
class ExternalVolume(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
class Command(RootModel[Optional[Union[str, List[str]]]]):
root: Optional[Union[str, List[str]]]
class ExternalSecret(YamlModel):
name: Optional[str] = None
class EnvFilePath(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
path: str
required: Optional[bool] = True
class ExternalConfig(YamlModel):
name: Optional[str] = None
class EnvFile(RootModel[Union[str, List[Union[str, EnvFilePath]]]]):
root: Union[str, List[Union[str, EnvFilePath]]]
class ListOfStrings(YamlModel):
__root__: List[str] = Field(..., unique_items=True)
class ListOfStrings(RootModel[List[str]]):
root: List[str]
class ListOrDict(YamlModel):
__root__: Union[Dict[constr(regex=r".+"), Optional[Union[str, float, bool]]], List[str]] # type: ignore # noqa: F722, E501
class ListOrDict1(RootModel[List[Any]]):
root: List[Any]
class BlkioLimit(YamlModel):
class Config:
extra = Extra.forbid
class ListOrDict(RootModel[Union[Dict[str, Optional[Union[str, float, bool]]], ListOrDict1]]):
root: Union[Dict[str, Optional[Union[str, float, bool]]], ListOrDict1]
class BlkioLimit(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
path: Optional[str] = None
rate: Optional[Union[int, str]] = None
class BlkioWeight(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class BlkioWeight(YamlModel):
class Config:
extra = Extra.forbid
path: Optional[str] = None
weight: Optional[int] = None
class ServiceConfigOrSecret1(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ServiceConfigOrSecretItem(YamlModel):
class Config:
extra = Extra.forbid
source: Optional[str] = None
target: Optional[str] = None
uid: Optional[str] = None
@ -322,58 +295,40 @@ class ServiceConfigOrSecret1(BaseModel):
mode: Optional[float] = None
class ServiceConfigOrSecret(RootModel[List[Union[str, ServiceConfigOrSecret1]]]):
root: List[Union[str, ServiceConfigOrSecret1]]
class ServiceConfigOrSecret(YamlModel):
__root__: List[Union[str, ServiceConfigOrSecretItem]]
class Ulimits1(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
hard: int
soft: int
class Constraints(YamlModel):
__root__: Any
class Ulimits(RootModel[Dict[str, Union[int, Ulimits1]]]):
root: Dict[str, Union[int, Ulimits1]]
class BuildItem(YamlModel):
class Config:
extra = Extra.forbid
class Constraints(RootModel[Any]):
root: Any
class Build(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
context: Optional[str] = None
dockerfile: Optional[str] = None
dockerfile_inline: Optional[str] = None
entitlements: Optional[List[str]] = None
args: Optional[ListOrDict] = None
ssh: Optional[ListOrDict] = None
labels: Optional[ListOrDict] = None
cache_from: Optional[List[str]] = None
cache_to: Optional[List[str]] = None
no_cache: Optional[bool] = None
additional_contexts: Optional[ListOrDict] = None
network: Optional[str] = None
pull: Optional[bool] = None
target: Optional[str] = None
shm_size: Optional[Union[int, str]] = None
extra_hosts: Optional[ListOrDict] = None
isolation: Optional[str] = None
privileged: Optional[bool] = None
secrets: Optional[ServiceConfigOrSecret] = None
tags: Optional[List[str]] = None
ulimits: Optional[Ulimits] = None
platforms: Optional[List[str]] = None
class BlkioConfig(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class BlkioConfig(YamlModel):
class Config:
extra = Extra.forbid
device_read_bps: Optional[List[BlkioLimit]] = None
device_read_iops: Optional[List[BlkioLimit]] = None
device_write_bps: Optional[List[BlkioLimit]] = None
@ -382,22 +337,21 @@ class BlkioConfig(BaseModel):
weight_device: Optional[List[BlkioWeight]] = None
class Networks(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ServiceNetwork(YamlModel):
class Config:
extra = Extra.forbid
aliases: Optional[ListOfStrings] = None
ipv4_address: Optional[str] = None
ipv6_address: Optional[str] = None
link_local_ips: Optional[ListOfStrings] = None
mac_address: Optional[str] = None
priority: Optional[float] = None
class Device(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Device(YamlModel):
class Config:
extra = Extra.forbid
capabilities: Optional[ListOfStrings] = None
count: Optional[Union[str, int]] = None
device_ids: Optional[ListOfStrings] = None
@ -405,89 +359,86 @@ class Device(BaseModel):
options: Optional[ListOrDict] = None
class Devices(RootModel[List[Device]]):
root: List[Device]
class Devices(YamlModel):
__root__: List[Device]
class Network(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Network(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
driver: Optional[str] = None
driver_opts: Optional[Dict[str, Union[str, float]]] = None
driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722
ipam: Optional[Ipam] = None
external: Optional[ExternalVolumeNetwork] = None
external: Optional[ExternalNetwork] = None
internal: Optional[bool] = None
enable_ipv6: Optional[bool] = None
attachable: Optional[bool] = None
labels: Optional[ListOrDict] = None
class Volume(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Volume(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
driver: Optional[str] = None
driver_opts: Optional[Dict[str, Union[str, float]]] = None
external: Optional[ExternalVolumeNetwork] = None
driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722
external: Optional[ExternalVolume] = None
labels: Optional[ListOrDict] = None
class Secret(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Secret(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
environment: Optional[str] = None
file: Optional[str] = None
external: Optional[ExternalConfig] = None
external: Optional[ExternalSecret] = None
labels: Optional[ListOrDict] = None
driver: Optional[str] = None
driver_opts: Optional[Dict[str, Union[str, float]]] = None
driver_opts: Optional[Dict[constr(regex=r"^.+$"), Union[str, float]]] = None # type: ignore # noqa: F722
template_driver: Optional[str] = None
class Config(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Config(YamlModel):
class Config:
extra = Extra.forbid
name: Optional[str] = None
content: Optional[str] = None
environment: Optional[str] = None
file: Optional[str] = None
external: Optional[ExternalConfig] = None
labels: Optional[ListOrDict] = None
template_driver: Optional[str] = None
class StringOrList(RootModel[Union[str, ListOfStrings]]):
root: Union[str, ListOfStrings]
class StringOrList(YamlModel):
__root__: Union[str, ListOfStrings]
class Reservations(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Reservations(YamlModel):
class Config:
extra = Extra.forbid
cpus: Optional[Union[float, str]] = None
memory: Optional[str] = None
generic_resources: Optional[GenericResources] = None
devices: Optional[Devices] = None
class Resources(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Resources(YamlModel):
class Config:
extra = Extra.forbid
limits: Optional[Limits] = None
reservations: Optional[Reservations] = None
class Deployment(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class Deployment(YamlModel):
class Config:
extra = Extra.forbid
mode: Optional[str] = None
endpoint_mode: Optional[str] = None
replicas: Optional[int] = None
@ -499,38 +450,21 @@ class Deployment(BaseModel):
placement: Optional[Placement] = None
class Include1(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
path: Optional[StringOrList] = None
env_file: Optional[StringOrList] = None
project_directory: Optional[str] = None
class Service(YamlModel):
class Config:
extra = Extra.forbid
class Include(RootModel[Union[str, Include1]]):
root: Union[str, Include1]
class Service(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
develop: Optional[Development] = None
deploy: Optional[Deployment] = None
annotations: Optional[ListOrDict] = None
attach: Optional[bool] = None
build: Optional[Union[str, Build]] = None
build: Optional[Union[str, BuildItem]] = None
blkio_config: Optional[BlkioConfig] = None
cap_add: Optional[List[str]] = None
cap_drop: Optional[List[str]] = None
cgroup: Optional[Cgroup] = None
cap_add: Optional[List[str]] = Field(None, unique_items=True)
cap_drop: Optional[List[str]] = Field(None, unique_items=True)
cgroup_parent: Optional[str] = None
command: Optional[Command] = None
command: Optional[Union[str, List[str]]] = None
configs: Optional[ServiceConfigOrSecret] = None
container_name: Optional[str] = None
cpu_count: Optional[int] = Field(None, ge=0)
cpu_percent: Optional[int] = Field(None, ge=0, le=100)
cpu_count: Optional[conint(ge=0)] = None # type: ignore
cpu_percent: Optional[conint(ge=0, le=100)] = None # type: ignore
cpu_shares: Optional[Union[float, str]] = None
cpu_quota: Optional[Union[float, str]] = None
cpu_period: Optional[Union[float, str]] = None
@ -539,21 +473,21 @@ class Service(BaseModel):
cpus: Optional[Union[float, str]] = None
cpuset: Optional[str] = None
credential_spec: Optional[CredentialSpec] = None
depends_on: Optional[Union[ListOfStrings, Dict[str, DependsOn]]] = None
depends_on: Optional[Union[ListOfStrings, Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), DependsOn]]] = None # type: ignore # noqa: F722, E501
device_cgroup_rules: Optional[ListOfStrings] = None
devices: Optional[List[str]] = None
devices: Optional[List[str]] = Field(None, unique_items=True)
dns: Optional[StringOrList] = None
dns_opt: Optional[List[str]] = None
dns_opt: Optional[List[str]] = Field(None, unique_items=True)
dns_search: Optional[StringOrList] = None
domainname: Optional[str] = None
entrypoint: Optional[Command] = None
env_file: Optional[EnvFile] = None
entrypoint: Optional[Union[str, List[str]]] = None
env_file: Optional[StringOrList] = None
environment: Optional[ListOrDict] = None
expose: Optional[List[Union[str, float]]] = None
extends: Optional[Union[str, Extends]] = None
external_links: Optional[List[str]] = None
expose: Optional[List[Union[str, float]]] = Field(None, unique_items=True)
extends: Optional[Union[str, Extend]] = None
external_links: Optional[List[str]] = Field(None, unique_items=True)
extra_hosts: Optional[ListOrDict] = None
group_add: Optional[List[Union[str, float]]] = None
group_add: Optional[List[Union[str, float]]] = Field(None, unique_items=True)
healthcheck: Optional[Healthcheck] = None
hostname: Optional[str] = None
image: Optional[str] = None
@ -561,7 +495,7 @@ class Service(BaseModel):
ipc: Optional[str] = None
isolation: Optional[str] = None
labels: Optional[ListOrDict] = None
links: Optional[List[str]] = None
links: Optional[List[str]] = Field(None, unique_items=True)
logging: Optional[Logging] = None
mac_address: Optional[str] = None
mem_limit: Optional[Union[float, str]] = None
@ -569,13 +503,13 @@ class Service(BaseModel):
mem_swappiness: Optional[int] = None
memswap_limit: Optional[Union[float, str]] = None
network_mode: Optional[str] = None
networks: Optional[Union[ListOfStrings, Dict[str, Optional[Networks]]]] = None
networks: Optional[Union[ListOfStrings, Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[ServiceNetwork]]]] = None # type: ignore # noqa: F722, E501
oom_kill_disable: Optional[bool] = None
oom_score_adj: Optional[int] = Field(None, ge=-1000, le=1000)
pid: Optional[str] = None
oom_score_adj: Optional[conint(ge=-1000, le=1000)] = None # type: ignore
pid: Optional[Optional[str]] = None
pids_limit: Optional[Union[float, str]] = None
platform: Optional[str] = None
ports: Optional[List[Union[float, str, Ports]]] = None
ports: Optional[List[Union[float, str, Port]]] = Field(None, unique_items=True)
privileged: Optional[bool] = None
profiles: Optional[ListOfStrings] = None
pull_policy: Optional[PullPolicy] = None
@ -583,7 +517,7 @@ class Service(BaseModel):
restart: Optional[str] = None
runtime: Optional[str] = None
scale: Optional[int] = None
security_opt: Optional[List[str]] = None
security_opt: Optional[List[str]] = Field(None, unique_items=True)
shm_size: Optional[Union[float, str]] = None
secrets: Optional[ServiceConfigOrSecret] = None
sysctls: Optional[ListOrDict] = None
@ -593,28 +527,25 @@ class Service(BaseModel):
storage_opt: Optional[Dict[str, Any]] = None
tmpfs: Optional[StringOrList] = None
tty: Optional[bool] = None
ulimits: Optional[Ulimits] = None
ulimits: Optional[Dict[constr(regex=r"^[a-z]+$"), Union[int, Ulimit]]] = None # type: ignore # noqa: F722
user: Optional[str] = None
uts: Optional[str] = None
userns_mode: Optional[str] = None
volumes: Optional[List[Union[str, Volumes]]] = None
volumes_from: Optional[List[str]] = None
volumes: Optional[List[Union[str, ServiceVolume]]] = Field(None, unique_items=True)
volumes_from: Optional[List[str]] = Field(None, unique_items=True)
working_dir: Optional[str] = None
class ComposeSpecification(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
class ComposeSpecification(YamlModel):
class Config:
extra = Extra.forbid
version: Optional[str] = Field(None, description="declared for backward compatibility, ignored.")
name: Optional[str] = Field(
None,
description="define the Compose project name, until user defines one explicitly.",
pattern="^[a-z0-9][a-z0-9_-]*$",
)
include: Optional[List[Include]] = Field(None, description="compose sub-projects to be included.")
services: Optional[Dict[str, Service]] = None
networks: Optional[Dict[str, Optional[Network]]] = None
volumes: Optional[Dict[str, Optional[Volume]]] = None
secrets: Optional[Dict[str, Secret]] = None
configs: Optional[Dict[str, Config]] = None
services: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Service]] = None # type: ignore # noqa: F722
networks: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[Network]]] = None # type: ignore # noqa: F722
volumes: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Optional[Volume]]] = None # type: ignore # noqa: F722
secrets: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Secret]] = None # type: ignore # noqa: F722
configs: Optional[Dict[constr(regex=r"^[a-zA-Z0-9._-]+$"), Config]] = None # type: ignore # noqa: F722

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -6,19 +6,10 @@ 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:
@ -42,23 +33,11 @@ services:
extends:
service: frontend
ports:
- name: web-secured
target: 443
host_ip: 127.0.0.1
published: "8083-9000"
protocol: tcp
app_protocol: wbsock
mode : host
- "8000:5010"
links:
- "db:database"
cgroup_parent: awesome-parent
db:
image: postgres
profiles:
- foo
devices:
- "/dev/ttyUSB2:/dev/ttyUSB3"
- "/dev/sda:/dev/xvda:rwm"
networks:
front-tier:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -38,7 +38,8 @@
<!-- 0.0.0.06379&#45;&gt;redis -->
<g id="edge2" class="edge">
<title>0.0.0.06379&#45;&gt;redis</title>
<path fill="none" stroke="black" d="M96.9,-194.6C120.47,-174.43 149.35,-149.71 170.41,-131.68"/>
<path fill="none" stroke="black" d="M104.53,-188.06C126.48,-169.27 151.57,-147.8 170.41,-131.68"/>
<polygon fill="black" stroke="black" points="102.22,-185.44 96.9,-194.6 106.77,-190.75 102.22,-185.44"/>
<polygon fill="black" stroke="black" points="172.75,-134.28 178.07,-125.12 168.2,-128.96 172.75,-134.28"/>
<text text-anchor="middle" x="171.25" y="-146.8" font-family="Times New Roman,serif" font-size="14.00">6379</text>
</g>
@ -66,8 +67,7 @@
<!-- db&#45;data -->
<g id="node6" class="node">
<title>db&#45;data</title>
<path fill="none" stroke="black" d="M700.75,-32.73C700.75,-34.53 687.52,-36 671.25,-36 654.97,-36 641.75,-34.53 641.75,-32.73 641.75,-32.73 641.75,-3.27 641.75,-3.27 641.75,-1.47 654.97,0 671.25,0 687.52,0 700.75,-1.47 700.75,-3.27 700.75,-3.27 700.75,-32.73 700.75,-32.73"/>
<path fill="none" stroke="black" d="M700.75,-32.73C700.75,-30.92 687.52,-29.45 671.25,-29.45 654.97,-29.45 641.75,-30.92 641.75,-32.73"/>
<polygon fill="none" stroke="black" points="700.75,-36 697.75,-40 676.75,-40 673.75,-36 641.75,-36 641.75,0 700.75,0 700.75,-36"/>
<text text-anchor="middle" x="671.25" y="-14.3" font-family="Times New Roman,serif" font-size="14.00">db&#45;data</text>
</g>
<!-- db&#45;&gt;db&#45;data -->
@ -108,7 +108,8 @@
<!-- 0.0.0.05000&#45;&gt;vote -->
<g id="edge6" class="edge">
<title>0.0.0.05000&#45;&gt;vote</title>
<path fill="none" stroke="black" d="M271.25,-337.29C271.25,-311.63 271.25,-282.06 271.25,-260.85"/>
<path fill="none" stroke="black" d="M271.25,-327.24C271.25,-304.1 271.25,-279.18 271.25,-260.67"/>
<polygon fill="black" stroke="black" points="267.75,-327.29 271.25,-337.29 274.75,-327.29 267.75,-327.29"/>
<polygon fill="black" stroke="black" points="274.75,-260.58 271.25,-250.58 267.75,-260.58 274.75,-260.58"/>
<text text-anchor="middle" x="278.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">80</text>
</g>
@ -142,7 +143,8 @@
<!-- 0.0.0.05001&#45;&gt;result -->
<g id="edge9" class="edge">
<title>0.0.0.05001&#45;&gt;result</title>
<path fill="none" stroke="black" d="M614.25,-337.29C614.25,-311.63 614.25,-282.06 614.25,-260.85"/>
<path fill="none" stroke="black" d="M614.25,-327.24C614.25,-304.1 614.25,-279.18 614.25,-260.67"/>
<polygon fill="black" stroke="black" points="610.75,-327.29 614.25,-337.29 617.75,-327.29 610.75,-327.29"/>
<polygon fill="black" stroke="black" points="617.75,-260.58 614.25,-250.58 610.75,-260.58 617.75,-260.58"/>
<text text-anchor="middle" x="621.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">80</text>
</g>
@ -179,8 +181,7 @@
<!-- /var/run/docker.sock -->
<g id="node13" class="node">
<title>/var/run/docker.sock</title>
<path fill="none" stroke="black" d="M926.75,-120.73C926.75,-122.53 896.94,-124 860.25,-124 823.55,-124 793.75,-122.53 793.75,-120.73 793.75,-120.73 793.75,-91.27 793.75,-91.27 793.75,-89.47 823.55,-88 860.25,-88 896.94,-88 926.75,-89.47 926.75,-91.27 926.75,-91.27 926.75,-120.73 926.75,-120.73"/>
<path fill="none" stroke="black" d="M926.75,-120.73C926.75,-118.92 896.94,-117.45 860.25,-117.45 823.55,-117.45 793.75,-118.92 793.75,-120.73"/>
<polygon fill="none" stroke="black" points="926.75,-124 923.75,-128 902.75,-128 899.75,-124 793.75,-124 793.75,-88 926.75,-88 926.75,-124"/>
<text text-anchor="middle" x="860.25" y="-102.3" font-family="Times New Roman,serif" font-size="14.00">/var/run/docker.sock</text>
</g>
<!-- visualizer&#45;&gt;/var/run/docker.sock -->
@ -200,7 +201,8 @@
<!-- 0.0.0.08080&#45;&gt;visualizer -->
<g id="edge14" class="edge">
<title>0.0.0.08080&#45;&gt;visualizer</title>
<path fill="none" stroke="black" d="M860.25,-337.29C860.25,-311.63 860.25,-282.06 860.25,-260.85"/>
<path fill="none" stroke="black" d="M860.25,-327.24C860.25,-304.1 860.25,-279.18 860.25,-260.67"/>
<polygon fill="black" stroke="black" points="856.75,-327.29 860.25,-337.29 863.75,-327.29 856.75,-327.29"/>
<polygon fill="black" stroke="black" points="863.75,-260.58 860.25,-250.58 856.75,-260.58 863.75,-260.58"/>
<text text-anchor="middle" x="874.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">8080</text>
</g>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

1295
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "compose-viz"
version = "0.3.2"
version = "0.2.1"
description = "A compose file visualization tool that supports compose-spec and allows you to gernerate graph in several formats."
authors = ["Xyphuz Wu <xyphuzwu@gmail.com>"]
readme = "README.md"
@ -14,15 +14,16 @@ include = [
[tool.poetry.dependencies]
python = "^3.9"
typer = "^0.4.1"
PyYAML = "^6.0"
graphviz = "^0.20"
pydantic-yaml = "^1.3.0"
"ruamel.yaml" = "^0.17.21"
pydantic-yaml = "^0.6.3"
[tool.poetry.group.dev.dependencies]
pytest = "^8.1.2"
pre-commit = "^3.7.0"
coverage = "^7.5.0"
pytest-cov = "^5.0.0"
datamodel-code-generator = "^0.25.6"
[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pre-commit = "^2.19.0"
coverage = "^6.3.3"
pytest-cov = "^3.0.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

View file

@ -12,21 +12,14 @@ 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",
"examples/voting-app/docker-compose.yml",
],
)
def test_cli(test_file_path: str) -> None:

View file

@ -1,22 +0,0 @@
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

View file

@ -1,7 +1,6 @@
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
@ -172,14 +171,6 @@ from compose_viz.parser import Parser
container_port="6060",
protocol=Protocol.udp,
),
Port(
host_port="0.0.0.0:7777",
container_port="7777",
),
Port(
host_port="${BIND_IP:-127.0.0.1}:8080",
container_port="8080",
),
Port(
host_port="127.0.0.1:8080",
container_port="80",
@ -250,123 +241,6 @@ 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:
@ -401,16 +275,3 @@ 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

View file

@ -1,32 +0,0 @@
import os
from typer.testing import CliRunner
from compose_viz import cli
runner = CliRunner()
def test_root_service() -> None:
input_path = "examples/voting-app/docker-compose.yml"
output_filename = "compose-viz-test"
default_format = "png"
result = runner.invoke(cli.app, ["-r", "vote", "-o", output_filename, input_path])
assert result.exit_code == 0
assert f"Successfully parsed {input_path}\n" in result.stdout
assert os.path.exists(f"{output_filename}.{default_format}")
os.remove(f"{output_filename}.{default_format}")
def test_root_service_key_error() -> None:
input_path = "examples/voting-app/docker-compose.yml"
output_filename = "compose-viz-test"
default_format = "png"
result = runner.invoke(cli.app, ["-r", "not_exist_service", "-o", output_filename, input_path])
assert result.exit_code == 1
assert result.exception is not None
assert result.exception.args[0] == f"Service 'not_exist_service' not found in given compose file: '{input_path}'"
assert not os.path.exists(f"{output_filename}.{default_format}")

View file

@ -1,6 +0,0 @@
version: "3.9"
services:
frontend:
image: awesome/frontend
cgroup_parent: "system"

View file

@ -1,6 +0,0 @@
version: "3.9"
services:
frontend:
image: awesome/frontend
container_name: "myfrontend"

View file

@ -1,12 +0,0 @@
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"

View file

@ -1,15 +0,0 @@
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

View file

@ -1,12 +0,0 @@
version: "3.9"
services:
frontend:
image: awesome/frontend
expose:
- "27118"
backend:
image: awesome/backend
expose:
- "27017"
- "27018"

View file

@ -11,8 +11,6 @@ services:
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "6060:6060/udp"
- ":7777"
- "${BIND_IP:-127.0.0.1}:8080:8080"
- target: 80
host_ip: 127.0.0.1
published: 8080

View file

@ -1,14 +0,0 @@
version: "3.9"
services:
frontend:
image: awesome/frontend
profiles: ["frontend"]
phpmyadmin:
image: phpmyadmin
profiles:
- debug
db:
image: awesome/db
profiles:
- db
- sql

View file

@ -1,54 +0,0 @@
import re
def revise_naming_convention():
name_mapping = {
"EnvFile1": "EnvFilePath",
"Volume1": "AdditionalVolumeOption",
"External": "ExternalVolumeNetwork",
"External2": "ExternalConfig",
}
spec_content: str
with open("./compose_viz/spec/compose_spec.py", "r+") as spec_file:
spec_content: str = spec_file.read()
for origin_name, new_name in name_mapping.items():
spec_content = re.sub(rf"\b{origin_name}\b", new_name, spec_content)
spec_file.seek(0)
spec_file.write(spec_content)
print("Revised naming convention successfully!")
def update_version_number():
new_version_number: str
with open("./compose_viz/__init__.py", "r+") as init_file:
init_content: str = init_file.read()
version_number = init_content.split(" ")[-1].replace('"', "").strip()
major, minor, patch = version_number.split(".")
new_version_number = f"{major}.{minor}.{int(patch) + 1}"
init_file.seek(0)
init_file.write(
f"""__app_name__ = "compose_viz"
__version__ = "{new_version_number}"
"""
)
with open("./pyproject.toml", "r+") as pyproject_file:
pyproject_content: str = pyproject_file.read()
pyproject_content = pyproject_content.replace(version_number, new_version_number)
pyproject_file.seek(0)
pyproject_file.write(pyproject_content)
print(f"Version number updated to {new_version_number} successfully!")
if __name__ == "__main__":
revise_naming_convention()
update_version_number()