Skip to content

Commit

Permalink
Dot - group peers in namespace frames, connlist (#280)
Browse files Browse the repository at this point in the history
* code updates - splitting formatters for convenience and group peers by ns for dot

* generating dot new files (make test-update)

* required updates

* Update pkg/netpol/connlist/conns_formatter_dot.go

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

* gofmt err fix

---------

Co-authored-by: Adi Sosnovich <[email protected]>
  • Loading branch information
shireenf-ibm and adisos authored Dec 6, 2023
1 parent c9e1d07 commit 0cd241b
Show file tree
Hide file tree
Showing 28 changed files with 1,214 additions and 1,031 deletions.
10 changes: 6 additions & 4 deletions pkg/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,12 @@ func TestCommands(t *testing.T) {
"dot",
},
expectedOutput: "digraph {\n" +
"\t\"default/checkoutservice[Deployment]\" [label=\"default/checkoutservice[Deployment]\" color=\"blue\" fontcolor=\"blue\"]\n" +
"\t\"default/emailservice[Deployment]\" [label=\"default/emailservice[Deployment]\" color=\"blue\" fontcolor=\"blue\"]\n" +
"\t\"default/checkoutservice[Deployment]\" -> \"default/emailservice[Deployment]\"" +
" [label=\"TCP 8080\" color=\"gold2\" fontcolor=\"darkgreen\"]\n" +
"\tsubgraph cluster_default {\n" +
"\t\t\"checkoutservice[Deployment]\" [label=\"checkoutservice[Deployment]\" color=\"blue\" fontcolor=\"blue\"]\n" +
"\t\t\"emailservice[Deployment]\" [label=\"emailservice[Deployment]\" color=\"blue\" fontcolor=\"blue\"]\n" +
"\t\tlabel=\"default\"\n" +
"\t}\n" +
"\t\"checkoutservice[Deployment]\" -> \"emailservice[Deployment]\" [label=\"TCP 8080\" color=\"gold2\" fontcolor=\"darkgreen\"]\n" +
"}",
exact: true,
isErr: false,
Expand Down
135 changes: 0 additions & 135 deletions pkg/netpol/connlist/conns_formatter.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
package connlist

import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"sort"
"strings"

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

Expand Down Expand Up @@ -52,133 +47,3 @@ func formSingleConn(conn Peer2PeerConnection) singleConnFields {
connStr := common.ConnStrFromConnProperties(conn.AllProtocolsAndPorts(), conn.ProtocolsAndPorts())
return singleConnFields{Src: conn.Src().String(), Dst: conn.Dst().String(), ConnString: connStr}
}

// formatText: implements the connsFormatter interface for txt output format
type formatText struct {
}

// returns a textual string format of connections from list of Peer2PeerConnection objects
func (t formatText) writeOutput(conns []Peer2PeerConnection) (string, error) {
connLines := make([]string, len(conns))
for i := range conns {
connLines[i] = formSingleConn(conns[i]).string()
}
sort.Strings(connLines)
return strings.Join(connLines, newLineChar), nil
}

// formatJSON: implements the connsFormatter interface for JSON output format
type formatJSON struct {
}

// returns a json string form of connections from list of Peer2PeerConnection objects
func (j formatJSON) writeOutput(conns []Peer2PeerConnection) (string, error) {
// get an array of sorted conns items ([]singleConnFields)
sortedConnItems := sortConnections(conns)
jsonConns, err := json.MarshalIndent(sortedConnItems, "", " ")
if err != nil {
return "", err
}
return string(jsonConns), nil
}

// formatDOT: implements the connsFormatter interface for dot output format
type formatDOT struct {
}

// formats an edge line from a singleConnFields struct , to be used for dot graph
func getEdgeLine(c singleConnFields) string {
return fmt.Sprintf("\t%q -> %q [label=%q color=\"gold2\" fontcolor=\"darkgreen\"]", c.Src, c.Dst, c.ConnString)
}

// formats a peer line for dot graph
func getPeerLine(peer eval.Peer) string {
var peerColor string
if peer.IsPeerIPType() {
peerColor = "red2"
} else {
peerColor = "blue"
}
peerName := peer.String()
return fmt.Sprintf("\t%q [label=%q color=%q fontcolor=%q]", peerName, peerName, peerColor, peerColor)
}

// returns a dot string form of connections from list of Peer2PeerConnection objects
func (d formatDOT) writeOutput(conns []Peer2PeerConnection) (string, error) {
edgeLines := make([]string, len(conns)) // list of edges lines
peersVisited := make(map[string]struct{}, 0) // acts as a set
peerLines := make([]string, 0) // list of peers lines
for index := range conns {
connLine := formSingleConn(conns[index])
edgeLines[index] = getEdgeLine(connLine)
if _, ok := peersVisited[connLine.Src]; !ok {
peersVisited[connLine.Src] = struct{}{}
peerLines = append(peerLines, getPeerLine(conns[index].Src()))
}
if _, ok := peersVisited[connLine.Dst]; !ok {
peersVisited[connLine.Dst] = struct{}{}
peerLines = append(peerLines, getPeerLine(conns[index].Dst()))
}
}
// sort graph lines
sort.Strings(peerLines)
sort.Strings(edgeLines)
// collect all lines by order
allLines := []string{common.DotHeader}
allLines = append(allLines, peerLines...)
allLines = append(allLines, edgeLines...)
allLines = append(allLines, common.DotClosing)
return strings.Join(allLines, newLineChar), nil
}

// formatCSV: implements the connsFormatter interface for csv output format
type formatCSV struct {
}

// returns a CSV string form of connections from list of Peer2PeerConnection objects
func (cs formatCSV) writeOutput(conns []Peer2PeerConnection) (string, error) {
// get an array of sorted conns items ([]singleConnFields)
sortedConnItems := sortConnections(conns)
var headerCSV = []string{"src", "dst", "conn"}

// writing csv rows into a buffer
buf := new(bytes.Buffer)
writer := csv.NewWriter(buf)
if err := writer.Write(headerCSV); err != nil {
return "", err
}
for _, conn := range sortedConnItems {
row := []string{conn.Src, conn.Dst, conn.ConnString}
if err := writer.Write(row); err != nil {
return "", err
}
}
writer.Flush()
return buf.String(), nil
}

// formatMD: implements the connsFormatter interface for md output format
type formatMD struct {
}

// formats the md output header
func getMDHeader() string {
return "| src | dst | conn |\n|-----|-----|------|"
}

// formats a connection line for md output
func getMDLine(c singleConnFields) string {
return fmt.Sprintf("| %s | %s | %s |", c.Src, c.Dst, c.ConnString)
}

// returns a md string form of connections from list of Peer2PeerConnection objects
func (md formatMD) writeOutput(conns []Peer2PeerConnection) (string, error) {
mdLines := make([]string, len(conns))
for index := range conns {
mdLines[index] = getMDLine(formSingleConn(conns[index]))
}
sort.Strings(mdLines)
allLines := []string{getMDHeader()}
allLines = append(allLines, mdLines...)
return strings.Join(allLines, newLineChar), nil
}
32 changes: 32 additions & 0 deletions pkg/netpol/connlist/conns_formatter_csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package connlist

import (
"bytes"
"encoding/csv"
)

// formatCSV: implements the connsFormatter interface for csv output format
type formatCSV struct {
}

// returns a CSV string form of connections from list of Peer2PeerConnection objects
func (cs formatCSV) writeOutput(conns []Peer2PeerConnection) (string, error) {
// get an array of sorted conns items ([]singleConnFields)
sortedConnItems := sortConnections(conns)
var headerCSV = []string{"src", "dst", "conn"}

// writing csv rows into a buffer
buf := new(bytes.Buffer)
writer := csv.NewWriter(buf)
if err := writer.Write(headerCSV); err != nil {
return "", err
}
for _, conn := range sortedConnItems {
row := []string{conn.Src, conn.Dst, conn.ConnString}
if err := writer.Write(row); err != nil {
return "", err
}
}
writer.Flush()
return buf.String(), nil
}
125 changes: 125 additions & 0 deletions pkg/netpol/connlist/conns_formatter_dot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package connlist

import (
"fmt"
"sort"
"strings"

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

const (
ipColor = "red2"
nonIPPeerColor = "blue"
)

// formatDOT: implements the connsFormatter interface for dot output format
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)
}

