docker-network-graph-go/main.go

363 lines
8 KiB
Go
Raw Permalink Normal View History

2024-12-09 18:14:45 +01:00
package main
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/goccy/go-graphviz"
"github.com/goccy/go-graphviz/cgraph"
"golang.org/x/net/context"
)
// COLORS enthält eine Liste von Farben für die Netzwerke
var COLORS = []string{
"#1f78b4", "#33a02c", "#e31a1c", "#ff7f00", "#6a3d9a", "#b15928",
"#a6cee3", "#b2df8a", "#fdbf6f", "#cab2d6", "#90f530", "#0d8bad",
"#e98420", "#0e9997", "#6a5164", "#afa277", "#149ead", "#a54a56",
}
var i = 0
// Network repräsentiert ein Docker-Netzwerk
type Network struct {
Name string
Gateway string
Internal bool
Isolated bool
Color string
}
// Interface repräsentiert eine Netzwerkschnittstelle eines Containers
type Interface struct {
EndpointID string
Address string
Aliases []string
}
// Port repräsentiert einen Port eines Containers
type Port struct {
Port string
}
// Container repräsentiert einen Docker-Container
type Container struct {
ContainerID string
Name string
Interfaces []Interface
Ports []Port
}
// Link repräsentiert eine Verbindung zwischen einem Container und einem Netzwerk
type Link struct {
ContainerID string
EndpointID string
NetworkName string
}
func getUniqueColor() string {
if i < len(COLORS) {
c := COLORS[i]
i++
return c
}
return fmt.Sprintf("#%06x", rand.Intn(0xFFFFFF))
}
func getNetworks(cli *client.Client, verbose bool) (map[string]Network, error) {
networks := make(map[string]Network)
ctx := context.Background()
netList, err := cli.NetworkList(ctx, types.NetworkListOptions{})
if err != nil {
return nil, err
}
for _, net := range netList {
gateway := ""
if len(net.IPAM.Config) > 0 {
gateway = net.IPAM.Config[0].Subnet
}
if gateway == "" {
continue
}
internal := net.Internal
isolated := false
if val, ok := net.Options["com.docker.network.bridge.enable_icc"]; ok && val == "false" {
isolated = true
}
if verbose {
fmt.Printf("Network: %s %s %s gw:%s\n", net.Name,
map[bool]string{true: "internal", false: ""}[internal],
map[bool]string{true: "isolated", false: ""}[isolated],
gateway)
}
color := getUniqueColor()
networks[net.Name] = Network{net.Name, gateway, internal, isolated, color}
}
networks["host"] = Network{"host", "0.0.0.0", false, false, "#808080"}
return networks, nil
}
func getContainers(cli *client.Client, verbose bool) ([]Container, []Link, error) {
var containers []Container
var links []Link
ctx := context.Background()
contList, err := cli.ContainerList(ctx, container.ListOptions{})
if err != nil {
return nil, nil, err
}
for _, cont := range contList {
var interfaces []Interface
var ports []Port
contInspect, err := cli.ContainerInspect(ctx, cont.ID)
if err != nil {
return nil, nil, err
}
for portName := range contInspect.NetworkSettings.Ports {
ports = append(ports, Port{portName.Port()})
}
for netName, netInfo := range contInspect.NetworkSettings.Networks {
aliases := []string{}
for _, alias := range netInfo.Aliases {
if alias != cont.ID[:12] && alias != cont.Names[0][1:] {
aliases = append(aliases, alias)
}
}
interfaces = append(interfaces, Interface{
EndpointID: netInfo.EndpointID,
Address: netInfo.IPAddress,
Aliases: aliases,
})
links = append(links, Link{
ContainerID: cont.ID,
EndpointID: netInfo.EndpointID,
NetworkName: netName,
})
}
if verbose {
fmt.Printf("Container: %s %v %s\n", cont.Names[0][1:], ports, interfaces)
}
containers = append(containers, Container{
ContainerID: cont.ID,
Name: cont.Names[0][1:],
Interfaces: interfaces,
Ports: ports,
})
}
return containers, links, nil
}
func drawNetwork(graph *cgraph.Graph, net Network) error {
label := fmt.Sprintf("{%s", net.Name)
if net.Internal {
label += " | Internal"
}
if net.Isolated {
label += " | Containers isolated"
}
label += "}"
n, err := graph.CreateNodeByName(net.Name)
if err != nil {
return err
}
n.SetShape(cgraph.BoxShape)
n.SetLabel(label)
n.SetColor(net.Color + "60")
n.SetStyle("rounded")
return nil
}
func drawContainer(graph *cgraph.Graph, c Container) error {
var ifaceLabels []string
var portLabels []string
for _, port := range c.Ports {
portLabels = append(portLabels, fmt.Sprintf("{%s}}", port.Port))
}
for _, iface := range c.Interfaces {
ifaceLabel := "{"
for _, alias := range iface.Aliases {
ifaceLabel += fmt.Sprintf(" %s |", alias)
}
ifaceLabel += fmt.Sprintf("<%s> %s }}", iface.EndpointID, iface.Address)
ifaceLabels = append(ifaceLabels, ifaceLabel)
}
label := fmt.Sprintf("{{ %s ", c.Name)
if len(portLabels) > 0 {
label += fmt.Sprintf("| {{ %s }} ", strings.Join(portLabels, " | "))
}
label += fmt.Sprintf("| {{ %s }} }}", strings.Join(ifaceLabels, " | "))
n, err := graph.CreateNodeByName(c.ContainerID)
if err != nil {
return err
}
n.SetShape(cgraph.BoxShape)
n.SetLabel(label)
n.SetFillColor("#cdcdcd")
n.SetStyle("filled")
return nil
}
func drawLink(graph *cgraph.Graph, networks map[string]Network, link Link) error {
style := cgraph.SolidEdgeStyle
if networks[link.NetworkName].Isolated {
style = cgraph.DashedEdgeStyle
} else if networks[link.NetworkName].Name == "host" {
style = cgraph.BoldEdgeStyle
}
n, err := graph.NodeByName(link.ContainerID)
if err != nil {
return err
}
m, err := graph.NodeByName(link.NetworkName)
if err != nil {
return err
}
e, err := graph.CreateEdgeByName("", n, m)
if err != nil {
return err
}
e.SetColor(networks[link.NetworkName].Color)
e.SetStyle(style)
return nil
}
func generateGraph(verbose bool, file string, url bool) error {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return err
}
networks, err := getNetworks(cli, verbose)
if err != nil {
return err
}
containers, links, err := getContainers(cli, verbose)
if err != nil {
return err
}
ctx := context.Background()
g, err := graphviz.New(ctx)
if err != nil {
return err
}
graph, err := g.Graph()
if err != nil {
return err
}
graph.SetLayout("sfdp")
graph.SetRankDir("LR")
//graph.SetBackground("transparent")
for _, network := range networks {
if err := drawNetwork(graph, network); err != nil {
return err
}
}
for _, container := range containers {
if err := drawContainer(graph, container); err != nil {
return err
}
}
for _, link := range links {
if link.NetworkName != "none" {
if err := drawLink(graph, networks, link); err != nil {
return err
}
}
}
for _, network := range networks {
if !network.Internal && network.Name != "host" {
startNode, err := graph.CreateNodeByName("start")
if err != nil {
return err
}
endNode, err := graph.CreateNodeByName("end")
if err != nil {
return err
}
e, err := graph.CreateEdgeByName("e", startNode, endNode)
if err != nil {
return err
}
e.SetColor("#808080")
e.SetStyle("dotted")
}
}
if file != "" {
if err := g.RenderFilename(ctx, graph, graphviz.Format(filepath.Ext(file)[1:]), file); err != nil {
return err
}
} else if url {
// URL-Generierung ist in dieser Go-Version nicht implementiert
fmt.Println("URL generation is not implemented in this Go version")
} else {
if err := g.Render(ctx, graph, graphviz.Format("dot"), os.Stdout); err != nil {
return err
}
}
return nil
}
func main() {
verbose := false
file := ""
url := false
// Einfache Argumentverarbeitung (kann durch ein Argument-Parsing-Paket ersetzt werden)
for i := 1; i < len(os.Args); i++ {
switch os.Args[i] {
case "-v", "--verbose":
verbose = true
case "-o", "--out":
if i+1 < len(os.Args) {
file = os.Args[i+1]
i++
}
case "-u", "--url":
url = true
}
}
if err := generateGraph(verbose, file, url); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}