Compare commits

...

88 commits
v0.2.0 ... main

Author SHA1 Message Date
Xyphuz
b17b23cbf8
Merge pull request #56 from compose-viz/dev
Fix #51 #54
2024-04-28 03:39:05 +08:00
Xyphuz
95c315b82d chore: update naming convention and version number when submodules have been updated 2024-04-28 03:36:27 +08:00
Xyphuz
49e3f14191 ci: move naming script execution from ci to update-submodules 2024-04-28 02:20:14 +08:00
Xyphuz
b8ea7a88bb ci: check updates of compose-spec and file PR 2024-04-28 02:03:41 +08:00
Xyphuz
1407a77f07 fix: should follow conventional commit when updating new compose-spec 2024-04-28 01:36:28 +08:00
Cheng Chih Yuan
36d18a6300
Fix #51
* fix: correcting regex for formatting string #51 
* ci: add cron.yml

---------

Co-authored-by: Xyphuz <38447974+wst24365888@users.noreply.github.com>
2024-04-28 01:34:03 +08:00
Xyphuz
0ffcc16b61 chore: update versiono tag 2024-04-28 00:45:24 +08:00
Xyphuz
c3423f8aac feat: add legend #54 2024-04-28 00:42:34 +08:00
Xyphuz
f5d45fca30 chore: update poetry version in github actions 2024-04-27 17:37:12 +08:00
Xyphuz
7b0466885a fix: wrong typing in tests 2024-04-27 17:29:14 +08:00
Xyphuz
aa4d3b70ed chore: update dep versions 2024-04-27 16:55:24 +08:00
Xyphuz
1c14fde444
Merge pull request #50 from compose-viz/dev
chore: update versiono tag
2023-05-06 22:48:05 +08:00
Xyphuz
239ea20a68 chore: update versiono tag 2023-05-06 22:45:16 +08:00
Xyphuz
75d442acaa
Merge pull request #49 from kevinjone25/main
fix: extension-fields parse error from compose spec #48
2023-05-04 02:15:38 +08:00
Zheng Zhi Yuan
78a7654339 fix: extension-fields parse error from compose spec #48 2023-05-04 02:11:59 +08:00
Zheng Zhi Yuan
567961bd61 build: upgrade isort version to 5.12.0 2023-05-04 01:22:57 +08:00
Zheng Zhi Yuan
e649eac539 fix: extension-fields parse error from compose spec #48 2023-05-04 00:00:19 +08:00
Zheng Zhi Yuan
e902237356 fix: extension-fields parse error from compose spec #48 2023-05-03 16:50:39 +08:00
Xyphuz
9cb6baaa03
Merge pull request #46 from compose-viz/dev
chore: update about link
2023-01-11 01:34:37 +08:00
Xyphuz
7b23141655 chore: update about link 2023-01-11 01:34:00 +08:00
Xyphuz
230645523f
Merge pull request #45 from compose-viz/dev
Update poetry version in release-tagged-version
2023-01-11 01:14:16 +08:00
Xyphuz
9adece48e5 chore: update poetry version in release-tagged-version 2023-01-11 01:13:37 +08:00
Xyphuz
2fa1b035c7
Merge pull request #44 from compose-viz/dev
Update version tag
2023-01-11 01:07:13 +08:00
Xyphuz
e1524460c5 chore: update version tag 2023-01-11 01:06:35 +08:00
Xyphuz
975e39ab56
Merge pull request #43 from compose-viz/dev
Fix service root KeyError
2023-01-11 01:05:18 +08:00
Xyphuz
eb84c5ed05 test: add test for root service 2023-01-11 00:54:06 +08:00
Xyphuz
168d034c05 fix: handle key error during parsing root service 2023-01-10 22:37:51 +08:00
Xyphuz
f2d0ce0d22 chore: update doc of --root-service 2023-01-10 22:31:59 +08:00
Xyphuz
52262faf39
Merge pull request #42 from compose-viz/dev
Fix #38 #41
2023-01-10 22:11:12 +08:00
Xyphuz
3cfe312982 chore: update version tag 2023-01-10 22:08:51 +08:00
Xyphuz
2a815e1fba Merge branch 'main' of https://github.com/compose-viz/compose-viz into dev 2023-01-10 22:07:10 +08:00
Xyphuz
ba8c282cdb
Merge pull request #40 from josef-v/jv/root-service
feat: Add root service feature
2023-01-10 22:06:16 +08:00
Xyphuz
90b6bbcf26 chore: update version of pytest-cov 2023-01-10 21:55:30 +08:00
Xyphuz
6f9f9479d1 chore: update poetry version in ci 2023-01-10 21:47:36 +08:00
Xyphuz
daa7c4dc73 fix: wrong generated data model of compose spec #41 2023-01-10 21:43:49 +08:00
Xyphuz
389c5252c4 chore: update compose spec 2023-01-10 21:33:08 +08:00
Josef Vondrlik
c4b767736a feat: Add root-service option 2023-01-10 14:23:23 +01:00
Xyphuz
e0d3baa522 fix: ports start with colon cannot be parsed #38 2023-01-10 21:08:44 +08:00
Xyphuz
1b516a12e1 chore: update dependencies 2023-01-10 21:00:03 +08:00
Xyphuz
8d409336c9
Merge pull request #37 from compose-viz/dev
fix: warning in ci
2022-06-09 17:33:06 +08:00
Xyphuz
6c2a6929a3 chore: update version number 2022-06-09 17:29:13 +08:00
Xyphuz
f879a3de14 chore: update version of pydantic-yaml 2022-06-09 17:28:44 +08:00
Xyphuz
5851ad1355 chore: update examples 2022-06-09 17:28:18 +08:00
Xyphuz
09e4fbedb0
Merge pull request #36 from compose-viz/dev
chore: update version number
2022-06-09 14:00:36 +08:00
Xyphuz
e79b52d809 chore: update version number 2022-06-09 14:00:07 +08:00
Xyphuz
a9df873278
Merge pull request #35 from compose-viz/dev
chore: add graph of new components
2022-06-09 13:59:41 +08:00
Xyphuz
80b046d1e5 chore: apply missing pre-commit hooks 2022-06-09 13:56:12 +08:00
Xyphuz
80c62ac94d chore: remove unnecessary validation 2022-06-09 13:52:49 +08:00
Xyphuz
d2afcfb243 chore: change profile's and cgroup_parent's arrow direction 2022-06-09 13:47:41 +08:00
Xyphuz
18da6cc728 chore: change volumes_ro type to dashed 2022-06-09 03:08:14 +08:00
wolfyeva
698a9a770b add: device test 2022-06-09 03:06:51 +08:00
Xyphuz
600ca95ba5 test: add test of new components 2022-06-09 00:21:54 +08:00
Xyphuz
84e1eee801 chore: update examples 2022-06-09 00:20:03 +08:00
Xyphuz
b2be560807 chore: add missing type annotations 2022-06-08 23:29:01 +08:00
Chuan Ou Yang
e0ef69febd feat: only make rw mode volume both dir 2022-06-08 23:27:12 +08:00
Chuan Ou Yang
bdb227b1d8 feat: add device graph 2022-06-08 23:25:52 +08:00
Xyphuz
198a8aada0
Merge pull request #34 from compose-viz/dev-testcase
chore: new device format
2022-06-08 22:53:00 +08:00
Xyphuz
cb66f9da60 chore: apply pre-commit hooks 2022-06-08 20:57:28 +08:00
uccuz
35e710f9e4 chore: implement device parse 2022-06-07 00:42:30 +08:00
uccuz
10c36344da chore: create device test case 2022-06-07 00:41:45 +08:00
uccuz
321e687049 feat: create device model 2022-06-07 00:41:15 +08:00
uccuz
edf56997bf feat: add service property 2022-06-07 00:34:18 +08:00
uccuz
382487eca0 chore: add new devices yaml data 2022-06-07 00:21:50 +08:00
Chuan Ou Yang
7a4a050b5c feat: add env_file, expose, porfilem, cgroup graph 2022-06-05 14:17:38 +08:00
Xyphuz
1f2d7ff136
Merge pull request #33 from compose-viz/dev-testcase
feat: new components
2022-06-03 17:29:10 +08:00
Xyphuz
eca68a8190 chore: apply pre-commit hooks 2022-06-03 17:19:43 +08:00
uccuz
747dd06be1 feat: implement new test case 2022-06-03 16:57:58 +08:00
uccuz
50589db102 chore: create env, expose, profiles test case 2022-06-03 16:57:40 +08:00
uccuz
f6615ca1b9 feat: add service new member 2022-06-03 16:57:06 +08:00
uccuz
1076459e7c chore: add devices, env_file, expose and profiles 2022-06-03 16:56:36 +08:00
uccuz
603f552551 chore: add cgroup and container_name test case 2022-06-03 16:55:17 +08:00
uccuz
5892237eae chore: add cgroup_parent and container_name yaml 2022-06-03 16:52:12 +08:00
Xyphuz
6271ef895b
Merge pull request #32 from compose-viz/dev
feat: release Dockerfile
2022-05-29 16:43:12 +08:00
Xyphuz
eb4cf44583 chore: update version number 2022-05-29 16:38:51 +08:00
Xyphuz
3eb3e8a8fe chore: add Docker instructions in README.md 2022-05-29 16:38:13 +08:00
Xyphuz
64938cb070
Merge pull request #31 from compose-viz/dev
chore: add Dockerfile in release
2022-05-29 16:19:29 +08:00
Xyphuz
cec8d6d579 chore: add Dockerfile in release 2022-05-29 16:19:00 +08:00
Xyphuz
b469cbc50b
Merge pull request #30 from compose-viz/dev
fix: wrong secret name
2022-05-29 16:11:43 +08:00
Xyphuz
a87100e0dc fix: wrong secret name 2022-05-29 16:11:17 +08:00
Xyphuz
c7b0aa4afa
Merge pull request #29 from compose-viz/dev
feat: docker related
2022-05-29 16:07:14 +08:00
Xyphuz
60ff997f2c chore: update version number 2022-05-29 16:02:59 +08:00
Xyphuz
19a7888e99
Merge pull request #28 from compose-viz/dev-docker
feat: docker related
2022-05-29 16:02:18 +08:00
Xyphuz
a12cb6dd17 ci: add docker image related actions 2022-05-29 15:59:05 +08:00
Xyphuz
62ba00adcb feat: add Dockerfile 2022-05-29 03:04:24 +08:00
Xyphuz
ed0e08e195
Merge pull request #26 from compose-viz/dev
chore: update README.md
2022-05-28 03:02:44 +08:00
Xyphuz
0276ec5737 chore: update README.md 2022-05-28 03:02:25 +08:00
Xyphuz
08e719ab85
Merge pull request #25 from compose-viz/dev
chore: update README.md
2022-05-28 02:37:13 +08:00
Xyphuz
810cc12aa1 chore: update README.md 2022-05-28 02:36:53 +08:00
37 changed files with 2502 additions and 2210 deletions

