Compare commits

...

30 commits

Author SHA1 Message Date
1a31d9790e replace user as root 2024-03-18 13:41:09 +01:00
k----n
e1dfdc65f2
Fetch networks instead of volumes (#59) 2022-01-18 15:08:25 +01:00
Julien BIANCHI
3ff9b0ccff
feat: Add GraphViz renderer (#53)
This will allow users to use the `dot` executable to produce any
supported output. For example, as requested in #52, one can now output a
SVG file. Here is an example:

```bash
bin/dcv render -m graphviz -o output.svg --graphviz-output-format svg
```

Closes #52
2020-08-19 19:40:57 +02:00
markiewb
60b3599c63
Update readme.md with troubleshooting of #41 (#51)
It has been proposed by others to include it in the readme.md
2020-07-29 21:39:24 +02:00
jubianchi
d1ef03be39
chore: Update copyright year 2020-07-23 23:11:19 +02:00
jubianchi
edaad5164c
chore: Fix workflow name 2020-07-23 22:45:01 +02:00
Julien BIANCHI
8cbae63adf
docs: Add a PowerShell example (#50)
thanks @PramodKumarYadav for the hint

Closes #44
2020-07-23 22:42:16 +02:00
Julien BIANCHI
03ab873e22
fix: Volume labels are correct (#49)
Closes #45
2020-07-23 22:35:06 +02:00
Julien BIANCHI
e1ad47ee9e
chore: Add github workflows (#48) 2020-07-23 22:26:42 +02:00
Grégory PLANCHAT
ead07e8fa9
Fixed the case where a service has no external file to import (#47) 2020-07-23 21:38:18 +02:00
Julien BIANCHI
bf8d4b200f
chore: Update PHP version (#46)
Closes #38
2020-07-23 21:10:34 +02:00
Julien BIANCHI
6ab4ce942b
fix: YAML parser error are more detailed (#34)
Closes #33
2020-07-23 20:55:49 +02:00
Julien BIANCHI
c025ca9266
Merge pull request #40 from karlwilbur/volumes-long-syntax
Support "Long Syntax" for volumes.
2020-05-27 22:42:54 +02:00
Karl Wilbur
1b9e7782e0 Support "Long Syntax" for volumes. 2018-04-12 14:09:27 -04:00
Julien BIANCHI
78c83037ee
Merge pull request #31 from pmsipilot/ip-port-mapping
fix: Support IP address in port mappings
2018-03-06 09:21:39 +01:00
jubianchi
a72bc1e45f
fix: Support IP address in port mappings
Closes #30
2018-01-30 22:36:59 +01:00
Julien BIANCHI
aa9cff3705
Merge pull request #29 from pmsipilot/update-deps
chore: Update dependencies
2018-01-20 11:10:31 +01:00
jubianchi
f6f67be46c
chore: Update dependencies 2018-01-20 10:49:09 +01:00
jubianchi
27758c0f0a
chore: Fix deploy script 2018-01-20 10:28:12 +01:00
jubianchi
96ee45edc9
feat: Add a logger and enable and options
Closes #26
2018-01-20 10:18:40 +01:00
jubianchi
deac235afe
feat: Add the and options to avoid rendering networks and ports
Closes #24
2018-01-20 09:50:37 +01:00
jubianchi
2a744599c0
feat: Add the option to set the graph's background color
Closes #25
2018-01-20 09:35:30 +01:00
jubianchi
29e2e60ef8
chore: Clean 2018-01-20 09:19:49 +01:00
jubianchi
fb1d7655f1
fix: Versions correctly merged and checked
Closes #28
2018-01-20 09:16:06 +01:00
Julien BIANCHI
add1fa32c0
Merge pull request #23 from pmsipilot/conditions-depends-on
Conditions depends on
2017-11-17 00:57:12 +01:00
jubianchi
9a87507a83
chore: Update changelog 2017-11-17 00:38:43 +01:00
jubianchi
1eb3843a45
feat: Display depends_on conditions
See #22
2017-11-17 00:36:54 +01:00
jubianchi
155f0e1fad
fix: Handle conditions in depends_on
Closes #22
2017-11-17 00:15:11 +01:00
Julien BIANCHI
acc508b1d8 Merge pull request #21 from pmsipilot/fix-entrypoint
fix: Entrypoint works with the lastest base image
2017-08-13 19:16:22 +02:00
jubianchi
ec09768b7e
fix: Entrypoint works with the lastest base image
Closes #20
2017-08-13 18:59:24 +02:00
19 changed files with 1471 additions and 416 deletions

30
.github/workflows/Deploy.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: Deploy
on:
push:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-node-
- name: Build and push Docker images
uses: docker/build-push-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: pmsipilot/docker-compose-viz
tag_with_ref: true

43
.github/workflows/Test.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Test
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Validate composer.json
run: composer validate --ansi --strict
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v2
with:
path: vendor
key: ${{ runner.os }}-node-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
- name: Unit tests
run: composer run ut
- name: Coding style
run: composer run cst
- name: Build and push Docker images
uses: docker/build-push-action@v1
with:
push: false

4
.gitignore vendored
View file

@ -2,4 +2,6 @@ vendor/
docker.lock
bin/
!bin/dcv
!bin/entry_script.sh
!bin/entrypoint.sh
.cache/
.php_cs.cache

16
.php_cs
View file

@ -1,12 +1,18 @@
<?php
$finder = Symfony\CS\Finder\DefaultFinder::create()
->exclude('somedir')
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.DIRECTORY_SEPARATOR.'src')
->in(__DIR__.DIRECTORY_SEPARATOR.'spec')
;
return Symfony\CS\Config\Config::create()
->level(Symfony\CS\FixerInterface::SYMFONY_LEVEL)
->finder($finder)
return (new PhpCsFixer\Config())
->setRules([
'@PSR2' => true,
'@Symfony' => true,
'array_syntax' => ['syntax' => 'short'],
'no_useless_else' => true,
'no_useless_return' => true,
'ordered_class_elements' => true,
])
->setFinder($finder)
;

View file

@ -1,50 +0,0 @@
language: php
sudo: required
services:
- docker
php:
- 7.0
- 7.1
- nightly
env:
matrix:
- COMPOSERFLAGS=
- COMPOSERFLAGS=--prefer-lowest
global:
# DOCKER_EMAIL
- secure: JAbpFtQJovMT1IqHJmSlVI3xQpLUoqDlbYXUgExK99dH7+GH78Rd6CgjCo5fDAw5KW4zBoTJut7BWv+qdF1j9qpUEXlxB6dEwKdLHCCM8M9p+BddkaZUWF9kG+4pC1h6AeDeWi7Q77TzrOX7VAP1UubFOPItRNayF99zCJH6ioh8qnHNi2XHedIagMW4PGLp6HmTuyhzdF6RVEXcCos2fpkx5wzcjALb/ffN8A0dqtfELRvs6bjaQZ1ktmNjd3fSnQOtGO14VOei8E58e4roZNdDHMoIxzeqaM+bphsTZvHne4DAZEn3wU2iguzXLZMCAgF336Inu+t2Rjv4UDLcPeppMrYA+02Ww0kbCXcIQx4Og0Un54SY5qdIHZtnTXA+SAFr+5r+T80p29Tlp9cUfxaHWFynDEEDWx7minAATweSQU50ipPnAkge1WY/ADSN1EARqM2GYVQYLvddIaTSIbnq37MnlncWL+jkcQ0cL83xidYd7JnQ7/gZJf9MzeZzMVXrY+2VYL1WOrv1uwmVi30bpQss1D7IVVtN5iBlB2ciph+iHdvkhv6f8ji9xZmWkotfDzsRoZg7csDAVgliz55QkJYKeWDoiW7kb1i+pIsgmrdEonPMg7NHjSqIIkrwI89016Tsx4LH1t2sRRYW8Z7wuVhW3VcJD+CnkhAWvLc=
# DOCKER_USER
- secure: "bdD/Sg7IvpY/qLBK8KiaMBihGx00sj+K3+qHnHoqzqqzF6NjZFgfeJfhvVRLz84JPxTqDOcISwAfCvrV/cmtQkFVI7UP5Xg2GniTH8VGJxhBQxCmqLGd1tOLR9PPGtEh4HcjybGcOcIE3zn0xHhTwgS6jLxrrwPeapDAc+BYJcbbyrT1tjy5mv+wR3HmB2p5oiRhUL3P6u1+ND/j2oDZDoSllNRs+5jTiSXToIlXjICr6f8bey5DfOpGEAnopMXqSQmcf92C9dixfWprk0KE2MvELbE4hWtuu44FPLJcXbrMunKpFOiN1IfIxU2CXjlYpG9vcroJe3hZWpXG9ZY9nn5KDv+6H48zu+CQgZx0uobBaNmVFOXxiqDElb/kjjF6+Xx55JxGuRpcsGNdsbqU6qwTJmgeBv1UPLGste3BHY5dQkf10QbxXJmHgF1GQihS5mjpf1cobNMra0JxIFDUfpedB3cnFoM+masxKY6fexu9hXb3d5Yi7DKsHA2JUGayVCaGjcJEYYBzNLlNs9NbMcQHqfQ6a90HM1yH11DKMPWEGkvp7+M1ixjOUpTYng5yrOhgl624LDsNu/NwadtKTrk3wO9liyOJgojTUvbpVlB9potFf8vtX8JhKqGyOcMgaOx8VZt+gIL1W25hHtmEYglsfkEze3fOwWW8o4CBI2E="
# DOCKER_PASS
- secure: "BqpmNofWU38cV3eUT9Hm9wxiiRlp1LKl1JQBxvsYng/dk4L7ONiBVsrG6T7nYhkTktWc/II4ZAoQ7AGHN42W1quzSmc6d0fszQ7uTwNobfFQu2JzFNkhyP9D6b0v8uXhT8n6TPqOGgoJUcguStvasG3TuZwn7+PhwjyVfnzf+DhyaXjCljMatg7ekL0JDPGdAz/SQhuBiwk2xlZtxtEokNy7IVr9VcMi2O0nG3LMhCl1sQjo3JKBxPsalQi78dShDUHcazAE68T7M1FjAZCJYia902FMDWiIuujLamq+NpDgEKB3aLCLwF/o3j8z3ekPrk2v9Zokz+t36cQ2BmPpwqFfhvPdUv9tj9bi7Qv2R4NKreX8TWB8KB5afSVWiKfufWV5hp5KfwEmcLBc/hQdjRIwzDqVPK/fyy/GJ5fT4X5kz+YYLQEFxeWPtxL+OpQUXx2P5iDhx5qz173lO4h1WX4vEQ3p4aFbfnNREUDPGYsMJo6flm5Azq8F0qh065sxPldKunr9H4fAXrFzqMJnTepReEGPNJRn35TLl08RI7GTp0hKxlaycsu+c2Qz0/GcKbODWf5w24d/pxrOMM9KmJpDZTBm9bWiZlRbbZm1OnK0PiaRi9ft44Em5NYTFVvuWL2M2tIyGObI3kquKTkANvrSuPTofJ0JawXg2YBKOH8="
matrix:
exclude:
- php: 7.0
env: COMPOSERFLAGS=
- php: 7.1
env: COMPOSERFLAGS=--prefer-lowest
- php: nightly
env: COMPOSERFLAGS=--prefer-lowest
allow_failures:
- php: nightly
script:
- make test
deploy:
- provider: script
script: ci/deploy.sh
on:
php: 7.1
all_branches: master
- provider: script
script: ci/deploy.sh
on:
php: 7.1
tags: true

View file

@ -1,5 +1,14 @@
# `1.1.0` (unreleased)
# `1.2.0` (unreleased)
* Add a logger and enable `-v` and `-vv` options
* Add the `--no-networks` and `--no-ports` options to avoid rendering networks and ports
* Add the `--background` option to set the graph's background color
* Versions correctly merged and checked
# `1.1.0`
* Display `depends_on` conditions
* Handle conditions in `depends_on`
* Automatically load override file if it exists or ignore it using `--ignore-override`
# `1.0.0`

View file

@ -1,4 +1,16 @@
FROM php:7.1-alpine
FROM php:7.4-alpine as builder
COPY composer.json /dcv/composer.json
COPY composer.lock /dcv/composer.lock
WORKDIR /dcv
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php && \
php -r "unlink('composer-setup.php');" && \
php composer.phar install --prefer-dist
FROM php:7.4-alpine
RUN apk update && \
apk add graphviz ttf-dejavu && \
@ -8,16 +20,17 @@ RUN apk update && \
COPY bin/ /dcv/bin
COPY src/ /dcv/src
COPY vendor/ /dcv/vendor
COPY --from=builder /dcv/vendor /dcv/vendor
RUN chmod +x /dcv/bin/dcv
RUN addgroup dcv && \
adduser -D -G dcv -s /bin/bash -g "docker-compose-viz" -h /input dcv
USER dcv
#USER dcv
USER root
VOLUME /input
WORKDIR /input
ENTRYPOINT ["/dcv/bin/entry_script.sh"]
ENTRYPOINT ["/dcv/bin/entrypoint.sh"]
CMD ["render", "-m", "image", "-f"]

View file

@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright (c) 2016 PMSIpilot
Copyright (c) 2020 PMSIpilot
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the

View file

@ -12,29 +12,23 @@ docker: docker.lock
test: vendor unit cs
unit: vendor
$(PHP) bin/kahlan --pattern='*.php' --reporter=verbose --persistent=false --cc=true
$(COMPOSER) run ut
cs:
$(PHP) bin/php-cs-fixer fix --dry-run
$(COMPOSER) run cst
fix-cs:
$(PHP) bin/php-cs-fixer fix
$(COMPOSER) run cs
clean:
rm -rf vendor/
docker.lock: Dockerfile vendor
$(COMPOSER) dump-autoload --classmap-authoritative
docker.lock: Dockerfile bin/entrypoint.sh vendor src/application.php src/functions.php
$(DOCKER) build -t $(DCV_IMAGE_NAME) .
touch docker.lock
ifndef COMPOSERFLAGS
vendor: composer.lock
$(COMPOSER) install --prefer-dist
else
vendor: composer.lock
$(COMPOSER) update $(COMPOSERFLAGS)
endif
composer.lock: composer.json
$(COMPOSER) update $(COMPOSERFLAGS)

View file

@ -1,7 +1,5 @@
# `docker-compose-viz`
[![Build Status](https://img.shields.io/travis/pmsipilot/docker-compose-viz/master.svg?style=flat-square)](https://travis-ci.org/pmsipilot/docker-compose-viz)
[![StyleCI](https://styleci.io/repos/65026022/shield)](https://styleci.io/repos/65026022)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/pmsipilot/docker-compose-viz.svg)](http://isitmaintained.com/project/pmsipilot/docker-compose-viz "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/pmsipilot/docker-compose-viz.svg)](http://isitmaintained.com/project/pmsipilot/docker-compose-viz "Percentage of issues still open")
[![Docker Stars](https://img.shields.io/docker/stars/pmsipilot/docker-compose-viz.svg?style=flat)](https://hub.docker.com/r/pmsipilot/docker-compose-viz/)
@ -13,8 +11,13 @@
Considering the current working directory is where your `docker-compose.yml` file is located:
```
```bash
docker build . -t pmsipilot/docker-compose-viz
docker run --rm -it --name dcv -v $(pwd):/input pmsipilot/docker-compose-viz render -m image docker-compose.yml
# PowerShell
docker run --rm -it --name dcv -v ${pwd}:/input pmsipilot/docker-compose-viz render -m image docker-compose.yml
```
This will generate the `docker-compose.png` file in the current working directory.
@ -24,7 +27,7 @@ This will generate the `docker-compose.png` file in the current working director
Before you start, make sure you have:
* [Composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx) installed,
* [PHP 7](http://php.net/downloads.php#v7.0.9) installed,
* [PHP 7.2](http://php.net/downloads.php#v7.2.32) (at least) installed,
* GraphViz installed (see below for a guide on how to install it)
```
@ -175,7 +178,14 @@ digraph G {
![display renderer](resources/display.png)
### Troubleshooting
#### Getting "failed to open stream: Permission denied"?
Make sure the target directory is writeable by the user in the Docker container.
Or create a writeable directory first. See [workaround #41](https://github.com/pmsipilot/docker-compose-viz/issues/41#issuecomment-483384999)
## License
The MIT License (MIT)
Copyright (c) 2016 PMSIpilot
Copyright ® 2020 PMSIpilot

View file

@ -6,7 +6,7 @@ CURRENT_DIR=$(dirname $0)
if [ "$1" = "render" ]
then
$("$CURRENT_DIR/dcv" "$@")
$CURRENT_DIR/dcv "$@"
else
exec "$@"
fi

View file

@ -4,8 +4,9 @@ export REPO=pmsipilot/docker-compose-viz
export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi`
git reset --hard
git clean -dfx
composer install --no-dev --prefer-dist --classmap-authoritative
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker build -f Dockerfile -t $REPO:$TAG .
docker push $REPO

View file

@ -2,14 +2,15 @@
"name": "pmsipilot/docker-compose-viz",
"description": "Docker compose graph visualization",
"require": {
"symfony/yaml": "^3.1 || ^4@dev",
"php": "^7.2",
"symfony/yaml": "^3.1 || ^4",
"symfony/console": "^3.1",
"clue/graph": "^0.9",
"graphp/graphviz": "^0.2"
},
"require-dev": {
"crysalead/kahlan": "^2.5.4",
"friendsofphp/php-cs-fixer": "^1.11"
"friendsofphp/php-cs-fixer": "^2",
"kahlan/kahlan": "^4.7"
},
"license": "MIT",
"authors": [
@ -24,7 +25,9 @@
"PMSIpilot\\DockerComposeViz\\": "src/"
}
},
"config": {
"bin-dir": "bin/"
"scripts": {
"cs": "php-cs-fixer fix",
"cst": "php-cs-fixer fix --dry-run",
"ut": "kahlan --grep='*.php' --reporter=verbose --persistent=false"
}
}

1334
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +0,0 @@
{
"version": 2,
"services": {
"foo": {
"image": "bar"
}
}
}

View file

@ -0,0 +1,4 @@
version
services:
foo:
image: bar

View file

@ -19,7 +19,7 @@ describe('Reading configuration', function () {
it('should report if YAML is invalid', function () {
expect(function () {
readConfiguration(__DIR__.'/fixtures/read-configuration/invalid.json');
readConfiguration(__DIR__.'/fixtures/read-configuration/invalid.yml');
})
->toThrow(new InvalidArgumentException());
});

View file

@ -4,12 +4,6 @@ namespace PMSIpilot\DockerComposeViz;
use Graphp\GraphViz\GraphViz;
use Symfony\Component\Console;
use function PMSIpilot\DockerComposeViz\applyGraphvizStyle;
use function PMSIpilot\DockerComposeViz\createGraph;
use function PMSIpilot\DockerComposeViz\fetchNetworks;
use function PMSIpilot\DockerComposeViz\fetchServices;
use function PMSIpilot\DockerComposeViz\fetchVolumes;
use function PMSIpilot\DockerComposeViz\readConfiguration;
$application = new Console\Application();
@ -18,50 +12,82 @@ $application->register('render')
->addOption('override', null, Console\Input\InputOption::VALUE_REQUIRED, 'Tag of the override file to use', 'override')
->addOption('output-file', 'o', Console\Input\InputOption::VALUE_REQUIRED, 'Path to a output file (Only for "dot" and "image" output format)')
->addOption('output-format', 'm', Console\Input\InputOption::VALUE_REQUIRED, 'Output format (one of: "dot", "image", "display")', 'display')
->addOption('output-format', 'm', Console\Input\InputOption::VALUE_REQUIRED, 'Output format (one of: "dot", "image", "display", "graphviz")', 'display')
->addOption('graphviz-output-format', null, Console\Input\InputOption::VALUE_REQUIRED, 'GraphViz Output format (see `man dot` for details)', 'svg')
->addOption('only', null, Console\Input\InputOption::VALUE_IS_ARRAY | Console\Input\InputOption::VALUE_REQUIRED, 'Display a graph only for a given services')
->addOption('force', 'f', Console\Input\InputOption::VALUE_NONE, 'Overwrites output file if it already exists')
->addOption('no-volumes', null, Console\Input\InputOption::VALUE_NONE, 'Do not display volumes')
->addOption('no-networks', null, Console\Input\InputOption::VALUE_NONE, 'Do not display networks')
->addOption('no-ports', null, Console\Input\InputOption::VALUE_NONE, 'Do not display ports')
->addOption('horizontal', 'r', Console\Input\InputOption::VALUE_NONE, 'Display a horizontal graph')
->addOption('ignore-override', null, Console\Input\InputOption::VALUE_NONE, 'Ignore override file')
->addOption('background', null, Console\Input\InputOption::VALUE_REQUIRED, 'Set the graph background color', '#ffffff')
->setCode(function (Console\Input\InputInterface $input, Console\Output\OutputInterface $output) {
$backgroundColor = $input->getOption('background');
if (0 === preg_match('/^#[a-fA-F0-9]{6}|transparent$/', $backgroundColor)) {
throw new Console\Exception\InvalidArgumentException(sprintf('Invalid background color "%s". It must be a valid hex color or "transparent".', $backgroundColor));
}
$logger = logger($output);
$inputFile = $input->getArgument('input-file');
$inputFileExtension = pathinfo($inputFile, PATHINFO_EXTENSION);
$overrideFile = dirname($inputFile).DIRECTORY_SEPARATOR.basename($inputFile, '.'.$inputFileExtension).'.'.$input->getOption('override').'.'.$inputFileExtension;
$outputFormat = $input->getOption('output-format');
$outputFile = $input->getOption('output-file') ?: getcwd().DIRECTORY_SEPARATOR.'docker-compose.'.($outputFormat === 'dot' ? $outputFormat : 'png');
$outputFile = $input->getOption('output-file') ?: getcwd().DIRECTORY_SEPARATOR.'docker-compose.'.('dot' === $outputFormat ? $outputFormat : 'png');
$onlyServices = $input->getOption('only');
if (in_array($outputFormat, ['dot', 'image', 'display']) === false) {
if (false === in_array($outputFormat, ['dot', 'image', 'display', 'graphviz'])) {
throw new Console\Exception\InvalidArgumentException(sprintf('Invalid output format "%s". It must be one of "dot", "image" or "display".', $outputFormat));
}
if ($outputFormat === 'display') {
if ('display' === $outputFormat) {
if ($input->getOption('force') || $input->getOption('output-file')) {
$output->writeln('<comment>The following options are ignored with the "display" output format: "--force", "--output-file"</comment>');
}
} else {
if (file_exists($outputFile) === true && $input->getOption('force') === false) {
if (true === file_exists($outputFile) && false === $input->getOption('force')) {
throw new Console\Exception\InvalidArgumentException(sprintf('File "%s" already exists. Use the "--force" option to overwrite it.', $outputFile));
}
}
$logger(sprintf('Reading <comment>configuration</comment> from <info>"%s"</info>', $inputFile));
$configuration = readConfiguration($inputFile);
$configurationVersion = (string) ($configuration['version'] ?? 1);
if (!$input->getOption('ignore-override') && file_exists($overrideFile)) {
$logger(sprintf('Reading <comment>override</comment> from <info>"%s"</info>', $overrideFile));
$override = readConfiguration($overrideFile);
$overrideVersion = (string) ($override['version'] ?? 1);
if ($configurationVersion !== $overrideVersion) {
throw new Console\Exception\LogicException(sprintf('Version mismatch: file "%s" specifies version "%s" but file "%s" uses version "%s"', $inputFile, $configurationVersion, $overrideFile, $overrideVersion));
}
$configuration = array_merge_recursive($configuration, $override);
$logger(sprintf('Configuration <comment>version</comment> is <info>"%s"</info>', $configurationVersion), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE);
$configuration['version'] = $configurationVersion;
}
$logger('Fetching <comment>services</comment>');
$services = fetchServices($configuration);
$logger(sprintf('Found <info>%d</info> <comment>services</comment>', count($services)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE);
$logger('Fetching <comment>volumes</comment>');
$volumes = fetchVolumes($configuration);
$logger(sprintf('Found <info>%d</info> <comment>volumes</comment>', count($volumes)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE);
$logger('Fetching <comment>networks</comment>');
$networks = fetchNetworks($configuration);
$logger(sprintf('Found <info>%d</info> <comment>networks</comment>', count($networks)), Console\Output\OutputInterface::VERBOSITY_VERY_VERBOSE);
if ([] !== $onlyServices) {
$logger(sprintf('Only <info>%s</info> <comment>services</comment> will be displayed', implode(', ', $onlyServices)));
$intersect = array_intersect($onlyServices, array_keys($services));
if ($intersect !== $onlyServices) {
@ -77,9 +103,30 @@ $application->register('render')
);
}
$flags = 0;
if (true === $input->getOption('no-volumes')) {
$logger('<comment>Volumes</comment> will not be displayed');
$flags |= WITHOUT_VOLUMES;
}
if (true === $input->getOption('no-networks')) {
$logger('<comment>Networks</comment> will not be displayed');
$flags |= WITHOUT_NETWORKS;
}
if (true === $input->getOption('no-ports')) {
$logger('<comment>Ports</comment> will not be displayed');
$flags |= WITHOUT_PORTS;
}
$logger('Rendering <comment>graph</comment>');
$graph = applyGraphvizStyle(
createGraph($services, $volumes, $networks, $input->getOption('no-volumes') === false, $inputFile),
$input->getOption('horizontal')
createGraph($services, $volumes, $networks, $inputFile, $flags),
$input->getOption('horizontal'),
$input->getOption('background')
);
switch ($outputFormat) {
@ -95,6 +142,13 @@ $application->register('render')
$renderer = new GraphViz();
$renderer->display($graph);
break;
case 'graphviz':
$renderer = new GraphViz();
$format = $input->getOption('graphviz-output-format');
file_put_contents($outputFile, $renderer->setFormat($format)->createImageData($graph));
break;
}
});

View file

@ -5,26 +5,40 @@ namespace PMSIpilot\DockerComposeViz;
use Fhaculty\Graph\Edge;
use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Vertex;
use InvalidArgumentException;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
const WITHOUT_VOLUMES = 1;
const WITHOUT_NETWORKS = 2;
const WITHOUT_PORTS = 4;
/**
* @internal
*/
function logger(OutputInterface $output): callable
{
return function (string $message, int $verbosity = null) use ($output) {
$output->writeln(sprintf('[%s] %s', date(DATE_ISO8601), $message), $verbosity ?: OutputInterface::VERBOSITY_VERBOSE);
};
}
/**
* @public
*
* @param string $path Path to a YAML file
*
* @return array
*/
function readConfiguration(string $path) : array
function readConfiguration(string $path): array
{
if (file_exists($path) === false) {
throw new \InvalidArgumentException(sprintf('File "%s" does not exist', $path));
if (false === file_exists($path)) {
throw new InvalidArgumentException(sprintf('File "%s" does not exist', $path));
}
try {
return Yaml::parse(file_get_contents($path));
} catch (ParseException $exception) {
throw new \InvalidArgumentException(sprintf('File "%s" does not contain valid YAML', $path));
throw new InvalidArgumentException(sprintf('File "%s" does not contain valid YAML', $path), $exception->getCode(), $exception);
}
}
@ -35,9 +49,9 @@ function readConfiguration(string $path) : array
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchServices(array $configuration) : array
function fetchServices(array $configuration): array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) {
return $configuration;
}
@ -51,9 +65,9 @@ function fetchServices(array $configuration) : array
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchVolumes(array $configuration) : array
function fetchVolumes(array $configuration): array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) {
return [];
}
@ -67,9 +81,9 @@ function fetchVolumes(array $configuration) : array
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchNetworks(array $configuration) : array
function fetchNetworks(array $configuration): array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
if (false === isset($configuration['version']) || 1 === (int) $configuration['version']) {
return [];
}
@ -87,26 +101,28 @@ function fetchNetworks(array $configuration) : array
*
* @return Graph The complete graph for the given list of services
*/
function createGraph(array $services, array $volumes, array $networks, bool $withVolumes, string $path) : Graph
function createGraph(array $services, array $volumes, array $networks, string $path, int $flags): Graph
{
return makeVerticesAndEdges(new Graph(), $services, $volumes, $networks, $withVolumes, $path);
return makeVerticesAndEdges(new Graph(), $services, $volumes, $networks, $path, $flags);
}
/**
* @public
*
* @param Graph $graph Input graph
* @param bool $horizontal Display a horizontal graph
* @param Graph $graph Input graph
* @param bool $horizontal Display a horizontal graph
* @param string $horizontal Background color (any hex color or 'transparent')
*
* @return Graph A copy of the input graph with style attributes
*/
function applyGraphvizStyle(Graph $graph, bool $horizontal) : Graph
function applyGraphvizStyle(Graph $graph, bool $horizontal, string $background): Graph
{
$graph = $graph->createGraphClone();
$graph->setAttribute('graphviz.graph.bgcolor', $background);
$graph->setAttribute('graphviz.graph.pad', '0.5');
$graph->setAttribute('graphviz.graph.ratio', 'fill');
if ($horizontal === true) {
if (true === $horizontal) {
$graph->setAttribute('graphviz.graph.rankdir', 'LR');
}
@ -137,7 +153,7 @@ function applyGraphvizStyle(Graph $graph, bool $horizontal) : Graph
case 'port':
$vertex->setAttribute('graphviz.shape', 'circle');
if (($proto = $vertex->getAttribute('docker_compose.proto')) === 'udp') {
if ('udp' === ($proto = $vertex->getAttribute('docker_compose.proto'))) {
$vertex->setAttribute('graphviz.style', 'dashed');
}
break;
@ -172,8 +188,12 @@ function applyGraphvizStyle(Graph $graph, bool $horizontal) : Graph
break;
}
if (($alias = $edge->getAttribute('docker_compose.alias')) !== null) {
if (null !== ($alias = $edge->getAttribute('docker_compose.alias'))) {
$edge->setAttribute('graphviz.label', $alias);
if (null !== $edge->getAttribute('docker_compose.condition')) {
$edge->setAttribute('graphviz.fontsize', '10');
}
}
if ($edge->getAttribute('docker_compose.bidir')) {
@ -195,34 +215,39 @@ function applyGraphvizStyle(Graph $graph, bool $horizontal) : Graph
*
* @return Graph A copy of the input graph with vertices and edges for services
*/
function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, array $networks, bool $withVolumes, $path) : Graph
function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, array $networks, string $path, int $flags): Graph
{
if ($withVolumes === true) {
if (false === ((bool) ($flags & WITHOUT_VOLUMES))) {
foreach (array_keys($volumes) as $volume) {
addVolume($graph, 'named: '.$volume);
}
}
foreach ($networks as $network => $definition) {
addNetwork(
$graph, 'net: '.$network,
isset($definition['external']) && $definition['external'] === true ? 'external_network' : 'network'
);
if (false === ((bool) ($flags & WITHOUT_NETWORKS))) {
foreach ($networks as $network => $definition) {
addNetwork(
$graph,
'net: '.$network,
isset($definition['external']) && true === $definition['external'] ? 'external_network' : 'network'
);
}
}
foreach ($services as $service => $definition) {
addService($graph, $service);
if (isset($definition['extends'])) {
$configuration = readConfiguration(dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file']);
$extendedServices = fetchServices($configuration);
$extendedVolumes = fetchVolumes($configuration);
$extendedNetworks = fetchVolumes($configuration);
if (isset($definition['extends']['file'])) {
$configuration = readConfiguration(dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file']);
$extendedServices = fetchServices($configuration);
$extendedVolumes = fetchVolumes($configuration);
$extendedNetworks = fetchNetworks($configuration);
$graph = makeVerticesAndEdges($graph, $extendedServices, $extendedVolumes, $extendedNetworks, $withVolumes, dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file']);
$graph = makeVerticesAndEdges($graph, $extendedServices, $extendedVolumes, $extendedNetworks, dirname($path).DIRECTORY_SEPARATOR.$definition['extends']['file'], $flags);
}
addRelation(
addService($graph, $definition['extends']['service']),
addService($graph, $definition['extends']['service']),
$graph->getVertex($service),
'extends'
);
@ -256,11 +281,14 @@ function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, arr
);
}
foreach ($definition['depends_on'] ?? [] as $dependency) {
foreach ($definition['depends_on'] ?? [] as $key => $dependency) {
addRelation(
$graph->getVertex($service),
addService($graph, $dependency),
'depends_on'
addService($graph, is_array($dependency) ? $key : $dependency),
'depends_on',
is_array($dependency) && isset($dependency['condition']) ? $dependency['condition'] : null,
false,
is_array($dependency) && isset($dependency['condition'])
);
}
@ -272,11 +300,17 @@ function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, arr
);
}
if ($withVolumes === true) {
if (false === ((bool) ($flags & WITHOUT_VOLUMES))) {
$serviceVolumes = [];
foreach ($definition['volumes'] ?? [] as $volume) {
list($host, $container, $attr) = explodeMapping($volume);
if (is_array($volume)) {
$host = $volume['source'];
$container = $volume['target'];
$attr = !empty($volume['read-only']) ? 'ro' : '';
} else {
list($host, $container, $attr) = explodeVolumeMapping($volume);
}
$serviceVolumes[$container] = [$host, $attr];
}
@ -284,7 +318,7 @@ function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, arr
foreach ($serviceVolumes as $container => $volume) {
list($host, $attr) = $volume;
if ($host[0] !== '.' && $host[0] !== DIRECTORY_SEPARATOR) {
if ('.' !== $host[0] && DIRECTORY_SEPARATOR !== $host[0]) {
$host = 'named: '.$host;
}
@ -293,33 +327,37 @@ function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, arr
$graph->getVertex($service),
'volumes',
$host !== $container ? $container : null,
$attr !== 'ro'
'ro' !== $attr
);
}
}
foreach ($definition['ports'] ?? [] as $port) {
list($host, $container, $proto) = explodeMapping($port);
if (false === ((bool) ($flags & WITHOUT_PORTS))) {
foreach ($definition['ports'] ?? [] as $port) {
list($target, $host, $container, $proto) = explodePortMapping($port);
addRelation(
addPort($graph, (int) $host, $proto),
$graph->getVertex($service),
'ports',
$host !== $container ? $container : null
);
addRelation(
addPort($graph, (int) $host, $proto, $target),
$graph->getVertex($service),
'ports',
$host !== $container ? $container : null
);
}
}
foreach ($definition['networks'] ?? [] as $network => $config) {
$network = is_int($network) ? $config : $network;
$config = is_int($network) ? [] : $config;
$aliases = $config['aliases'] ?? [];
if (false === ((bool) ($flags & WITHOUT_NETWORKS))) {
foreach ($definition['networks'] ?? [] as $network => $config) {
$network = is_int($network) ? $config : $network;
$config = is_int($network) ? [] : $config;
$aliases = $config['aliases'] ?? [];
addRelation(
$graph->getVertex($service),
addNetwork($graph, 'net: '.$network),
'networks',
count($aliases) > 0 ? implode(', ', $aliases) : null
);
addRelation(
$graph->getVertex($service),
addNetwork($graph, 'net: '.$network),
'networks',
count($aliases) > 0 ? implode(', ', $aliases) : null
);
}
}
}
@ -337,7 +375,7 @@ function makeVerticesAndEdges(Graph $graph, array $services, array $volumes, arr
*/
function addService(Graph $graph, string $service, string $type = null)
{
if ($graph->hasVertex($service) === true) {
if (true === $graph->hasVertex($service)) {
return $graph->getVertex($service);
}
@ -356,13 +394,15 @@ function addService(Graph $graph, string $service, string $type = null)
*
* @return Vertex
*/
function addPort(Graph $graph, int $port, string $proto = null)
function addPort(Graph $graph, int $port, string $proto = null, string $target = null)
{
if ($graph->hasVertex($port) === true) {
return $graph->getVertex($port);
$target = $target ? $target.':' : null;
if (true === $graph->hasVertex($target.$port)) {
return $graph->getVertex($target.$port);
}
$vertex = $graph->createVertex($port);
$vertex = $graph->createVertex($target.$port);
$vertex->setAttribute('docker_compose.type', 'port');
$vertex->setAttribute('docker_compose.proto', $proto ?: 'tcp');
@ -379,7 +419,7 @@ function addPort(Graph $graph, int $port, string $proto = null)
*/
function addVolume(Graph $graph, string $path)
{
if ($graph->hasVertex($path) === true) {
if (true === $graph->hasVertex($path)) {
return $graph->getVertex($path);
}
@ -400,7 +440,7 @@ function addVolume(Graph $graph, string $path)
*/
function addNetwork(Graph $graph, string $name, string $type = null)
{
if ($graph->hasVertex($name) === true) {
if (true === $graph->hasVertex($name)) {
return $graph->getVertex($name);
}
@ -418,10 +458,9 @@ function addNetwork(Graph $graph, string $name, string $type = null)
* @param string $type Type of the relation (one of "links", "volumes_from", "depends_on", "ports");
* @param string|null $alias Alias associated to the linked element
* @param bool|null $bidirectional Biderectional or not
*
* @return Edge\Directed
* @param bool|null $condition Wether the alias represents a condition or not
*/
function addRelation(Vertex $from, Vertex $to, string $type, string $alias = null, bool $bidirectional = false) : Edge\Directed
function addRelation(Vertex $from, Vertex $to, string $type, string $alias = null, bool $bidirectional = false, bool $condition = false): Edge\Directed
{
$edge = null;
@ -441,10 +480,14 @@ function addRelation(Vertex $from, Vertex $to, string $type, string $alias = nul
$edge->setAttribute('docker_compose.type', $type);
if ($alias !== null) {
if (null !== $alias) {
$edge->setAttribute('docker_compose.alias', $alias);
}
if (true === $condition) {
$edge->setAttribute('docker_compose.condition', true);
}
$edge->setAttribute('docker_compose.bidir', $bidirectional);
return $edge;
@ -455,19 +498,56 @@ function addRelation(Vertex $from, Vertex $to, string $type, string $alias = nul
*
* @param string $mapping A docker mapping (<from>[:<to>])
*
* @return array An 2 items array containing the parts of the mapping.
* @return array An 2 or 3 items array containing the parts of the mapping.
* If the mapping does not specify a second part, the first one will be repeated
*/
function explodeMapping($mapping) : array
function explodeMapping($mapping): array
{
$parts = explode(':', $mapping);
$parts[1] = $parts[1] ?? $parts[0];
$subparts = array_values(array_filter(explode('/', $parts[1])));
return [$parts[0], $parts[1]];
}
if (count($subparts) > 2) {
$subparts = [$parts[1], $parts[2] ?? null];
/**
* @internal
*
* @param string $mapping A docker mapping (<from>[:<to>])
*
* @return array An 2 or 3 items array containing the parts of the mapping.
* If the mapping does not specify a second part, the first one will be repeated
*/
function explodeVolumeMapping($mapping): array
{
$parts = explode(':', $mapping);
$parts[1] = $parts[1] ?? $parts[0];
return [$parts[0], $parts[1], $parts[2] ?? null];
}
/**
* @internal
*
* @param string $mapping A docker mapping (<from>[:<to>])
*
* @return array An 2 or 3 items array containing the parts of the mapping.
* If the mapping does not specify a second part, the first one will be repeated
*/
function explodePortMapping($mapping): array
{
$parts = explode(':', $mapping);
if (count($parts) < 3) {
$target = null;
$host = $parts[0];
$container = $parts[1] ?? $parts[0];
} else {
$target = $parts[0];
$host = $parts[1];
$container = $parts[2];
}
return [$parts[0], $subparts[0], $subparts[1] ?? null];
$subparts = array_values(array_filter(explode('/', $container)));
return [$target, $host, $subparts[0], $subparts[1] ?? null];
}