├── .gitignore ├── Dockerfile ├── Dockerfile.build ├── Makefile ├── README.md ├── Vagrantfile ├── build └── PLACEHOLDER └── docker-compose-dot.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | build/* 3 | !build/PLACEHOLDER 4 | docker-compose-dot 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.3 2 | WORKDIR /app 3 | RUN apk add --no-cache openssl ca-certificates 4 | ADD ./build/app /app 5 | 6 | CMD ["/app/app"] 7 | -------------------------------------------------------------------------------- /Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang:1.6 2 | 3 | VOLUME ["/app"] 4 | WORKDIR /go/src/app 5 | ADD . /go/src/app 6 | RUN go get -d && \ 7 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -v -x 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test 2 | IMAGE ?= digibib/docker-compose-dot 3 | 4 | all: reload provision run 5 | 6 | reload: halt up 7 | 8 | halt: 9 | vagrant halt || true 10 | 11 | up: 12 | vagrant up --no-provision 13 | 14 | provision: 15 | sleep 3 && vagrant provision 16 | 17 | run: delete 18 | @echo "======= RUNNING DOCKER-COMPOSE-DOT CONTAINER ======\n" 19 | @vagrant ssh -c 'sudo docker run -d --name docker-compose-dot -p 9999:9999 $(IMAGE)' 20 | 21 | stop: 22 | @echo "======= STOPPING DOCKER-COMPOSE-DOT CONTAINER ======\n" 23 | vagrant ssh -c 'sudo docker stop docker-compose-dot' || true 24 | 25 | delete: stop 26 | @echo "======= DELETING DOCKER-COMPOSE-DOT CONTAINER ======\n" 27 | vagrant ssh -c 'sudo docker rm docker-compose-dot' || true 28 | 29 | test: 30 | vagrant ssh -c 'docker stats --no-stream docker-compose-dot' 31 | 32 | login: # needs EMAIL, PASSWORD, USERNAME 33 | @ vagrant ssh -c 'docker login --email=$(EMAIL) --username=$(USERNAME) --password=$(PASSWORD)' 34 | 35 | TAG = "$(shell git rev-parse HEAD)" 36 | 37 | tag: 38 | vagrant ssh -c 'docker tag -f $(IMAGE) $(IMAGE):$(TAG)' 39 | 40 | push: tag 41 | @echo "======= PUSHING DOCKER_COMPOSE_DOT CONTAINER ======\n" 42 | vagrant ssh -c 'docker push $(IMAGE):$(TAG)' 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-compose-dot 2 | 3 | Generate Graphviz dot files from docker-compose yaml files. 4 | Inspired by https://github.com/abesto/docker-compose-graphviz 5 | Adapted to use docker-compose yaml v2 and table formatting 6 | 7 | ## Usage 8 | 9 | ``` 10 | go get github.com/digibib/docker-compose-dot 11 | ``` 12 | 13 | ``` 14 | Usage: 15 | docker-compose-dot docker-compose.yml 16 | ``` 17 | 18 | ## Docker image use 19 | 20 | ``` 21 | export TAG=21af6b4fd714903cebd3d4658ad35da4d0db0051 22 | ``` 23 | 24 | ``` 25 | docker pull digibib/docker-compose-dot:$TAG 26 | ``` 27 | 28 | converting a docker-compose.yml in the current dir: 29 | 30 | ``` 31 | docker run --rm -v $(pwd):/tmp digibib/docker-compose-dot:$(TAG) ./app /tmp/docker-compose.yml 2> /dev/null 1> docker-compose.dot 32 | ``` 33 | 34 | You will need the Graphviz package to convert dot to image formats. 35 | 36 | #### MIT License 37 | 38 | Copyright © 2016 Oslo Public Library 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining 41 | a copy of this software and associated documentation files (the 42 | 'Software'), to deal in the Software without restriction, including 43 | without limitation the rights to use, copy, modify, merge, publish, 44 | distribute, sublicense, and/or sell copies of the Software, and to 45 | permit persons to whom the Software is furnished to do so, subject to 46 | the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be 49 | included in all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 52 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 53 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 54 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 55 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 56 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 57 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 58 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | 10 | config.vm.box = "dduportal/boot2docker" 11 | config.vm.network :forwarded_port, guest: 9999, host: 9999 12 | config.vm.provision :docker do |d| 13 | 14 | config.vm.provider "virtualbox" do |vb| 15 | vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"] 16 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"] 17 | end 18 | 19 | # BUILD APP 20 | d.build_image "/vagrant", 21 | args: "-t digibib/build -f /vagrant/Dockerfile.build" 22 | 23 | # COPY COMPILED APP 24 | d.run "builder", 25 | image: "digibib/build", 26 | daemonize: false, 27 | restart: false, 28 | args: "--rm -v /vagrant/build:/app", 29 | cmd: "cp app /app" 30 | end 31 | 32 | config.vm.provision :docker do |d| 33 | # BUILD MINIMAL DOCKER IMAGE WITH COMPILED APP 34 | d.build_image "/vagrant", 35 | args: "-t digibib/docker-compose-dot -f /vagrant/Dockerfile" 36 | 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /build/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digibib/docker-compose-dot/4cac844c9651a401e88e2eee6b3c053504edda1a/build/PLACEHOLDER -------------------------------------------------------------------------------- /docker-compose-dot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "log" 10 | 11 | "github.com/awalterschulze/gographviz" 12 | yaml "gopkg.in/yaml.v2" 13 | ) 14 | 15 | type config struct { 16 | Version string 17 | Networks map[string]network 18 | Volumes map[string]volume 19 | Services map[string]service 20 | } 21 | 22 | type network struct { 23 | Driver, External string 24 | DriverOpts map[string]string "driver_opts" 25 | } 26 | 27 | type volume struct { 28 | Driver, External string 29 | DriverOpts map[string]string "driver_opts" 30 | } 31 | 32 | type MapOrArrayWrapper []string 33 | 34 | type service struct { 35 | ContainerName string "container_name" 36 | Image string 37 | Networks, Ports, Volumes, Command, Links []string 38 | VolumesFrom []string "volumes_from" 39 | DependsOn []string "depends_on" 40 | CapAdd []string "cap_add" 41 | Build struct{ Context, Dockerfile string } 42 | Environment MapOrArrayWrapper 43 | } 44 | 45 | func (w *MapOrArrayWrapper) UnmarshalYAML(unmarshal func(interface{}) error) error { 46 | var envsArray []string 47 | var envsMap map[string]string 48 | if err := unmarshal(&envsMap); err == nil { 49 | for key, val := range envsMap { 50 | envsArray = append(envsArray, key + "=" + val) 51 | } 52 | } 53 | 54 | if len(envsArray) == 0 { 55 | if err := unmarshal(&envsArray); err != nil { 56 | return err 57 | } 58 | } 59 | *w = envsArray 60 | return nil 61 | } 62 | 63 | func nodify(s string) string { 64 | return strings.Replace(s, "-", "_", -1) 65 | } 66 | 67 | func main() { 68 | var ( 69 | bytes []byte 70 | err error 71 | graph *gographviz.Graph 72 | project string 73 | ) 74 | 75 | if len(os.Args) < 2 { 76 | log.Fatal("Need input file!") 77 | } 78 | 79 | bytes, err = ioutil.ReadFile(os.Args[1]) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | // Parse it as YML 85 | data := &config{} 86 | err = yaml.Unmarshal(bytes, &data) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | // Create directed graph 92 | graph = gographviz.NewGraph() 93 | graph.SetName(project) 94 | graph.SetDir(true) 95 | 96 | // Add legend 97 | graph.AddSubGraph(project, "cluster_legend", map[string]string{"label": "Legend"}) 98 | graph.AddNode("cluster_legend", "legend_service", 99 | map[string]string{"shape": "plaintext", 100 | "label": "<" + 101 | "" + 102 | "" + 103 | "" + 104 | "" + 105 | "
container_name
ports ext:int
volumes host:container
environment
>", 106 | }) 107 | 108 | /** NETWORK NODES **/ 109 | for name := range data.Networks { 110 | graph.AddNode(project, nodify(name), map[string]string{ 111 | "label": fmt.Sprintf("\"Network: %s\"", name), 112 | "style": "filled", 113 | "shape": "box", 114 | "fillcolor": "palegreen", 115 | }) 116 | } 117 | 118 | /** SERVICE NODES **/ 119 | for name, service := range data.Services { 120 | var attrs = map[string]string{"shape": "plaintext", "label": "<"} 121 | attrs["label"] += fmt.Sprintf("", name) 122 | 123 | if service.Ports != nil { 124 | for _, port := range service.Ports { 125 | attrs["label"] += fmt.Sprintf("", port) 126 | } 127 | } 128 | if service.Volumes != nil { 129 | for _, vol := range service.Volumes { 130 | attrs["label"] += fmt.Sprintf("", vol) 131 | } 132 | } 133 | if service.Environment != nil { 134 | for _, v := range service.Environment { 135 | attrs["label"] += fmt.Sprintf("", v) 136 | } 137 | } 138 | attrs["label"] += "
%s
%s
%s
%s
>" 139 | graph.AddNode(project, nodify(name), attrs) 140 | } 141 | /** EDGES **/ 142 | for name, service := range data.Services { 143 | // Links to networks 144 | if service.Networks != nil { 145 | for _, linkTo := range service.Networks { 146 | if strings.Contains(linkTo, ":") { 147 | linkTo = strings.Split(linkTo, ":")[0] 148 | } 149 | graph.AddEdge(nodify(name), nodify(linkTo), true, 150 | map[string]string{"dir": "none"}) 151 | } 152 | } 153 | // volumes_from 154 | if service.VolumesFrom != nil { 155 | for _, linkTo := range service.VolumesFrom { 156 | graph.AddEdge(nodify(name), nodify(linkTo), true, 157 | map[string]string{"style": "dashed", "label": "volumes_from"}) 158 | } 159 | } 160 | // depends_on 161 | if service.DependsOn != nil { 162 | for _, linkTo := range service.DependsOn { 163 | graph.AddEdge(nodify(name), nodify(linkTo), true, 164 | map[string]string{"style": "dashed", "label": "depends_on"}) 165 | } 166 | } 167 | // links 168 | if service.Links != nil { 169 | for _, linkTo := range service.Links { 170 | if strings.Contains(linkTo, ":") { 171 | linkTo = strings.Split(linkTo, ":")[0] 172 | } 173 | graph.AddEdge(nodify(name), nodify(linkTo), true, 174 | map[string]string{"style": "dashed", "label": "links"}) 175 | } 176 | } 177 | } 178 | fmt.Print(graph) 179 | } 180 | --------------------------------------------------------------------------------