// 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) {
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
}

// 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
}

// 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
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 _, ok := peersVisited[dstStr]; !ok {
peersVisited[dstStr] = struct{}{}
externalDstLine := checkAndAddPeerToNsGroup(nsPeers, conns[index].Dst())
if externalDstLine != "" {
externalPeersLines = append(externalPeersLines, externalDstLine)
}
}
}
// sort graph lines
sort.Strings(edgeLines)
sort.Strings(externalPeersLines)
// collect all lines by order
allLines := []string{common.DotHeader}
allLines = append(allLines, addNsGroups(nsPeers)...)
allLines = append(allLines, externalPeersLines...)
allLines = append(allLines, edgeLines...)
allLines = append(allLines, common.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
}
18 changes: 18 additions & 0 deletions pkg/netpol/connlist/conns_formatter_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package connlist

import "encoding/json"

// formatJSON: implements the connsFormatter interface for JSON output format
type formatJSON struct {
}

// returns a json string form of connections from list of Peer2PeerConnection objects
func (j formatJSON) writeOutput(conns []Peer2PeerConnection) (string, error) {
// get an array of sorted conns items ([]singleConnFields)
sortedConnItems := sortConnections(conns)
jsonConns, err := json.MarshalIndent(sortedConnItems, "", " ")
if err != nil {
return "", err
}
return string(jsonConns), nil
}
33 changes: 33 additions & 0 deletions pkg/netpol/connlist/conns_formatter_md.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package connlist

import (
"fmt"
"sort"
"strings"
)

// formatMD: implements the connsFormatter interface for md output format
type formatMD struct {
}

// formats the md output header
func getMDHeader() string {
return "| src | dst | conn |\n|-----|-----|------|"
}

// formats a connection line for md output
func getMDLine(c singleConnFields) string {
return fmt.Sprintf("| %s | %s | %s |", c.Src, c.Dst, c.ConnString)
}

// returns a md string form of connections from list of Peer2PeerConnection objects
func (md formatMD) writeOutput(conns []Peer2PeerConnection) (string, error) {
mdLines := make([]string, len(conns))
for index := range conns {
mdLines[index] = getMDLine(formSingleConn(conns[index]))
}
sort.Strings(mdLines)
allLines := []string{getMDHeader()}
allLines = append(allLines, mdLines...)
return strings.Join(allLines, newLineChar), nil
}
20 changes: 20 additions & 0 deletions pkg/netpol/connlist/conns_formatter_txt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package connlist

import (
"sort"
"strings"
)

// formatText: implements the connsFormatter interface for txt output format
type formatText struct {
}

// returns a textual string format of connections from list of Peer2PeerConnection objects
func (t formatText) writeOutput(conns []Peer2PeerConnection) (string, error) {
connLines := make([]string, len(conns))
for i := range conns {
connLines[i] = formSingleConn(conns[i]).string()
}
sort.Strings(connLines)
return strings.Join(connLines, newLineChar), nil
}
Loading

0 comments on commit 0cd241b

Please sign in to comment.