View file

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

View file

@ -0,0 +1,39 @@
name: Release Docker Image
on:
push:
tags:
- "v*"
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract Tag Version
id: tag_version
run: |
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
-
name: Build and push
uses: docker/build-push-action@v3
with:
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/compose-viz:latest
${{ secrets.DOCKERHUB_USERNAME }}/compose-viz:${{ steps.tag_version.outputs.VERSION }}
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/compose-viz:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/compose-viz:buildcache,mode=max

View file

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

79
.github/workflows/update-submodules.yml vendored Normal file
View file

@ -0,0 +1,79 @@
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 Normal file
View file

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

View file

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

35
Dockerfile Normal file
View file

@ -0,0 +1,35 @@
FROM python:alpine3.16 as builder
COPY ./ /compose-viz/
RUN \
apk update && \
pip install --upgrade pip
RUN \
apk add binutils alpine-sdk libffi-dev
RUN \
pip install poetry && \
pip install pyinstaller
RUN \
cd /compose-viz && \
poetry config virtualenvs.create false && \
poetry install --no-root
RUN \
cd /compose-viz && \
pyinstaller --onefile --name cpv ./compose_viz/__main__.py
FROM alpine:3.16 as release
COPY --from=builder /compose-viz/dist/cpv /usr/local/bin/cpv
RUN \
apk add --no-cache graphviz ttf-droid
VOLUME [ "/in" ]
WORKDIR "/in"
ENTRYPOINT [ "cpv" ]

