Skip to content

Commit

Permalink
Diff dot ns frames (#283)
Browse files Browse the repository at this point in the history
* code-updates

* outputs-update

* doc update

* fix - peer nodes should have the peer.String() (i.e. full name), eliminating the namespace only from the label displayed on the graph.
to avoid wrong edges if different namespaces have peers with similar names

* fixing the doc dot outputs

* Update docs/diff_output.md

Co-authored-by: Adi Sosnovich <[email protected]>

* Update docs/diff_output.md

Co-authored-by: Adi Sosnovich <[email protected]>

* splitting to internal/dotformatting

* make test-update

* Update docs/diff_output.md

Co-authored-by: Adi Sosnovich <[email protected]>

* doc update

* comment update

---------

Co-authored-by: Adi Sosnovich <[email protected]>
  • Loading branch information
shireenf-ibm and adisos authored Dec 18, 2023
1 parent 57631bc commit 5d82e99
Show file tree
Hide file tree
Showing 52 changed files with 2,059 additions and 1,894 deletions.
82 changes: 44 additions & 38 deletions docs/diff_example_svg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 13 additions & 5 deletions docs/diff_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,34 @@ added,0.0.0.0-255.255.255.255,default/backend[Deployment],No Connections,TCP 909
```

Diff output in `dot` format:

In dot output graphs, all the peers of the analyzed cluster are grouped by their namespaces.

```
$ ./bin/k8snetpolicy diff --dir1 tests/netpol-analysis-example-minimal/ --dir2 tests/netpol-diff-example-minimal/ -o dot
digraph {
subgraph cluster_default {
"default/backend[Deployment]" [label="backend[Deployment]" color="blue" fontcolor="blue"]
"default/frontend[Deployment]" [label="frontend[Deployment]" color="blue" fontcolor="blue"]
label="default"
}
"0.0.0.0-255.255.255.255" [label="0.0.0.0-255.255.255.255" color="blue" fontcolor="blue"]
"default/backend[Deployment]" [label="default/backend[Deployment]" color="blue" fontcolor="blue"]
"default/frontend[Deployment]" [label="default/frontend[Deployment]" color="blue" fontcolor="blue"]
"0.0.0.0-255.255.255.255" -> "default/backend[Deployment]" [label="TCP 9090" color="#008000" fontcolor="#008000"]
"0.0.0.0-255.255.255.255" -> "default/frontend[Deployment]" [label="TCP 8080" color="grey" fontcolor="grey"]
"default/frontend[Deployment]" -> "0.0.0.0-255.255.255.255" [label="UDP 53" color="grey" fontcolor="grey"]
"default/frontend[Deployment]" -> "default/backend[Deployment]" [label="TCP 9090,UDP 53 (old: TCP 9090)" color="magenta" fontcolor="magenta"]
"default/frontend[Deployment]" -> "default/backend[Deployment]" [label="TCP 9090,UDP 53 (dir1: TCP 9090)" color="magenta" fontcolor="magenta"]
}
```

`svg` graph from `dot` format output can be produced using `graphviz` as following:

```
$ dot -Tsvg tests/netpol-diff-example-minimal/diff_output_from_netpol-analysis-example-minimal.dot -o tests/netpol-diff-example-minimal/diff_output_from_netpol-analysis-example-minimal.svg
$ dot -Tsvg test_outputs/diff/diff_between_netpol-diff-example-minimal_and_netpol-analysis-example-minimal.dot -O
```
The frames in the graph represent namespaces of the analyzed cluster.


![svg graph](./diff_example_svg.svg)

### Understanding the output
Expand Down
100 changes: 28 additions & 72 deletions pkg/netpol/connlist/conns_formatter_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/np-guard/netpol-analyzer/pkg/netpol/internal/common"
"github.com/np-guard/netpol-analyzer/pkg/netpol/internal/dotformatting"
)

const (
Expand All @@ -20,106 +21,61 @@ type formatDOT struct {
// formats an edge line from a singleConnFields struct , to be used for dot graph
func getEdgeLine(c Peer2PeerConnection) string {
connStr := common.ConnStrFromConnProperties(c.AllProtocolsAndPorts(), c.ProtocolsAndPorts())
srcName, _, _ := peerNameAndColorByType(c.Src())
dstName, _, _ := peerNameAndColorByType(c.Dst())
return fmt.Sprintf("\t%q -> %q [label=%q color=\"gold2\" fontcolor=\"darkgreen\"]", srcName, dstName, connStr)
return fmt.Sprintf("\t%q -> %q [label=%q color=\"gold2\" fontcolor=\"darkgreen\"]", c.Src().String(), c.Dst().String(), connStr)
}

// returns the peer name and color to be represented in the graph, and whether the peer is external to cluster's namespaces
func peerNameAndColorByType(peer Peer) (name, color string, isExternal bool) {
// returns the peer label and color to be represented in the graph, and whether the peer is external to cluster's namespaces
func peerNameAndColorByType(peer Peer) (nameLabel, color string, isExternal bool) {
if peer.IsPeerIPType() {
return peer.String(), ipColor, true
} else if peer.Name() == common.IngressPodName {
return peer.String(), nonIPPeerColor, true
}
return peer.Name() + "[" + peer.Kind() + "]", nonIPPeerColor, false
return dotformatting.NodeClusterPeerLabel(peer.Name(), peer.Kind()), nonIPPeerColor, false
}

// formats a peer line for dot graph
func getPeerLine(peer Peer) (string, bool) {
peerName, peerColor, isExternalPeer := peerNameAndColorByType(peer)
linePrefix := "\t\t"
if isExternalPeer {
linePrefix = "\t"
}
return fmt.Sprintf("%s%q [label=%q color=%q fontcolor=%q]", linePrefix, peerName, peerName, peerColor, peerColor), isExternalPeer
peerNameLabel, peerColor, isExternalPeer := peerNameAndColorByType(peer)
return fmt.Sprintf("\t%q [label=%q color=%q fontcolor=%q]", peer.String(), peerNameLabel, peerColor, peerColor), isExternalPeer
}

// returns a dot string form of connections from list of Peer2PeerConnection objects
func (d formatDOT) writeOutput(conns []Peer2PeerConnection) (string, error) {
nsPeers := make(map[string][]string) // map from namespace to its peers (grouping peers by namespaces)
externalPeersLines := make([]string, 0) // list of peers which are not in a cluster's namespace (will not be grouped)
edgeLines := make([]string, len(conns)) // list of edges lines
peersVisited := make(map[string]struct{}, 0) // acts as a set
nsPeers := make(map[string][]string) // map from namespace to its peers (grouping peers by namespaces)
externalPeersLines := make([]string, 0) // list of peers which are not in a cluster's namespace (will not be grouped)
edgeLines := make([]string, len(conns)) // list of edges lines
peersVisited := make(map[string]bool, 0) // acts as a set
for index := range conns {
srcStr, dstStr := conns[index].Src().String(), conns[index].Dst().String()
edgeLines[index] = getEdgeLine(conns[index])
if _, ok := peersVisited[srcStr]; !ok {
peersVisited[srcStr] = struct{}{}
externalSrcLine := checkAndAddPeerToNsGroup(nsPeers, conns[index].Src())
if externalSrcLine != "" {
externalPeersLines = append(externalPeersLines, externalSrcLine)
if !peersVisited[srcStr] {
peersVisited[srcStr] = true
peerLine, isExternalPeer := getPeerLine(conns[index].Src())
if isExternalPeer { // peer that does not belong to a cluster's namespace (i.e. ip/ ingress-controller)
externalPeersLines = append(externalPeersLines, peerLine)
} else { // add to Ns group
dotformatting.AddPeerToNsGroup(conns[index].Src().Namespace(), peerLine, nsPeers)
}
}
if _, ok := peersVisited[dstStr]; !ok {
peersVisited[dstStr] = struct{}{}
externalDstLine := checkAndAddPeerToNsGroup(nsPeers, conns[index].Dst())
if externalDstLine != "" {
externalPeersLines = append(externalPeersLines, externalDstLine)
if !peersVisited[dstStr] {
peersVisited[dstStr] = true
peerLine, isExternalPeer := getPeerLine(conns[index].Dst())
if isExternalPeer {
externalPeersLines = append(externalPeersLines, peerLine)
} else {
dotformatting.AddPeerToNsGroup(conns[index].Dst().Namespace(), peerLine, nsPeers)
}
}
}
// sort graph lines
sort.Strings(edgeLines)
sort.Strings(externalPeersLines)
// collect all lines by order
allLines := []string{common.DotHeader}
allLines = append(allLines, addNsGroups(nsPeers)...)
allLines := []string{dotformatting.DotHeader}
allLines = append(allLines, dotformatting.AddNsGroups(nsPeers)...)
allLines = append(allLines, externalPeersLines...)
allLines = append(allLines, edgeLines...)
allLines = append(allLines, common.DotClosing)
allLines = append(allLines, dotformatting.DotClosing)
return strings.Join(allLines, newLineChar), nil
}

// checks if the peer is in cluster's namespace, then adds its line to the namespace list in the given map.
// else, returns its line to be added to the external peers lines
func checkAndAddPeerToNsGroup(mapNsToPeers map[string][]string, peer Peer) string {
peerLine, isExternalPeer := getPeerLine(peer)
if !isExternalPeer { // belongs to a cluster's namespace
if _, ok := mapNsToPeers[peer.Namespace()]; !ok {
mapNsToPeers[peer.Namespace()] = []string{}
}
mapNsToPeers[peer.Namespace()] = append(mapNsToPeers[peer.Namespace()], peerLine)
return ""
}
// else case - an external (ip/ ingress-controller) peer
return peerLine
}

func addNsGroups(nsPeersMap map[string][]string) []string {
res := []string{}
// sort namespaces (map's keys) to ensure same output always
nsKeys := sortMapKeys(nsPeersMap)
// write ns groups
for _, ns := range nsKeys {
peersLines := nsPeersMap[ns]
sort.Strings(peersLines)
// create ns subgraph cluster
nsLabel := strings.ReplaceAll(ns, "-", "_") // dot format does not accept "-" in its sub-graphs names (headers)
nsLines := []string{"\tsubgraph cluster_" + nsLabel + " {"} // subgraph header
nsLines = append(nsLines, peersLines...)
nsLines = append(nsLines, "\t\tlabel=\""+ns+"\"", "\t}")
// add ns section to the res
res = append(res, nsLines...)
}
return res
}

func sortMapKeys(nsPeersMap map[string][]string) []string {
keys := make([]string, 0, len(nsPeersMap))
for k := range nsPeersMap {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
Loading

0 comments on commit 5d82e99

Please sign in to comment.