Initial source import
This commit is contained in:
parent
bf4e1d1fc0
commit
fe46cbd889
24 changed files with 1331 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
vendor/
|
||||||
|
docker.lock
|
17
.travis.yml
Normal file
17
.travis.yml
Normal 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
21
Dockerfile
Normal 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
15
LICENSE
Normal 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
32
Makefile
Normal 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
146
README.md
Normal 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
24
bin/dcv
Normal 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
48
bin/kahlan
Normal 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
29
composer.json
Normal 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
376
composer.lock
generated
Normal 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
BIN
resources/dependencies.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
resources/display.png
Normal file
BIN
resources/display.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 175 KiB |
BIN
resources/image.png
Normal file
BIN
resources/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
resources/links.png
Normal file
BIN
resources/links.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
resources/ports.png
Normal file
BIN
resources/ports.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
resources/volumes.png
Normal file
BIN
resources/volumes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
23
spec/fetch-networks.php
Normal file
23
spec/fetch-networks.php
Normal 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
23
spec/fetch-services.php
Normal 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
23
spec/fetch-volumes.php
Normal 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']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
8
spec/fixtures/read-configuration/invalid.json
vendored
Normal file
8
spec/fixtures/read-configuration/invalid.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"services": {
|
||||||
|
"foo": {
|
||||||
|
"image": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
spec/fixtures/read-configuration/valid.yml
vendored
Normal file
4
spec/fixtures/read-configuration/valid.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
version: 2
|
||||||
|
services:
|
||||||
|
foo:
|
||||||
|
image: bar
|
22
spec/read-configuratoin.php
Normal file
22
spec/read-configuratoin.php
Normal 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
94
src/application.php
Normal 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
424
src/functions.php
Normal 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];
|
||||||
|
}
|
Loading…
Reference in a new issue