Skip to content

Commit 2268d1e

Browse files
BenjaminGuzmanndeloof
authored andcommitted
Started working on viz subcommand
Signed-off-by: Benjamín Guzmán <[email protected]>
1 parent 7b0ed13 commit 2268d1e

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

cmd/compose/alpha.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func alphaCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
3434
cmd.AddCommand(
3535
watchCommand(p, backend),
3636
dryRunRedirectCommand(p),
37+
vizCommand(p),
3738
)
3839
return cmd
3940
}

cmd/compose/viz.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
Copyright 2023 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package compose
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"github.com/compose-spec/compose-go/types"
23+
"github.com/spf13/cobra"
24+
"os"
25+
"strconv"
26+
"strings"
27+
)
28+
29+
type vizOptions struct {
30+
*ProjectOptions
31+
includeNetworks bool
32+
includePorts bool
33+
includeImageName bool
34+
indentationStr string
35+
}
36+
37+
// maps a service with the services it depends on
38+
type vizGraph map[*types.ServiceConfig][]*types.ServiceConfig
39+
40+
func vizCommand(p *ProjectOptions) *cobra.Command {
41+
opts := vizOptions{
42+
ProjectOptions: p,
43+
}
44+
var indentationSize int
45+
var useSpaces bool
46+
47+
cmd := &cobra.Command{
48+
Use: "viz [OPTIONS]",
49+
Short: "EXPERIMENTAL - Generate a graphviz graph from your compose file",
50+
PreRunE: Adapt(func(ctx context.Context, args []string) error {
51+
var err error
52+
opts.indentationStr, err = preferredIndentationStr(indentationSize, useSpaces)
53+
return err
54+
}),
55+
RunE: Adapt(func(ctx context.Context, args []string) error {
56+
return runViz(ctx, &opts)
57+
}),
58+
}
59+
60+
cmd.Flags().BoolVar(&opts.includePorts, "ports", false, "Include service's exposed ports in output graph")
61+
cmd.Flags().BoolVar(&opts.includeNetworks, "networks", false, "Include service's attached networks in output graph")
62+
cmd.Flags().BoolVar(&opts.includeImageName, "image", false, "Include service's image name in output graph")
63+
cmd.Flags().IntVar(&indentationSize, "indentation-size", 1, "Number of tabs or spaces to use for indentation")
64+
cmd.Flags().BoolVar(&useSpaces, "spaces", false, "If given, space character ' ' will be used to indent,\notherwise tab character '\\t' will be used")
65+
return cmd
66+
}
67+
68+
func runViz(ctx context.Context, opts *vizOptions) error {
69+
_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
70+
project, err := opts.ToProject(nil)
71+
if err != nil {
72+
return err
73+
}
74+
75+
// build graph
76+
graph := make(vizGraph)
77+
for i, serviceConfig := range project.Services {
78+
serviceConfigPtr := &project.Services[i]
79+
graph[serviceConfigPtr] = make([]*types.ServiceConfig, 0, len(serviceConfig.DependsOn))
80+
for dependencyName := range serviceConfig.DependsOn {
81+
// no error should be returned since dependencyName should exist
82+
dependency, _ := project.GetService(dependencyName)
83+
graph[serviceConfigPtr] = append(graph[serviceConfigPtr], &dependency)
84+
}
85+
}
86+
87+
// build graphviz graph
88+
var graphBuilder strings.Builder
89+
graphBuilder.WriteString("digraph " + project.Name + " {\n")
90+
graphBuilder.WriteString(opts.indentationStr + "layout=dot;\n")
91+
addNodes(&graphBuilder, graph, opts)
92+
graphBuilder.WriteRune('\n')
93+
addEdges(&graphBuilder, graph, opts)
94+
graphBuilder.WriteString("}\n")
95+
96+
fmt.Println(graphBuilder.String())
97+
98+
return nil
99+
}
100+
101+
// addNodes adds the corresponding graphviz representation of all the nodes in the given graph to the graphBuilder
102+
// returns the same graphBuilder
103+
func addNodes(graphBuilder *strings.Builder, graph vizGraph, opts *vizOptions) *strings.Builder {
104+
for serviceNode := range graph {
105+
// write:
106+
// "service name" [style="filled" label<<font point-size="15">service name</font>
107+
graphBuilder.WriteString(opts.indentationStr)
108+
writeQuoted(graphBuilder, serviceNode.Name)
109+
graphBuilder.WriteString(" [style=\"filled\" label=<<font point-size=\"15\">")
110+
graphBuilder.WriteString(serviceNode.Name)
111+
graphBuilder.WriteString("</font>")
112+
113+
if opts.includeNetworks && len(serviceNode.Networks) > 0 {
114+
graphBuilder.WriteString("<font point-size=\"10\">")
115+
graphBuilder.WriteString("<br/><br/><b>Networks:</b>")
116+
for _, networkName := range serviceNode.NetworksByPriority() {
117+
graphBuilder.WriteString("<br/>")
118+
graphBuilder.WriteString(networkName)
119+
}
120+
graphBuilder.WriteString("</font>")
121+
}
122+
123+
if opts.includePorts && len(serviceNode.Ports) > 0 {
124+
graphBuilder.WriteString("<font point-size=\"10\">")
125+
graphBuilder.WriteString("<br/><br/><b>Ports:</b>")
126+
for _, portConfig := range serviceNode.Ports {
127+
graphBuilder.WriteString("<br/>")
128+
if len(portConfig.HostIP) > 0 {
129+
graphBuilder.WriteString(portConfig.HostIP)
130+
graphBuilder.WriteRune(':')
131+
}
132+
graphBuilder.WriteString(portConfig.Published)
133+
graphBuilder.WriteRune(':')
134+
graphBuilder.WriteString(strconv.Itoa(int(portConfig.Target)))
135+
graphBuilder.WriteString(" (")
136+
graphBuilder.WriteString(portConfig.Protocol)
137+
graphBuilder.WriteString(", ")
138+
graphBuilder.WriteString(portConfig.Mode)
139+
graphBuilder.WriteString(")")
140+
}
141+
graphBuilder.WriteString("</font>")
142+
}
143+
144+
if opts.includeImageName {
145+
graphBuilder.WriteString("<font point-size=\"10\">")
146+
graphBuilder.WriteString("<br/><br/><b>Image:</b><br/>")
147+
graphBuilder.WriteString(serviceNode.Image)
148+
graphBuilder.WriteString("</font>")
149+
}
150+
151+
graphBuilder.WriteString(">];\n")
152+
}
153+
154+
return graphBuilder
155+
}
156+
157+
// addEdges adds the corresponding graphviz representation of all edges in the given graph to the graphBuilder
158+
// returns the same graphBuilder
159+
func addEdges(graphBuilder *strings.Builder, graph vizGraph, opts *vizOptions) *strings.Builder {
160+
for parent, children := range graph {
161+
for _, child := range children {
162+
graphBuilder.WriteString(opts.indentationStr)
163+
writeQuoted(graphBuilder, parent.Name)
164+
graphBuilder.WriteString(" -> ")
165+
writeQuoted(graphBuilder, child.Name)
166+
graphBuilder.WriteString(";\n")
167+
}
168+
}
169+
170+
return graphBuilder
171+
}
172+
173+
// writeQuoted writes "str" to builder
174+
func writeQuoted(builder *strings.Builder, str string) {
175+
builder.WriteRune('"')
176+
builder.WriteString(str)
177+
builder.WriteRune('"')
178+
}
179+
180+
// preferredIndentationStr returns a single string given the indentation preference
181+
func preferredIndentationStr(size int, useSpace bool) (string, error) {
182+
if size < 0 {
183+
return "", fmt.Errorf("invalid indentation size: %d", size)
184+
}
185+
186+
indentationStr := "\t"
187+
if useSpace {
188+
indentationStr = " "
189+
}
190+
return strings.Repeat(indentationStr, size), nil
191+
}

0 commit comments

Comments
 (0)