View file

@ -56,7 +56,7 @@
## About The Project ## About The Project
`compose-viz` is a compose file visualization tool that supports [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) and allows you to gernerate graph in several formats. `compose-viz` is a compose file visualization tool that follows [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) and allows you to gernerate graph in several formats.
If you are looking for a compose file vizualization tool, and you are using one of the [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) implementations (e.g. [docker-compose](https://github.com/docker/compose)/[podman-compose](https://github.com/containers/podman-compose)), then `compose-viz` is a great choice for you. If you are looking for a compose file vizualization tool, and you are using one of the [compose-spec](https://github.com/compose-spec/compose-spec/blob/master/spec.md) implementations (e.g. [docker-compose](https://github.com/docker/compose)/[podman-compose](https://github.com/containers/podman-compose)), then `compose-viz` is a great choice for you.
@ -76,19 +76,33 @@ You need to install [Graphviz](https://graphviz.org/download/) to generate graph
#### Using `pip` #### Using `pip`
`pip install compose-viz` ```
pip install compose-viz
```
#### Using `.whl` #### Using `.whl`
See [releases](https://github.com/compose-viz/compose-viz/releases). 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 ### Example
This example yml is from [docker compose beginner tutorial](https://github.com/docker/labs/blob/master/beginner/chapters/votingapp.md). This example yml is from [docker compose beginner tutorial](https://github.com/docker/labs/blob/master/beginner/chapters/votingapp.md).
```bash ```bash
cd examples/voting-app/ cd examples/voting-app/
# using python script
cpv -m svg docker-compose.yml 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: And this is what the result looks like:
@ -103,12 +117,14 @@ Check out the result [here](https://github.com/compose-viz/compose-viz/blob/main
### Options ### Options
| Option | Description | | Option | Description |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-o, --output-filename` | Output filename for the generated visualization file. [default: compose-viz] | | `-o, --output-filename 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/viz_formats.py). [default: png] | | `-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] |
| `-v, --version` | Show the version of compose-viz. | | `-r, --root-service SERVICE_NAME` | Root of the service tree (convenient for large compose yamls) |
| `--help` | Show help and exit. | | `-l, --legend` | Include a legend in the visualization. |
| `-v, --version` | Show the version of compose-viz. |
| `--help` | Show help and exit. |
<p align="right">(<a href="#top">back to top</a>)</p> <p align="right">(<a href="#top">back to top</a>)</p>
@ -162,7 +178,7 @@ for more information.
- HSING-HAN, WU (Xyphuz) - HSING-HAN, WU (Xyphuz)
- Mail me: xyphuzwu@gmail.com - Mail me: xyphuzwu@gmail.com
- About me: <https://about.xyphuz.com> - About me: <https://www.xyphuz.com>
- GitHub: <https://github.com/wst24365888> - GitHub: <https://github.com/wst24365888>
### Project Link ### Project Link

1
compose-spec Submodule

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

View file

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

View file

@ -36,6 +36,18 @@ def compose_viz(
"-m", "-m",
help="Output format for the generated visualization file.", 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( _: Optional[bool] = typer.Option(
None, None,
"--version", "--version",
@ -46,12 +58,12 @@ def compose_viz(
), ),
) -> None: ) -> None:
parser = Parser() parser = Parser()
compose = parser.parse(input_path) compose = parser.parse(input_path, root_service=root_service)
if compose: if compose:
typer.echo(f"Successfully parsed {input_path}") typer.echo(f"Successfully parsed {input_path}")
Graph(compose, output_filename).render(format) Graph(compose, output_filename, include_legend).render(format)
raise typer.Exit() raise typer.Exit()

View file

@ -3,15 +3,16 @@ from typing import Optional
import graphviz import graphviz
from compose_viz.models.compose import Compose from compose_viz.models.compose import Compose
from compose_viz.models.port import AppProtocol, Protocol
def apply_vertex_style(type) -> dict: def apply_vertex_style(type: str) -> dict:
style = { style = {
"service": { "service": {
"shape": "component", "shape": "component",
}, },
"volume": { "volume": {
"shape": "folder", "shape": "cylinder",
}, },
"network": { "network": {
"shape": "pentagon", "shape": "pentagon",
@ -19,24 +20,39 @@ def apply_vertex_style(type) -> dict:
"port": { "port": {
"shape": "circle", "shape": "circle",
}, },
"env_file": {
"shape": "tab",
},
"porfile": {
"shape": "invhouse",
},
"cgroup": {
"shape": "diamond",
},
"device": {
"shape": "box3d",
},
} }
return style[type] return style[type]
def apply_edge_style(type) -> dict: def apply_edge_style(type: str) -> dict:
style = { style = {
"ports": { "exposes": {
"style": "solid", "style": "solid",
"dir": "both", "dir": "both",
}, },
"links": { "links": {
"style": "solid", "style": "solid",
}, },
"volumes": { "volumes_rw": {
"style": "dashed", "style": "dashed",
"dir": "both", "dir": "both",
}, },
"volumes_ro": {
"style": "dashed",
},
"depends_on": { "depends_on": {
"style": "dotted", "style": "dotted",
}, },
@ -45,18 +61,67 @@ def apply_edge_style(type) -> dict:
"arrowhead": "inv", "arrowhead": "inv",
"arrowtail": "dot", "arrowtail": "dot",
}, },
"env_file": {
"style": "solid",
},
} }
return style[type] return style[type]
class Graph: class Graph:
def __init__(self, compose: Compose, filename: str) -> None: def __init__(self, compose: Compose, filename: str, include_legend: bool) -> None:
self.dot = graphviz.Digraph() self.dot = graphviz.Digraph()
self.dot.attr("graph", background="#ffffff", pad="0.5", ratio="fill") self.dot.attr("graph", background="#ffffff", pad="0.5", ratio="fill")
self.compose = compose self.compose = compose
self.filename = filename 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: def validate_name(self, name: str) -> str:
# graphviz does not allow ':' in node name # graphviz does not allow ':' in node name
transTable = name.maketrans({":": ""}) transTable = name.maketrans({":": ""})
@ -71,19 +136,45 @@ class Graph:
def render(self, format: str, cleanup: bool = True) -> None: def render(self, format: str, cleanup: bool = True) -> None:
for service in self.compose.services: for service in self.compose.services:
if service.image is not None: 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: if service.extends is not None:
self.add_vertex(service.name, "service", lable=f"{service.name}\n") self.add_vertex(service.name, "service", lable=f"{service.name}\n")
self.add_edge(service.extends.service_name, service.name, "extends") 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: for network in service.networks:
self.add_vertex(network, "network", lable=f"net:{network}") self.add_vertex(network, "network", lable=f"net:{network}")
self.add_edge(service.name, network, "links") self.add_edge(service.name, network, "links")
for volume in service.volumes: for volume in service.volumes:
self.add_vertex(volume.source, "volume") 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: for port in service.ports:
self.add_vertex(port.host_port, "port", lable=port.host_port) 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
+ (("/" + 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")
for link in service.links: for link in service.links:
if ":" in link: if ":" in link:
service_name, alias = link.split(":", 1) service_name, alias = link.split(":", 1)
@ -92,5 +183,13 @@ class Graph:
self.add_edge(link, service.name, "links") self.add_edge(link, service.name, "links")
for depends_on in service.depends_on: for depends_on in service.depends_on:
self.add_edge(service.name, depends_on, "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) self.dot.render(outfile=f"{self.filename}.{format}", format=format, cleanup=cleanup)

View file

@ -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

View file

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

View file

@ -1,5 +1,6 @@
from typing import List, Optional from typing import List, Optional
from compose_viz.models.device import Device
from compose_viz.models.extends import Extends from compose_viz.models.extends import Extends
from compose_viz.models.port import Port from compose_viz.models.port import Port
from compose_viz.models.volume import Volume from compose_viz.models.volume import Volume
@ -16,6 +17,12 @@ class Service:
depends_on: List[str] = [], depends_on: List[str] = [],
links: List[str] = [], links: List[str] = [],
extends: Optional[Extends] = None, 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: ) -> None:
self._name = name self._name = name
self._image = image self._image = image
@ -25,6 +32,12 @@ class Service:
self._depends_on = depends_on self._depends_on = depends_on
self._links = links self._links = links
self._extends = extends 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 @property
def name(self): def name(self):
@ -57,3 +70,27 @@ class Service:
@property @property
def extends(self): def extends(self):
return self._extends 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,12 +1,13 @@
import re import re
from typing import List, Optional from typing import Any, Dict, List, Optional, Union
from pydantic import ValidationError from pydantic_yaml import parse_yaml_raw_as
import compose_viz.spec.compose_spec as spec import compose_viz.spec.compose_spec as spec
from compose_viz.models.compose import Compose, Service 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.extends import Extends
from compose_viz.models.port import Port, Protocol from compose_viz.models.port import AppProtocol, Port, Protocol
from compose_viz.models.volume import Volume, VolumeType from compose_viz.models.volume import Volume, VolumeType
@ -14,26 +15,57 @@ class Parser:
def __init__(self): def __init__(self):
pass pass
def parse(self, file_path: str) -> Compose: @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:
compose_data: spec.ComposeSpecification compose_data: spec.ComposeSpecification
try: try:
compose_data = spec.ComposeSpecification.parse_file(file_path) with open(file_path, "r") as file:
except ValidationError as e: file_content = file.read()
compose_data = parse_yaml_raw_as(spec.ComposeSpecification, file_content)
except Exception as e:
raise RuntimeError(f"Error parsing file '{file_path}': {e}") raise RuntimeError(f"Error parsing file '{file_path}': {e}")
services: List[Service] = [] services: List[Service] = []
assert compose_data.services is not None, "No services found, aborting." 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(): for service_name, service_data in compose_data.services.items():
service_name = str(service_name) service_name = str(service_name)
if root_service and service_name not in root_dependencies:
continue
service_image: Optional[str] = None service_image: Optional[str] = None
if service_data.build is not None: if service_data.build is not None:
if type(service_data.build) is str: if type(service_data.build) is str:
service_image = f"build from '{service_data.build}'" service_image = f"build from '{service_data.build}'"
elif type(service_data.build) is spec.BuildItem: elif type(service_data.build) is spec.Build:
if service_data.build.context is not None and service_data.build.dockerfile is not None: if service_data.build.context is not None and service_data.build.dockerfile is not None:
service_image = ( service_image = (
f"build from '{service_data.build.context}' using '{service_data.build.dockerfile}'" f"build from '{service_data.build.context}' using '{service_data.build.dockerfile}'"
@ -49,7 +81,7 @@ class Parser:
service_networks: List[str] = [] service_networks: List[str] = []
if service_data.networks is not None: if service_data.networks is not None:
if type(service_data.networks) is spec.ListOfStrings: 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: elif type(service_data.networks) is dict:
service_networks = list(service_data.networks.keys()) service_networks = list(service_data.networks.keys())
@ -57,7 +89,7 @@ class Parser:
if service_data.extends is not None: if service_data.extends is not None:
# https://github.com/compose-spec/compose-spec/blob/master/spec.md#extends # https://github.com/compose-spec/compose-spec/blob/master/spec.md#extends
# The value of the extends key MUST be a dictionary. # The value of the extends key MUST be a dictionary.
assert type(service_data.extends) is spec.Extend assert type(service_data.extends) is spec.Extends
service_extends = Extends( service_extends = Extends(
service_name=service_data.extends.service, from_file=service_data.extends.file service_name=service_data.extends.service, from_file=service_data.extends.file
) )
@ -69,12 +101,13 @@ class Parser:
host_port: Optional[str] = None host_port: Optional[str] = None
container_port: Optional[str] = None container_port: Optional[str] = None
protocol: Optional[str] = None protocol: Optional[str] = None
app_protocol: Optional[str] = None
if type(port_data) is float: if type(port_data) is float:
container_port = str(int(port_data)) container_port = str(int(port_data))
host_port = f"0.0.0.0:{container_port}" host_port = f"0.0.0.0:{container_port}"
elif type(port_data) is str: 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) match = re.match(regex, port_data)
if match: if match:
@ -92,20 +125,18 @@ class Parser:
host_port = f"{host_ip}{host_port}" host_port = f"{host_ip}{host_port}"
else: else:
host_port = f"0.0.0.0:{host_port}" host_port = f"0.0.0.0:{host_port}"
elif type(port_data) is spec.Port: elif type(port_data) is spec.Ports:
assert port_data.target is not None, "Invalid port format, aborting." assert port_data.target is not None, "Invalid port format, aborting."
# ruamel.yaml does not parse port as int if type(port_data.published) is str or type(port_data.published) is int:
assert type(port_data.published) is not int host_port = str(port_data.published)
if type(port_data.published) is str:
host_port = port_data.published
if type(port_data.target) is int: if type(port_data.target) is int:
container_port = str(port_data.target) container_port = str(port_data.target)
host_ip = port_data.host_ip host_ip = port_data.host_ip
protocol = port_data.protocol protocol = port_data.protocol
app_protocol = port_data.app_protocol
if container_port is not None and host_port is None: if container_port is not None and host_port is None:
host_port = container_port host_port = container_port
@ -121,21 +152,21 @@ class Parser:
if protocol is None: if protocol is None:
protocol = "any" protocol = "any"
if app_protocol is None:
app_protocol = "na"
service_ports.append( service_ports.append(
Port( Port(
host_port=host_port, host_port=host_port,
container_port=container_port, container_port=container_port,
protocol=Protocol[protocol], protocol=Protocol[protocol],
app_protocol=AppProtocol[app_protocol],
) )
) )
service_depends_on: List[str] = [] service_depends_on: List[str] = []
if service_data.depends_on is not None: if service_data.depends_on is not None:
if type(service_data.depends_on) is spec.ListOfStrings: service_depends_on = Parser._unwrap_depends_on(service_data.depends_on)
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] = [] service_volumes: List[Volume] = []
if service_data.volumes is not None: if service_data.volumes is not None:
@ -154,7 +185,7 @@ class Parser:
access_mode=spilt_data[2], access_mode=spilt_data[2],
) )
) )
elif type(volume_data) is spec.ServiceVolume: elif type(volume_data) is spec.Volumes:
assert volume_data.target is not None, "Invalid volume input, aborting." 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 # https://github.com/compose-spec/compose-spec/blob/master/spec.md#long-syntax-4
@ -176,6 +207,55 @@ class Parser:
if service_data.links is not None: if service_data.links is not None:
service_links = service_data.links 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( services.append(
Service( Service(
name=service_name, name=service_name,
@ -186,6 +266,12 @@ class Parser:
depends_on=service_depends_on, depends_on=service_depends_on,
volumes=service_volumes, volumes=service_volumes,
links=service_links, links=service_links,
cgroup_parent=cgroup_parent,
container_name=container_name,
env_file=env_file,
expose=expose,
profiles=profiles,
devices=devices,
) )
) )

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -6,10 +6,19 @@ services:
- back-tier - back-tier
monitoring: monitoring:
env_file:
- a.env
- b.env
container_name: monitoring-server
image: awesome/monitoring image: awesome/monitoring
networks: networks:
- admin - admin
expose:
- 1234
profiles:
- tools
- foo
cgroup_parent: awesome-parent
backend: backend:
networks: networks:
@ -33,11 +42,23 @@ services:
extends: extends:
service: frontend service: frontend
ports: ports:
- "8000:5010" - name: web-secured
target: 443
host_ip: 127.0.0.1
published: "8083-9000"
protocol: tcp
app_protocol: wbsock
mode : host
links: links:
- "db:database" - "db:database"
cgroup_parent: awesome-parent
db: db:
image: postgres image: postgres
profiles:
- foo
devices:
- "/dev/ttyUSB2:/dev/ttyUSB3"
- "/dev/sda:/dev/xvda:rwm"
networks: networks:
front-tier: front-tier:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View file

@ -38,8 +38,7 @@
<!-- 0.0.0.06379&#45;&gt;redis --> <!-- 0.0.0.06379&#45;&gt;redis -->
<g id="edge2" class="edge"> <g id="edge2" class="edge">
<title>0.0.0.06379&#45;&gt;redis</title> <title>0.0.0.06379&#45;&gt;redis</title>
<path fill="none" stroke="black" d="M104.53,-188.06C126.48,-169.27 151.57,-147.8 170.41,-131.68"/> <path fill="none" stroke="black" d="M96.9,-194.6C120.47,-174.43 149.35,-149.71 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"/> <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> <text text-anchor="middle" x="171.25" y="-146.8" font-family="Times New Roman,serif" font-size="14.00">6379</text>
</g> </g>
@ -67,7 +66,8 @@
<!-- db&#45;data --> <!-- db&#45;data -->
<g id="node6" class="node"> <g id="node6" class="node">
<title>db&#45;data</title> <title>db&#45;data</title>
<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"/> <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"/>
<text text-anchor="middle" x="671.25" y="-14.3" font-family="Times New Roman,serif" font-size="14.00">db&#45;data</text> <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> </g>
<!-- db&#45;&gt;db&#45;data --> <!-- db&#45;&gt;db&#45;data -->
@ -108,8 +108,7 @@
<!-- 0.0.0.05000&#45;&gt;vote --> <!-- 0.0.0.05000&#45;&gt;vote -->
<g id="edge6" class="edge"> <g id="edge6" class="edge">
<title>0.0.0.05000&#45;&gt;vote</title> <title>0.0.0.05000&#45;&gt;vote</title>
<path fill="none" stroke="black" d="M271.25,-327.24C271.25,-304.1 271.25,-279.18 271.25,-260.67"/> <path fill="none" stroke="black" d="M271.25,-337.29C271.25,-311.63 271.25,-282.06 271.25,-260.85"/>
<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"/> <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> <text text-anchor="middle" x="278.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">80</text>
</g> </g>
@ -143,8 +142,7 @@
<!-- 0.0.0.05001&#45;&gt;result --> <!-- 0.0.0.05001&#45;&gt;result -->
<g id="edge9" class="edge"> <g id="edge9" class="edge">
<title>0.0.0.05001&#45;&gt;result</title> <title>0.0.0.05001&#45;&gt;result</title>
<path fill="none" stroke="black" d="M614.25,-327.24C614.25,-304.1 614.25,-279.18 614.25,-260.67"/> <path fill="none" stroke="black" d="M614.25,-337.29C614.25,-311.63 614.25,-282.06 614.25,-260.85"/>
<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"/> <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> <text text-anchor="middle" x="621.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">80</text>
</g> </g>
@ -181,7 +179,8 @@
<!-- /var/run/docker.sock --> <!-- /var/run/docker.sock -->
<g id="node13" class="node"> <g id="node13" class="node">
<title>/var/run/docker.sock</title> <title>/var/run/docker.sock</title>
<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"/> <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"/>
<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> <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> </g>
<!-- visualizer&#45;&gt;/var/run/docker.sock --> <!-- visualizer&#45;&gt;/var/run/docker.sock -->
@ -201,8 +200,7 @@
<!-- 0.0.0.08080&#45;&gt;visualizer --> <!-- 0.0.0.08080&#45;&gt;visualizer -->
<g id="edge14" class="edge"> <g id="edge14" class="edge">
<title>0.0.0.08080&#45;&gt;visualizer</title> <title>0.0.0.08080&#45;&gt;visualizer</title>
<path fill="none" stroke="black" d="M860.25,-327.24C860.25,-304.1 860.25,-279.18 860.25,-260.67"/> <path fill="none" stroke="black" d="M860.25,-337.29C860.25,-311.63 860.25,-282.06 860.25,-260.85"/>
<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"/> <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> <text text-anchor="middle" x="874.25" y="-308.29" font-family="Times New Roman,serif" font-size="14.00">8080</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

1297
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -12,14 +12,21 @@ runner = CliRunner()
"test_file_path", "test_file_path",
[ [
"tests/ymls/builds/docker-compose.yml", "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/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/extends/docker-compose.yml",
"tests/ymls/links/docker-compose.yml", "tests/ymls/links/docker-compose.yml",
"tests/ymls/networks/docker-compose.yml", "tests/ymls/networks/docker-compose.yml",
"tests/ymls/ports/docker-compose.yml", "tests/ymls/ports/docker-compose.yml",
"tests/ymls/profiles/docker-compose.yml",
"tests/ymls/volumes/docker-compose.yml", "tests/ymls/volumes/docker-compose.yml",
"examples/full-stack-node-app/docker-compose.yml", "examples/full-stack-node-app/docker-compose.yml",
"examples/non-normative/docker-compose.yml", "examples/non-normative/docker-compose.yml",
"examples/voting-app/docker-compose.yml",
], ],
) )
def test_cli(test_file_path: str) -> None: def test_cli(test_file_path: str) -> None:

22
tests/test_devices.py Normal file
View file

@ -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

View file

@ -1,6 +1,7 @@
import pytest import pytest
from compose_viz.models.compose import Compose 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.extends import Extends
from compose_viz.models.port import Port, Protocol from compose_viz.models.port import Port, Protocol
from compose_viz.models.service import Service from compose_viz.models.service import Service
@ -171,6 +172,14 @@ from compose_viz.parser import Parser
container_port="6060", container_port="6060",
protocol=Protocol.udp, 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( Port(
host_port="127.0.0.1:8080", host_port="127.0.0.1:8080",
container_port="80", container_port="80",
@ -241,6 +250,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: def test_parse_file(test_file_path: str, expected: Compose) -> None:
@ -275,3 +401,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): 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.service_name == expected_service.extends.service_name
assert actual_service.extends.from_file == expected_service.extends.from_file 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

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

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

View file

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

View file

@ -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"

View file

@ -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

View file

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

View file

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

View file

@ -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

54
update-submodules.py Normal file
View file

@ -0,0 +1,54 @@
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()