Initial source import

This commit is contained in:
jubianchi 2016-08-05 15:10:28 +02:00
parent bf4e1d1fc0
commit fe46cbd889
No known key found for this signature in database
GPG key ID: 5D9C896D2AA9E390
24 changed files with 1331 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
vendor/
docker.lock

17
.travis.yml Normal file
View file

@ -0,0 +1,17 @@
language: php
php:
- 7.0
- nightly
env:
matrix:
- COMPOSERFLAGS="--prefer-stable"
- COMPOSERFLAGS="--prefer-lowest"
matrix:
allow_failures:
- php: nightly
script:
- make test

21
Dockerfile Normal file
View file

@ -0,0 +1,21 @@
FROM php:7-alpine
RUN apk update && \
apk add graphviz ttf-dejavu && \
rm -rf \
/var/cache/apk/* \
/tmp/*
COPY bin/ /dcv/bin
COPY src/ /dcv/src
COPY vendor/ /dcv/vendor
RUN addgroup dcv && \
adduser -D -G dcv -s /bin/bash -g "docker-compose-viz" -h /input dcv
USER dcv
VOLUME /input
WORKDIR /input
ENTRYPOINT ["/dcv/bin/dcv"]
CMD ["render", "-m", "image", "-f"]

15
LICENSE Normal file
View file

@ -0,0 +1,15 @@
The MIT License (MIT)
Copyright (c) 2016 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
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

32
Makefile Normal file
View file

@ -0,0 +1,32 @@
DCV_IMAGE_NAME=pmsipilot/docker-compose-viz
COMPOSER ?= composer
COMPOSERFLAGS ?=
DOCKER ?= docker
PHP ?= php
.PHONY: clean docker test
docker: docker.lock
test: vendor
$(PHP) bin/kahlan --pattern='*.php' --reporter=verbose
clean:
rm -rf vendor/
docker.lock: Dockerfile vendor
$(COMPOSER) dump-autoload --classmap-authoritative
$(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)

146
README.md Normal file
View file

@ -0,0 +1,146 @@
# `docker-compose-viz`
[![Build Status](https://travis-ci.org/pmsipilot/docker-compose-viz.svg?branch=master)](https://travis-ci.org/pmsipilot/docker-compose-viz)
## How to use
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,
* GraphViz installed (see below for a guide on how to install it)
### Docker
```
docker run --rm -it --name dcv -v $(pwd):/input pmsipilot/docker-compose-viz
```
### PHP
```
git clone https://github.com/pmsipilot/docker-compose-viz.git
make vendor
# Or
composer install --prefer-dist
bin/dcv
```
### Install GraphViz
* On MacOS: `brew install graphviz`
* On Debian: `sudo apt-get install graphviz`
## Usage
```
render [options] [--] [<input-file>]
Arguments:
input-file Path to a docker compose file [default: "./docker-compose.yml"]
Options:
-o, --output-file=OUTPUT-FILE Path to a output file (Only for "dot" and "image" output format) [default: "./docker-compose.dot" or "./docker-compose.png"]
-m, --output-format=OUTPUT-FORMAT Output format (one of: "dot", "image", "display") [default: "display"]
--only=ONLY Display a graph only for a given services (multiple values allowed)
-f, --force Overwrites output file if it already exists
--no-volumes Do not display volumes
-r, --horizontal Display a horizontal graph
```
## How to read the graph
### Links
Links (from `services.<service>.links`) are displayed as plain arrows pointing to the service that declares the link:
![links](resources/links.png)
If we look at the link between `mysql` and `ambassador`, it reads as follow: "`mysql` is known as `mysql` in `ambassador`."
If we look at the link between `ambassador` and `logs`, it reads as follow: "`ambassador` is known as `logstash` in `logs`."
### Volumes
Volumes (from `services.<service>.volumes_from`) are displayed as dashed arrows pointing to the service that uses the volumes:
![volumes](resources/volumes.png)
If we look at the link between `logs` and `api`, it reads as follow: "`api` uses volumes from `logs`."
Volumes (from `services.<service>.volumes`) are displayed as folders with the host directory as label and are linked to the service that uses them dashed arrows.
If we look at the link between `./api` and `api`, it reads as follow: "the host directory `./api`is mounted as a read-write folder on `/src` in `api`." Bidirectional arrows mean the directory is writable from the container.
If we look at the link between `./etc/api/php-fpm.d` and `api`, it reads as follow: "the host directory `./etc/api/php-fpm.d`is mounted as a read-only folder on `/usr/local/etc/php-fpm.d` in `api`." Unidirectional arrows mean the directory is not writable from the container.
### Dependencies
Dependencies (from `services.<service>.depends_on`) are displayed as dotted arrows pointing to the service that declares the dependencies:
![dependencies](resources/dependencies.png)
If we look at the link between `mysql` and `logs`, it reads as follow: "`mysql` depends on `logs`."
### Ports
Ports (from `services.<service>.ports`) are displayed as circle and are linked to containers using plain arrows pointing to the service that declares the ports:
![ports](resources/ports.png)
If we look at the link between port `2480` and `orientdb`, it reads as follow: "traffic coming to host port `2480` will be routed to port `2480` of `orientdb`."
If we look at the link between port `2580` and `elk`, it reads as follow: "traffix coming to host port `2580` will be routed to port `80` of `elk`."
## Examples
### `dot` renderer
```dot
digraph G {
graph [pad=0.5]
"front" [shape="component"]
"http" [shape="component"]
2380 [shape="circle"]
"ambassador" [shape="component"]
"mysql" [shape="component"]
"orientdb" [shape="component"]
"elk" [shape="component"]
"api" [shape="component"]
"piwik" [shape="component"]
"logs" [shape="component"]
"html" [shape="component"]
2580 [shape="circle"]
2480 [shape="circle"]
"http" -> "front" [style="solid"]
2380 -> "front" [style="solid" label=80]
"mysql" -> "ambassador" [style="solid"]
"orientdb" -> "ambassador" [style="solid"]
"elk" -> "ambassador" [style="solid"]
"api" -> "http" [style="solid"]
"piwik" -> "http" [style="solid"]
"logs" -> "http" [style="dashed"]
"piwik" -> "http" [style="dashed"]
"html" -> "http" [style="dashed"]
"ambassador" -> "api" [style="solid" label="graphdb"]
"ambassador" -> "api" [style="solid" label="reldb"]
"logs" -> "api" [style="dashed"]
"ambassador" -> "logs" [style="solid" label="logstash"]
2580 -> "elk" [style="solid" label=80]
"ambassador" -> "piwik" [style="solid" label="db"]
2480 -> "orientdb" [style="solid"]
}
```
### `image` renderer
![image renderer](resources/image.png)
### `display` renderer
![display renderer](resources/display.png)
## License
The MIT License (MIT)
Copyright (c) 2016 PMSIpilot

24
bin/dcv Normal file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env php
<?php
function resolve(array $components) : string {
return implode(DIRECTORY_SEPARATOR, $components);
}
require_once resolve(
[
__DIR__,
'..',
'vendor',
'autoload.php'
]
);
require_once resolve(
[
__DIR__,
'..',
'src',
'application.php'
]
);
?>

48
bin/kahlan Normal file
View file

@ -0,0 +1,48 @@
#!/usr/bin/env php
<?php
$autoloaders = [];
$vendorDir = 'vendor';
if ($composerPath = realpath(getcwd() . '/composer.json')) {
$composerJson = json_decode(file_get_contents($composerPath), true);
$vendorDir = isset($composerJson['vendor-dir']) ? $composerJson['vendor-dir'] : $vendorDir;
}
if ($relative = realpath(getcwd() . "/{$vendorDir}/autoload.php")) {
$autoloaders[] = include $relative;
}
if (!$absolute = realpath(__DIR__ . '/../../../autoload.php')) {
$absolute = realpath(__DIR__ . '/../vendor/autoload.php');
}
if ($absolute && $relative !== $absolute) {
$autoloaders[] = include $absolute;
}
if (!$autoloaders) {
echo "\033[1;31mYou need to set up the project dependencies using the following commands: \033[0m" . PHP_EOL;
echo 'curl -s http://getcomposer.org/installer | php' . PHP_EOL;
echo 'php composer.phar install' . PHP_EOL;
exit(1);
}
use Kahlan\Box\Box;
use Kahlan\Suite;
use Kahlan\Matcher;
use Kahlan\Cli\Kahlan;
$box = box('kahlan', new Box());
$box->service('suite.global', function() {
return new Suite();
});
$specs = new Kahlan([
'autoloader' => reset($autoloaders),
'suite' => $box->get('suite.global')
]);
$specs->loadConfig($argv);
$specs->run();
exit($specs->status());

29
composer.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "pmsipilot/docker-compose-viz",
"description": "Docker compose graph visualization",
"require": {
"symfony/yaml": "^3.1",
"symfony/console": "^3.1",
"clue/graph": "^0.9",
"graphp/graphviz": "^0.2"
},
"require-dev": {
"crysalead/kahlan": "^2.5"
},
"license": "MIT",
"authors": [
{
"name": "Julien Bianchi",
"email": "julien.bianchi@pmsipilot.com"
}
],
"autoload": {
"files": ["src/functions.php"],
"psr-4": {
"PMSIpilot\\DockerComposeViz\\": "src/"
}
},
"config": {
"bin-dir": "bin/"
}
}

376
composer.lock generated Normal file
View file

@ -0,0 +1,376 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "466baff14323f499d4a7257cdd2805bb",
"content-hash": "4685875c2d24d2ba11ca3ff77293a406",
"packages": [
{
"name": "clue/graph",
"version": "v0.9.0",
"source": {
"type": "git",
"url": "https://github.com/clue/graph.git",
"reference": "0336a4d5229fa61a20ccceaeab25e52ac9542700"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/clue/graph/zipball/0336a4d5229fa61a20ccceaeab25e52ac9542700",
"reference": "0336a4d5229fa61a20ccceaeab25e52ac9542700",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"suggest": {
"graphp/algorithms": "Common graph algorithms, such as Dijkstra and Moore-Bellman-Ford (shortest path), minimum spanning tree (MST), Kruskal, Prim and many more..",
"graphp/graphviz": "GraphViz graph drawing / DOT output"
},
"type": "library",
"autoload": {
"psr-4": {
"Fhaculty\\Graph\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A mathematical graph/network library written in PHP",
"homepage": "https://github.com/clue/graph",
"keywords": [
"edge",
"graph",
"mathematical",
"network",
"vertex"
],
"time": "2015-03-07 18:11:31"
},
{
"name": "graphp/algorithms",
"version": "v0.8.1",
"source": {
"type": "git",
"url": "https://github.com/graphp/algorithms.git",
"reference": "81db4049c35730767ec8f97fb5c4844234b86cef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/graphp/algorithms/zipball/81db4049c35730767ec8f97fb5c4844234b86cef",
"reference": "81db4049c35730767ec8f97fb5c4844234b86cef",
"shasum": ""
},
"require": {
"clue/graph": "~0.9.0|~0.8.0",
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Graphp\\Algorithms\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"description": "Common mathematical graph algorithms",
"homepage": "https://github.com/graphp/algorithms",
"keywords": [
"Graph algorithms",
"dijkstra",
"kruskal",
"minimum spanning tree",
"moore-bellman-ford",
"prim",
"shortest path"
],
"time": "2015-03-08 10:12:01"
},
{
"name": "graphp/graphviz",
"version": "v0.2.1",
"source": {
"type": "git",
"url": "https://github.com/graphp/graphviz.git",
"reference": "2676522dfcd907fd3cb52891ea64a052c4ac4c2a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/graphp/graphviz/zipball/2676522dfcd907fd3cb52891ea64a052c4ac4c2a",
"reference": "2676522dfcd907fd3cb52891ea64a052c4ac4c2a",
"shasum": ""
},
"require": {
"clue/graph": "~0.9.0|~0.8.0",
"graphp/algorithms": "~0.8.0",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Graphp\\GraphViz\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "GraphViz graph drawing for mathematical graph/network",
"homepage": "https://github.com/graphp/graphviz",
"keywords": [
"dot output",
"graph drawing",
"graph image",
"graphviz"
],
"time": "2015-03-08 10:30:28"
},
{
"name": "symfony/console",
"version": "v3.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/f9e638e8149e9e41b570ff092f8007c477ef0ce5",
"reference": "f9e638e8149e9e41b570ff092f8007c477ef0ce5",
"shasum": ""
},
"require": {
"php": ">=5.5.9",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
"psr/log": "~1.0",
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/process": "~2.8|~3.0"
},
"suggest": {
"psr/log": "For using the console logger",
"symfony/event-dispatcher": "",
"symfony/process": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Console\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2016-07-26 08:04:17"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "dff51f72b0706335131b00a7f49606168c582594"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/dff51f72b0706335131b00a7f49606168c582594",
"reference": "dff51f72b0706335131b00a7f49606168c582594",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"time": "2016-05-18 14:26:46"
},
{
"name": "symfony/yaml",
"version": "v3.1.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "1819adf2066880c7967df7180f4f662b6f0567ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac",
"reference": "1819adf2066880c7967df7180f4f662b6f0567ac",
"shasum": ""
},
"require": {
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Yaml\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2016-07-17 14:02:08"
}
],
"packages-dev": [
{
"name": "crysalead/kahlan",
"version": "2.5.4",
"source": {
"type": "git",
"url": "https://github.com/crysalead/kahlan.git",
"reference": "0f5deb7faa3a7a324bf0dc0186190ea18bc1eff3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/crysalead/kahlan/zipball/0f5deb7faa3a7a324bf0dc0186190ea18bc1eff3",
"reference": "0f5deb7faa3a7a324bf0dc0186190ea18bc1eff3",
"shasum": ""
},
"require": {
"php": ">=5.4"
},
"bin": [
"bin/kahlan"
],
"type": "library",
"autoload": {
"psr-4": {
"Kahlan\\": "src/"
},
"files": [
"src/init.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "CrysaLEAD"
}
],
"description": "Behavior-Driven Development (BDD) library.",
"keywords": [
"BDD",
"Behavior-Driven Development",
"Monkey Patching",
"TDD",
"mock",
"stub",
"testing",
"unit test"
],
"time": "2016-06-15 15:07:49"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

BIN
resources/dependencies.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
resources/display.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
resources/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
resources/links.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
resources/ports.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
resources/volumes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

23
spec/fetch-networks.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use function PMSIpilot\DockerComposeViz\fetchNetworks;
require_once __DIR__ . '/../vendor/autoload.php';
describe('Fetching networks', function() {
describe('from a version 1 configuration', function() {
it('should always return an empty array', function() {
$configuration = ['networks' => ['image' => 'bar']];
expect(fetchNetworks($configuration))->toBe([]);
});
});
describe('from a version 2 configuration', function() {
it('should fetch networks from the dedicated section', function() {
$configuration = ['version' => 2, 'networks' => ['foo' => [], 'bar' => []]];
expect(fetchNetworks($configuration))->toBe($configuration['networks']);
});
});
});

23
spec/fetch-services.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use function PMSIpilot\DockerComposeViz\fetchServices;
require_once __DIR__ . '/../vendor/autoload.php';
describe('Fetching services', function() {
describe('from a version 1 configuration', function() {
it('should fetch services from top-level keys', function() {
$configuration = ['foo' => ['image' => 'bar'], 'baz' => ['build' => '.']];
expect(fetchServices($configuration))->toBe($configuration);
});
});
describe('from a version 2 configuration', function() {
it('should fetch services from the dedicated section', function() {
$configuration = ['version' => 2, 'services' => ['foo' => ['image' => 'bar'], 'baz' => ['build' => '.']]];
expect(fetchServices($configuration))->toBe($configuration['services']);
});
});
});

23
spec/fetch-volumes.php Normal file
View file

@ -0,0 +1,23 @@
<?php
use function PMSIpilot\DockerComposeViz\fetchVolumes;
require_once __DIR__ . '/../vendor/autoload.php';
describe('Fetching volumes', function() {
describe('from a version 1 configuration', function() {
it('should always return an empty array', function() {
$configuration = ['volumes' => ['image' => 'bar']];
expect(fetchVolumes($configuration))->toBe([]);
});
});
describe('from a version 2 configuration', function() {
it('should fetch volumes from the dedicated section', function() {
$configuration = ['version' => 2, 'volumes' => ['foo' => [], 'bar' => []]];
expect(fetchVolumes($configuration))->toBe($configuration['volumes']);
});
});
});

View file

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

View file

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

View file

@ -0,0 +1,22 @@
<?php
use function PMSIpilot\DockerComposeViz\readConfiguration;
require_once __DIR__ . '/../vendor/autoload.php';
describe('Reading configuration', function() {
it('should check if file exists', function() {
expect(function() { readConfiguration(uniqid()); })
->toThrow(new InvalidArgumentException());
});
it('should parse YAML and return an array', function() {
expect(readConfiguration(__DIR__.'/fixtures/read-configuration/valid.yml'))
->toBe(['version' => 2, 'services' => ['foo' => ['image' => 'bar']]]);
});
it('should report if YAML is invalid', function() {
expect(function() { readConfiguration(__DIR__.'/fixtures/read-configuration/invalid.json'); })
->toThrow(new InvalidArgumentException());
});
});

94
src/application.php Normal file
View file

@ -0,0 +1,94 @@
<?php
namespace PMSIpilot\DockerComposeViz;
use Graphp\GraphViz\GraphViz;
use Symfony\Component\Console;
use Symfony\Component\Yaml\Yaml;
use function PMSIpilot\DockerComposeViz\{
readConfiguration,
fetchServices,
fetchVolumes,
fetchNetworks,
createGraph,
applyGraphvizStyle
};
$application = new Console\Application();
$application->register('render')
->addArgument('input-file',Console\Input\InputArgument::OPTIONAL, 'Path to a docker compose file', getcwd().DIRECTORY_SEPARATOR.'docker-compose.yml')
->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('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('horizontal', 'r', Console\Input\InputOption::VALUE_NONE, 'Display a horizontal graph')
->setCode(function(Console\Input\InputInterface $input, Console\Output\OutputInterface $output) {
$inputFile = $input->getArgument('input-file');
$outputFormat = $input->getOption('output-format');
$outputFile = $input->getOption('output-file') ?: getcwd().DIRECTORY_SEPARATOR.'docker-compose.'.($outputFormat === 'dot' ? $outputFormat : 'png');
$onlyServices = $input->getOption('only');
if (in_array($outputFormat, ['dot', 'image', 'display']) === false) {
throw new Console\Exception\InvalidArgumentException(sprintf('Invalid output format "%s". It must be one of "dot", "png" or "display".', $outputFormat));
}
if ($outputFormat === 'display') {
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) {
throw new Console\Exception\InvalidArgumentException(sprintf('File "%s" already exists. Use the "--force" option to overwrite it.', $outputFile));
}
}
$configuration = readConfiguration($inputFile);
$services = fetchServices($configuration);
$volumes = fetchVolumes($configuration);
$networks = fetchNetworks($configuration);
if ([] !== $onlyServices) {
$intersect = array_intersect($onlyServices, array_keys($services));
if ($intersect !== $onlyServices) {
throw new Console\Exception\InvalidArgumentException(sprintf('The following services do not exist: "%s"', implode('", "', array_diff($onlyServices, $intersect))));
}
$services = array_filter(
$services,
function($service) use ($onlyServices) {
return in_array($service, $onlyServices);
},
ARRAY_FILTER_USE_KEY
);
}
$graph = applyGraphvizStyle(
createGraph($services, $volumes, $networks, $input->getOption('no-volumes') === false),
$input->getOption('horizontal')
);
switch ($outputFormat) {
case 'dot':
case 'image':
$rendererClass = 'Graphp\GraphViz\\' . ucfirst($outputFormat);
$renderer = new $rendererClass();
file_put_contents($outputFile, $renderer->getOutput($graph));
break;
case 'display':
$renderer = new GraphViz();
$renderer->display($graph);
break;
}
});
$application->run();

424
src/functions.php Normal file
View file

@ -0,0 +1,424 @@
<?php
namespace PMSIpilot\DockerComposeViz;
use Fhaculty\Graph\Edge;
use Fhaculty\Graph\Graph;
use Fhaculty\Graph\Vertex;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
/**
* @public
*
* @param string $path Path to a YAML file
*
* @return array
*/
function readConfiguration(string $path) : array
{
if (file_exists($path) === false) {
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));
}
}
/**
* @public
*
* @param array $configuration Docker compose (version 1 or 2) configuration
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchServices(array $configuration) : array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
return $configuration;
}
return $configuration['services'] ?? [];
}
/**
* @public
*
* @param array $configuration Docker compose (version 1 or 2) configuration
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchVolumes(array $configuration) : array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
return [];
}
return $configuration['volumes'] ?? [];
}
/**
* @public
*
* @param array $configuration Docker compose (version 1 or 2) configuration
*
* @return array List of service definitions exctracted from the configuration
*/
function fetchNetworks(array $configuration) : array
{
if (isset($configuration['version']) === false || (int) $configuration['version'] === 1) {
return [];
}
return $configuration['networks'] ?? [];
}
/**
* @public
*
* @param array $services Docker compose service definitions
* @param array $volumes Docker compose volume definitions
* @param array $networks Docker compose network definitions
* @param bool $withVolumes Create vertices and edges for volumes
*
* @return Graph The complete graph for the given list of services
*/
function createGraph(array $services, array $volumes, array $networks, bool $withVolumes = true) : Graph
{
return makeVerticesAndEdges(new Graph(), $services, $volumes, $networks, $withVolumes);
}
/**
* @public
*
* @param Graph $graph Input graph
* @param bool $horizontal Display a horizontal graph
*
* @return Graph A copy of the input graph with style attributes
*/
function applyGraphvizStyle(Graph $graph, bool $horizontal) : Graph
{
$graph = $graph->createGraphClone();
$graph->setAttribute('graphviz.graph.pad', '0.5');
if ($horizontal === true) {
$graph->setAttribute('graphviz.graph.rankdir', 'LR');
}
foreach ($graph->getVertices() as $vertex) {
switch ($vertex->getAttribute('docker_compose.type')) {
case 'service':
$vertex->setAttribute('graphviz.shape', 'component');
break;
case 'external_service':
$vertex->setAttribute('graphviz.shape', 'component');
$vertex->setAttribute('graphviz.color', 'gray');
break;
case 'volume':
$vertex->setAttribute('graphviz.shape', 'folder');
break;
case 'network':
$vertex->setAttribute('graphviz.shape', 'pentagon');
break;
case 'external_network':
$vertex->setAttribute('graphviz.shape', 'pentagon');
$vertex->setAttribute('graphviz.color', 'gray');
break;
case 'port':
$vertex->setAttribute('graphviz.shape', 'circle');
if (($proto = $vertex->getAttribute('docker_compose.proto')) === 'udp') {
$vertex->setAttribute('graphviz.style', 'dashed');
}
break;
}
}
foreach ($graph->getEdges() as $edge) {
switch ($edge->getAttribute('docker_compose.type')) {
case 'ports':
case 'links':
$edge->setAttribute('graphviz.style', 'solid');
break;
case 'external_links':
$edge->setAttribute('graphviz.style', 'solid');
$edge->setAttribute('graphviz.color', 'gray');
break;
case 'volumes_from':
case 'volumes':
$edge->setAttribute('graphviz.style', 'dashed');
break;
case 'depends_on':
$edge->setAttribute('graphviz.style', 'dotted');
break;
}
if (($alias = $edge->getAttribute('docker_compose.alias')) !== null) {
$edge->setAttribute('graphviz.label', $alias);
}
if ($edge->getAttribute('docker_compose.bidir')) {
$edge->setAttribute('graphviz.dir', 'both');
}
}
return $graph;
}
/**
* @internal
*
* @param Graph $graph Input graph
* @param array $services Docker compose service definitions
* @param array $volumes Docker compose volume definitions
* @param array $networks Docker compose network definitions
* @param bool $withVolumes Create vertices and edges for volumes
*
* @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) : Graph
{
$graph = $graph->createGraphClone();
if ($withVolumes === true) {
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'
);
}
foreach ($services as $service => $definition) {
$vertices[$service] = addService($graph, $service);
foreach ($definition['links'] ?? [] as $link) {
list($target, $alias) = explodeMapping($link);
addRelation(
addService($graph, $target),
$graph->getVertex($service),
'links',
$alias !== $target ? $alias : null
);
}
foreach ($definition['external_links'] ?? [] as $link) {
list($target, $alias) = explodeMapping($link);
addRelation(
addService($graph, $target, 'external_service'),
$graph->getVertex($service),
'external_links',
$alias !== $target ? $alias : null
);
}
foreach ($definition['depends_on'] ?? [] as $dependency) {
addRelation(
$graph->getVertex($service),
addService($graph, $dependency),
'depends_on'
);
}
foreach ($definition['volumes_from'] ?? [] as $source) {
addRelation(
addService($graph, $source),
$graph->getVertex($service),
'volumes_from'
);
}
if ($withVolumes === true) {
foreach ($definition['volumes'] ?? [] as $volume) {
list($host, $container, $attr) = explodeMapping($volume);
if ($host[0] !== '.' && $host[0] !== DIRECTORY_SEPARATOR) {
$host = 'named: '.$host;
}
addRelation(
addVolume($graph, $host),
$graph->getVertex($service),
'volumes',
$host !== $container ? $container : null,
$attr !== 'ro'
);
}
}
foreach ($definition['ports'] ?? [] as $port) {
list($host, $container, $proto) = explodeMapping($port);
addRelation(
addPort($graph, (int) $host, $proto),
$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'] ?? [];
addRelation(
$graph->getVertex($service),
addNetwork($graph, 'net: '.$network),
'networks',
count($aliases) > 0 ? implode(', ', $aliases) : null
);
}
}
return $graph;
}
/**
* @internal
*
* @param Graph $graph Input graph
* @param string $service Service name
* @param string $type Service type
*
* @return Vertex
*/
function addService(Graph $graph, string $service, string $type = null)
{
if ($graph->hasVertex($service) === true) {
return $graph->getVertex($service);
}
$vertex = $graph->createVertex($service);
$vertex->setAttribute('docker_compose.type', $type ?: 'service');
return $vertex;
}
/**
* @internal
*
* @param Graph $graph Input graph
* @param int $port Port number
* @param string|null $proto Protocol
*
* @return Vertex
*/
function addPort(Graph $graph, int $port, string $proto = null)
{
if ($graph->hasVertex($port) === true) {
return $graph->getVertex($port);
}
$vertex = $graph->createVertex($port);
$vertex->setAttribute('docker_compose.type', 'port');
$vertex->setAttribute('docker_compose.proto', $proto ?: 'tcp');
return $vertex;
}
/**
* @internal
*
* @param Graph $graph Input graph
* @param string $path Path
*
* @return Vertex
*/
function addVolume(Graph $graph, string $path)
{
if ($graph->hasVertex($path) === true) {
return $graph->getVertex($path);
}
$vertex = $graph->createVertex($path);
$vertex->setAttribute('docker_compose.type', 'volume');
return $vertex;
}
/**
* @internal
*
* @param Graph $graph Input graph
* @param string $name Name of the network
* @param string $type Network type
*
* @return Vertex
*/
function addNetwork(Graph $graph, string $name, string $type = null)
{
if ($graph->hasVertex($name) === true) {
return $graph->getVertex($name);
}
$vertex = $graph->createVertex($name);
$vertex->setAttribute('docker_compose.type', $type ?: 'network');
return $vertex;
}
/**
* @internal
*
* @param Vertex $from Source vertex
* @param Vertex $to Destination vertex
* @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
*/
function addRelation(Vertex $from, Vertex $to, string $type, string $alias = null, bool $bidirectional = false) : Edge\Directed
{
$edge = $from->createEdgeTo($to);
$edge->setAttribute('docker_compose.type', $type);
if ($alias !== null) {
$edge->setAttribute('docker_compose.alias', $alias);
}
$edge->setAttribute('docker_compose.bidir', $bidirectional);
return $edge;
}
/**
* @internal
*
* @param string $mapping A docker mapping (<from>[:<to>])
*
* @return array An 2 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
{
$parts = explode(':', $mapping);
$parts[1] = $parts[1] ?? $parts[0];
$subparts = array_values(array_filter(explode('/', $parts[1])));
if (count($subparts) > 2) {
$subparts = [$parts[1], $parts[2] ?? null];
}
return [$parts[0], $subparts[0], $subparts[1] ?? null];
}