diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..0868300 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,17 @@ +name: CD + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Build and Publish to PyPi + uses: JRubics/poetry-publish@v1.10 + with: + pypi_token: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e1b848..c40e9d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ main, dev ] pull_request: - branches: [ main ] + branches: [ main, dev ] jobs: test: @@ -86,9 +86,16 @@ jobs: docker-compose -f tests/in/111101.yaml config -q docker-compose -f tests/in/111110.yaml config -q docker-compose -f tests/in/111111.yaml config -q - + + - name: Setup Poetry + uses: Gr1N/setup-poetry@v7 + with: + poetry-version: 1.1.7 + + - name: Install Dependencies + run: | + poetry install --no-root + - name: Validate Custom Input File run: | - pip install pytest - pip install pytest-cov - python tests/test_validate_input_file.py tests/validate_input_file.py tests/in/docker-compose.yaml + poetry run python -m pytest diff --git a/.github/workflows/release-tagged-version.yml b/.github/workflows/release-tagged-version.yml new file mode 100644 index 0000000..3c6d547 --- /dev/null +++ b/.github/workflows/release-tagged-version.yml @@ -0,0 +1,45 @@ +name: Release Tagged Version + +on: + push: + tags: + - "v*" + +permissions: + id-token: "write" + contents: "write" + packages: "write" + pull-requests: "read" + +jobs: + tagged-release: + runs-on: "ubuntu-latest" + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + + - name: Switch to Current Branch + run: git checkout ${{ env.BRANCH }} + + - name: Setup Python 3.10.4 + uses: actions/setup-python@v3 + with: + python-version: '3.10.4' + + - name: Setup Poetry + uses: Gr1N/setup-poetry@v7 + with: + poetry-version: 1.1.7 + - run: | + poetry install --no-root + poetry build + + - name: "Release Tagged Version" + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false + files: | + LICENSE + dist/** \ No newline at end of file diff --git a/compose_viz/__init__.py b/compose_viz/__init__.py new file mode 100644 index 0000000..bce9bc5 --- /dev/null +++ b/compose_viz/__init__.py @@ -0,0 +1,2 @@ +__app_name__ = "compose_viz" +__version__ = "0.1.0" diff --git a/compose_viz/__main__.py b/compose_viz/__main__.py new file mode 100644 index 0000000..4065f3b --- /dev/null +++ b/compose_viz/__main__.py @@ -0,0 +1,5 @@ +from compose_viz.cli import start_cli + + +if __name__ == "__main__": + start_cli() diff --git a/compose_viz/cli.py b/compose_viz/cli.py new file mode 100644 index 0000000..d2bf1d3 --- /dev/null +++ b/compose_viz/cli.py @@ -0,0 +1,61 @@ +from enum import Enum +import typer +from typing import Optional +from compose_viz import __app_name__, __version__ +from compose_viz.parser import Parser + + +class VisualizationFormats(str, Enum): + png = "PNG" + dot = "DOT" + + +app = typer.Typer( + invoke_without_command=True, + no_args_is_help=True, + subcommand_metavar="", + add_completion=False, +) + + +def _version_callback(value: bool) -> None: + if value: + typer.echo(f"{__app_name__} {__version__}") + raise typer.Exit() + + +@app.callback() +def compose_viz( + input_path: str, + output_path: Optional[str] = typer.Option( + None, + "--output_path", + "-o", + help="Output path for the generated visualization.", + ), + format: VisualizationFormats = typer.Option( + "PNG", + "--format", + "-m", + help="Output format for the generated visualization.", + ), + _: Optional[bool] = typer.Option( + None, + "--version", + "-v", + help="Show the version of compose_viz.", + callback=_version_callback, + is_eager=True, + ) +) -> None: + parser = Parser() + compose = parser.parse(input_path) + + if compose: + typer.echo(f"Successfully parsed {input_path}") + + raise typer.Exit() + + +def start_cli() -> None: + app(prog_name=__app_name__) diff --git a/compose_viz/compose.py b/compose_viz/compose.py new file mode 100644 index 0000000..6c65a8a --- /dev/null +++ b/compose_viz/compose.py @@ -0,0 +1,10 @@ +from typing import List +from compose_viz.service import Service + + +class Compose: + def __init__(self, services: List[Service]) -> None: + self.services = services + + def extract_networks(self) -> List[str]: + raise NotImplementedError diff --git a/compose_viz/parser.py b/compose_viz/parser.py new file mode 100644 index 0000000..1f44bbc --- /dev/null +++ b/compose_viz/parser.py @@ -0,0 +1,10 @@ +from compose_viz.compose import Compose + + +class Parser: + def __init__(self): + pass + + def parse(self, file_path: str) -> Compose: + # validate input file using `docker-compose config -q sys.argv[1]` first + raise NotImplementedError diff --git a/compose_viz/service.py b/compose_viz/service.py new file mode 100644 index 0000000..6e42483 --- /dev/null +++ b/compose_viz/service.py @@ -0,0 +1,13 @@ +from typing import List + + +class Service: + def __init__(self, name: str, image: str, ports: List[str] = [], networks: List[str] = [], volumes: List[str] = [], depends_on: List[str] = [], links: List[str] = [], extends: List[str] = []) -> None: + self.name = name + self.image = image + self.ports = ports + self.networks = networks + self.volumes = volumes + self.depends_on = depends_on + self.links = links + self.extends = extends diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..381ddcb --- /dev/null +++ b/poetry.lock @@ -0,0 +1,191 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typer" +version = "0.4.1" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=7.1.1,<9.0.0" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] +test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "a80ea7abd86b8e5579a192dfa02a55d2219a3a1850bad12da89c30aa42e99156" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typer = [ + {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, + {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b7de75 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "compose-viz" +version = "0.1.0" +description = "A docker-compose/podman-compose graph visualization tool that allows you to gernerate graph in DOT format or PNG." +authors = ["Xyphuz Wu "] +readme = "README.md" +license = "MIT" +homepage = "https://github.com/compose-viz/compose-viz" +repository = "https://github.com/compose-viz/compose-viz" +include = [ + "LICENSE", +] + +[tool.poetry.dependencies] +python = "^3.9" +typer = "^0.4.1" + +[tool.poetry.dev-dependencies] +pytest = "^7.1.2" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +cpv = "compose_viz.cli:start_cli" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..3f427b5 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,13 @@ +from typer.testing import CliRunner +from compose_viz import cli + + +runner = CliRunner() + + +def test_cli(): + input_path = "tests/in/000001.yaml" + result = runner.invoke(cli.app, [input_path]) + + assert result.exit_code == 0 + assert f"Successfully parsed {input_path}\n" in result.stdout diff --git a/tests/test_parse_file.py b/tests/test_parse_file.py new file mode 100644 index 0000000..bfc6b4a --- /dev/null +++ b/tests/test_parse_file.py @@ -0,0 +1,28 @@ +from compose_viz.parser import Parser +from compose_viz.compose import Compose +from compose_viz.service import Service + + +def test_parse_file(): + expected: Compose = Compose([ + Service( + name='frontend', + image='awesome/webapp', + networks=['front-tier', 'back-tier'], + ), + Service( + name='monitoring', + image='awesome/monitoring', + networks=['admin'], + ), + Service( + name='backend', + image='awesome/backend', + networks=['back-tier', 'admin'], + ), + ]) + + parser = Parser() + actual = parser.parse('tests/in/000001.yaml') + + assert actual == expected diff --git a/tests/test_validate_input_file.py b/tests/test_validate_input_file.py deleted file mode 100644 index 54140c0..0000000 --- a/tests/test_validate_input_file.py +++ /dev/null @@ -1,5 +0,0 @@ -import sys -import pytest - -if __name__ == '__main__': - pytest.main([sys.argv[1]]) \ No newline at end of file diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..1d2abd6 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,12 @@ +from typer.testing import CliRunner +from compose_viz import cli, __app_name__, __version__ + + +runner = CliRunner() + + +def test_version(): + result = runner.invoke(cli.app, ["--version"]) + + assert result.exit_code == 0 + assert f"{__app_name__} {__version__}\n" in result.stdout diff --git a/tests/validate_input_file.py b/tests/validate_input_file.py deleted file mode 100644 index 73a4f3e..0000000 --- a/tests/validate_input_file.py +++ /dev/null @@ -1,6 +0,0 @@ -import os -import sys - -def test_validate_input_file(): - process = os.system("docker-compose -f " + sys.argv[2] + " config -q") - assert process == 0 \ No newline at end of file