diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index b06a35ede..57e0ec387 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: - go-version: 1.21 + go-version-file: ./go.mod - name: Build env: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index f2e412c7b..de2088160 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -11,8 +11,8 @@ jobs: name: golangci-lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe with: - go-version: 1.21 - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + go-version-file: ./go.mod - uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc diff --git a/cmd/analyzer/main_test.go b/cmd/analyzer/main_test.go new file mode 100644 index 000000000..6f39b4c4b --- /dev/null +++ b/cmd/analyzer/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "strings" + "testing" +) + +func Test_main(t *testing.T) { + tests := []struct { + name string + args string + }{ + {"drawio_multi_vpc", "-output-file aaa.drawio -vpc-config ../../pkg/ibmvpc/examples/input_multiple_vpcs.json -format drawio"}, + {"txt_multi_vpc", "-output-file aaa.txt -vpc-config ../../pkg/ibmvpc/examples/input_multiple_vpcs.json -format txt"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := _main(strings.Split(tt.args, " ")); err != nil { + t.Errorf("_main(), name %s, error = %v", tt.name, err) + } + }) + } +} diff --git a/pkg/common/genericSet.go b/pkg/common/genericSet.go new file mode 100644 index 000000000..9435fbf76 --- /dev/null +++ b/pkg/common/genericSet.go @@ -0,0 +1,53 @@ +package common + +import ( + "fmt" + "reflect" + "sort" + "strings" +) + +// ////////////////////////////////////////////////////////////////////////////////////////////// + +// a genericSet is a generic implementation of a set. +// the main functionality of genericSet is asKey() - conversion to a string. +// asKey() is needed for using genericSet as a key of a map +// ////////////////////////////////////////////////////////////////////////////////////////////// + +type GenericSet[T comparable] map[T]bool +type SetAsKey string + +func (s GenericSet[T]) AsKey() SetAsKey { + ss := []string{} + for i := range s { + key := "" + rv := reflect.ValueOf(i) + if rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface { + key = fmt.Sprintf("%x", rv.Pointer()) + } else { + key = fmt.Sprint(i) + } + ss = append(ss, key) + } + sort.Strings(ss) + return SetAsKey(strings.Join(ss, ",")) +} + +func (s GenericSet[T]) AsList() []T { + keys := make([]T, len(s)) + i := 0 + for k := range s { + keys[i] = k + i++ + } + return keys +} + +func (s GenericSet[T]) IsIntersect(s2 GenericSet[T]) bool { + for i := range s { + if (s2)[i] { + return true + } + } + return false +} diff --git a/pkg/drawio/abstractTreeNode.go b/pkg/drawio/abstractTreeNode.go index b37dec67d..19c5d52fc 100644 --- a/pkg/drawio/abstractTreeNode.go +++ b/pkg/drawio/abstractTreeNode.go @@ -15,14 +15,15 @@ const ( ) type abstractTreeNode struct { - id uint - x int - y int - name string - width int - height int - parent TreeNodeInterface - location *Location + id uint + x int + y int + name string + width int + height int + parent TreeNodeInterface + location *Location + doNotShowInDrawio bool } func (tn *abstractTreeNode) Label() string { return tn.name } @@ -51,7 +52,8 @@ func (tn *abstractTreeNode) DrawioParent() TreeNodeInterface { return tn.parent func (tn *abstractTreeNode) setLocation(location *Location) { tn.location = location } func (tn *abstractTreeNode) setParent(p TreeNodeInterface) { tn.parent = p } -func (tn *abstractTreeNode) NotShownInDrawio() bool { return false } +func (tn *abstractTreeNode) NotShownInDrawio() bool { return tn.doNotShowInDrawio } +func (tn *abstractTreeNode) SetNotShownInDrawio() { tn.doNotShowInDrawio = true } var idCounter uint = minID diff --git a/pkg/drawio/createMapFile.go b/pkg/drawio/createMapFile.go index 205f742fc..0562457e4 100644 --- a/pkg/drawio/createMapFile.go +++ b/pkg/drawio/createMapFile.go @@ -42,13 +42,17 @@ func (data *drawioData) ElementComment(tn TreeNodeInterface) string { // (the last in the file will be on top in the canvas) // 1. we put the lines at the top so they will overlap the icons // 2. we put the icons above the squares so we can mouse over it for tooltips -// 3. we put the sgs in the bottom. if a sg is above a square, it will block the the tooltip of the children of the square. +// 3. we put the sgs and the gs in the bottom. +// (if a sg ot a gs is above a square, it will block the the tooltip of the children of the square.) func orderNodesForDrawio(nodes []TreeNodeInterface) []TreeNodeInterface { - var sg, sq, ln, ic, orderedNodes []TreeNodeInterface + var sg, sq, ln, ic, gs, orderedNodes []TreeNodeInterface for _, tn := range nodes { switch { case reflect.TypeOf(tn).Elem() == reflect.TypeOf(PartialSGTreeNode{}): sg = append(sg, tn) + case tn.IsSquare() && tn.(SquareTreeNodeInterface).IsGroupingSquare(), + tn.IsSquare() && tn.(SquareTreeNodeInterface).IsGroupSubnetsSquare(): + gs = append(gs, tn) case tn.IsSquare(): sq = append(sq, tn) case tn.IsIcon(): @@ -57,6 +61,7 @@ func orderNodesForDrawio(nodes []TreeNodeInterface) []TreeNodeInterface { ln = append(ln, tn) } } + orderedNodes = append(orderedNodes, gs...) orderedNodes = append(orderedNodes, sg...) orderedNodes = append(orderedNodes, sq...) orderedNodes = append(orderedNodes, ic...) @@ -64,8 +69,8 @@ func orderNodesForDrawio(nodes []TreeNodeInterface) []TreeNodeInterface { return orderedNodes } -func CreateDrawioConnectivityMapFile(network SquareTreeNodeInterface, outputFile string) error { - newLayout(network).layout() +func CreateDrawioConnectivityMapFile(network SquareTreeNodeInterface, outputFile string, subnetMode bool) error { + newLayout(network, subnetMode).layout() return writeDrawioFile(NewDrawioData(network), outputFile) } diff --git a/pkg/drawio/drawio_test.go b/pkg/drawio/drawio_test.go index f719fb6a9..cd1d371ae 100644 --- a/pkg/drawio/drawio_test.go +++ b/pkg/drawio/drawio_test.go @@ -8,31 +8,37 @@ import ( func TestWithParsing(t *testing.T) { n := createNetwork() - err := CreateDrawioConnectivityMapFile(n, "fake.drawio") + err := CreateDrawioConnectivityMapFile(n, "fake.drawio", false) if err != nil { fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) } n = createNetwork2() - err = CreateDrawioConnectivityMapFile(n, "fake2.drawio") + err = CreateDrawioConnectivityMapFile(n, "fake2.drawio", false) if err != nil { fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) } n = createNetworkGrouping() - err = CreateDrawioConnectivityMapFile(n, "grouping.drawio") + err = CreateDrawioConnectivityMapFile(n, "grouping.drawio", false) if err != nil { fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) } + n = createNetworkSubnetGrouping() + err = CreateDrawioConnectivityMapFile(n, "subnetGrouping.drawio", true) + if err != nil { + fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) + } + n2 := NewNetworkTreeNode() NewCloudTreeNode(n2, "empty Cloud") NewPublicNetworkTreeNode(n2) NewCloudTreeNode(n2, "empty cloud2") - err = CreateDrawioConnectivityMapFile(n2, "fake3.drawio") + err = CreateDrawioConnectivityMapFile(n2, "fake3.drawio", false) if err != nil { fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) } n = createNetworkAllTypes() - err = CreateDrawioConnectivityMapFile(n, "all.drawio") + err = CreateDrawioConnectivityMapFile(n, "all.drawio", false) if err != nil { fmt.Println("Error when calling CreateDrawioConnectivityMapFile():", err) } @@ -278,6 +284,59 @@ func createNetworkAllTypes() SquareTreeNodeInterface { return network } +func createZone(zones *[][]SquareTreeNodeInterface, vpc *VpcTreeNode, size int, name string) { + zone := NewZoneTreeNode(vpc, name) + subnets := make([]SquareTreeNodeInterface, size) + *zones = append(*zones, subnets) + for i := 0; i < size; i++ { + sname := fmt.Sprint(name, i) + subnets[i] = NewSubnetTreeNode(zone, sname, "", "") + } +} +func createGroup(zones *[][]SquareTreeNodeInterface, vpc *VpcTreeNode, i1, i2, j1, j2 int) SquareTreeNodeInterface { + gr := []SquareTreeNodeInterface{} + for i := i1; i <= i2; i++ { + for j := j1; j <= j2; j++ { + gr = append(gr, (*zones)[i][j]) + } + } + g := GroupedSubnetsSquare(vpc, gr) + g.(*GroupSubnetsSquareTreeNode).name = fmt.Sprintf("%d-%d,%d,%d", i1, i2, j1, j2) + return g +} + +func createNetworkSubnetGrouping() SquareTreeNodeInterface { + network := NewNetworkTreeNode() + zones := &[][]SquareTreeNodeInterface{} + cloud1 := NewCloudTreeNode(network, "IBM Cloud") + publicNetwork := NewPublicNetworkTreeNode(network) + vpc1 := NewVpcTreeNode(cloud1, "vpc1") + for i := 0; i < 10; i++ { + createZone(zones, vpc1, 8, fmt.Sprintf("z%d", i)) + } + groups := []SquareTreeNodeInterface{ + createGroup(zones, vpc1, 0, 0, 0, 1), + createGroup(zones, vpc1, 1, 1, 0, 1), + createGroup(zones, vpc1, 0, 2, 0, 6), + createGroup(zones, vpc1, 0, 2, 4, 6), + createGroup(zones, vpc1, 3, 3, 1, 2), + createGroup(zones, vpc1, 2, 3, 1, 2), + createGroup(zones, vpc1, 0, 4, 0, 3), + createGroup(zones, vpc1, 0, 5, 0, 3), + + createGroup(zones, vpc1, 6, 7, 0, 1), + createGroup(zones, vpc1, 6, 6, 2, 3), + createGroup(zones, vpc1, 7, 8, 1, 2), + } + NewConnectivityLineTreeNode(network, groups[0], groups[len(groups)-1], true, "gconn") + + for _, gr := range groups { + i1 := NewInternetTreeNode(publicNetwork, "I "+gr.Label()) + NewConnectivityLineTreeNode(network, gr, i1, true, "gconn "+gr.Label()) + } + return network +} + func createNetworkGrouping() SquareTreeNodeInterface { network := NewNetworkTreeNode() publicNetwork := NewPublicNetworkTreeNode(network) diff --git a/pkg/drawio/layout.go b/pkg/drawio/layout.go index 787bbe011..7d448affc 100644 --- a/pkg/drawio/layout.go +++ b/pkg/drawio/layout.go @@ -10,6 +10,7 @@ import ( // the input to the layout algorithm is the tree itself. the output is the geometry for each node in the drawio (x, y, height, width) // the steps: // 1. create a 2D matrix - for each subnet icon, it set the location in the matrix (see details about layouting a subnet) +// (when we are in subnet mode, we set the locations of the subnets, using the subnetsLayout struct. see subnetsLayout.go // 2. set the locations of the SG in the matrix, according to the locations of the icons // 3. add squares borders - resizing the matrix. adding rows and columns to the matrix, to be used as the borders of all squares // 4. set the locations of all the squares in the matrix @@ -55,18 +56,24 @@ const ( ) type layoutS struct { - network SquareTreeNodeInterface - matrix *layoutMatrix + network SquareTreeNodeInterface + matrix *layoutMatrix + subnetMode bool } -func newLayout(network SquareTreeNodeInterface) *layoutS { - return &layoutS{network: network, matrix: newLayoutMatrix()} +func newLayout(network SquareTreeNodeInterface, subnetMode bool) *layoutS { + return &layoutS{network: network, matrix: newLayoutMatrix(), subnetMode: subnetMode} } func (ly *layoutS) layout() { // main layout algorithm: // 1. create a 2D matrix - for each subnet icon, it set the location in the matrix - ly.layoutSubnetsIcons() + // in case of subnet mode, set the locations of the subnets + if !ly.subnetMode { + ly.layoutSubnetsIcons() + } else { + ly.layoutSubnets() + } ly.matrix.removeUnusedLayers() // 2. set the locations of the SG in the matrix, according to the locations of the icons ly.setSGLocations() @@ -80,7 +87,9 @@ func (ly *layoutS) layout() { // 6. set the geometry for each node in the drawio ly.matrix.setLayersDistance() ly.setGeometries() - newLayoutOverlap(ly.network).fixOverlapping() + if !ly.subnetMode { + newLayoutOverlap(ly.network).fixOverlapping() + } } // setDefaultLocation() set locations to squares @@ -275,6 +284,9 @@ func (ly *layoutS) layoutSubnetsIcons() { for _, group := range groups { rowIndex, colIndex = ly.layoutGroupIcons(group, rowIndex, colIndex) } + if rowIndex == subnet.Location().firstRow.index { + rowIndex++ + } } colIndex++ } @@ -288,6 +300,111 @@ func (ly *layoutS) layoutSubnetsIcons() { } } +func (ly *layoutS) layoutSubnets() { + sly := newSubnetsLayout(ly.network) + sly.layout() + ly.setSubnetsLocations(sly.subnetMatrix, sly.zonesCol) +} + +func (ly *layoutS) setSubnetsLocations(subnetMatrix [][]TreeNodeInterface, zonesCol map[TreeNodeInterface]int) { + locatedSubnets := map[TreeNodeInterface]bool{} + for ri, row := range subnetMatrix { + for ci, s := range row { + if s != nil && s != fakeSubnet { + ly.setDefaultLocation(s.(SquareTreeNodeInterface), ri, ci) + locatedSubnets[s] = true + } + } + } + ly.setDefaultLocation(ly.network, 0, 0) + for _, cloud := range ly.network.(*NetworkTreeNode).clouds { + for _, vpc := range cloud.(*CloudTreeNode).vpcs { + for _, zone := range vpc.(*VpcTreeNode).zones { + if _, ok := zonesCol[zone]; !ok { + zonesCol[zone] = len(zonesCol) + } + rowIndex := 0 + for _, subnet := range zone.(*ZoneTreeNode).subnets { + if !locatedSubnets[subnet] { + a := len(subnetMatrix) + for rowIndex < a && zonesCol[zone] < len(subnetMatrix[rowIndex]) && subnetMatrix[rowIndex][zonesCol[zone]] != nil { + rowIndex++ + } + ly.setDefaultLocation(subnet, rowIndex, zonesCol[zone]) + rowIndex++ + } + } + } + } + } +} + +//////////////////////////////////////////////////////////////////// +// resolveGroupedSubnetsOverlap() handles overlapping GroupSubnetsSquare. +// it makes sure that the borders of two squares will not overlap each other. +// the borders of two squares overlap if these two condition happened: +// 1. they have the same first raw, or the same last raw, or the same first col or the same last col +// 2. they have the same xOffset (since xOffset == yOffset == xEndOffset == yEndOffset) +// +// in case we find such a pair, we shrink the smaller one by increasing its offsets +// we continue to look for such pairs till cant find any + +func (ly *layoutS) resolveGroupedSubnetsOverlap() { + allSubnetsSquares := map[*GroupSubnetsSquareTreeNode]bool{} + for _, tn := range getAllNodes(ly.network) { + if !tn.NotShownInDrawio() && tn.IsSquare() && tn.(SquareTreeNodeInterface).IsGroupSubnetsSquare() { + allSubnetsSquares[tn.(*GroupSubnetsSquareTreeNode)] = true + } + } + for foundOverlap := true; foundOverlap; { + foundOverlap = false + for tn1 := range allSubnetsSquares { + for tn2 := range allSubnetsSquares { + if tn1 == tn2 { + continue + } + l1 := tn1.Location() + l2 := tn2.Location() + if l1.firstRow == l2.firstRow || l1.firstCol == l2.firstCol || l1.lastRow == l2.lastRow || l1.lastCol == l2.lastCol { + if l1.xOffset == l2.xOffset { + toShrink := tn1 + if len(tn2.groupedSubnets) < len(tn1.groupedSubnets) { + toShrink = tn2 + } + toShrink.Location().xOffset += groupInnerBorderWidth + toShrink.Location().yOffset += groupInnerBorderWidth + toShrink.Location().xEndOffset += groupInnerBorderWidth + toShrink.Location().yEndOffset += groupInnerBorderWidth + foundOverlap = true + } + } + } + } + } +} + +// since we do not have subnet icons, we set the subnets smaller and the GroupSubnetsSquare bigger +func (ly *layoutS) setGroupedSubnetsOffset() { + for _, tn := range getAllNodes(ly.network) { + switch { + case tn.NotShownInDrawio(): + case !tn.IsSquare(): + case tn.(SquareTreeNodeInterface).IsSubnet(): + tn.Location().xOffset = borderWidth + tn.Location().yOffset = borderWidth + tn.Location().xEndOffset = borderWidth + tn.Location().yEndOffset = borderWidth + + case tn.(SquareTreeNodeInterface).IsGroupSubnetsSquare(): + tn.Location().xOffset = -groupBorderWidth + tn.Location().yOffset = -groupBorderWidth + tn.Location().xEndOffset = -groupBorderWidth + tn.Location().yEndOffset = -groupBorderWidth + } + } + ly.resolveGroupedSubnetsOverlap() +} + // ////////////////////////////////////////////////////////////////////////////////////////// // SG can have more than one squares. so setSGLocations() will add treeNodes of the kind PartialSGTreeNode // PartialSGTreeNode can not have more than one row. and can have only cell that contains icons that belong to the SG @@ -400,10 +517,16 @@ func (ly *layoutS) setSquaresLocations() { } } } + for _, groupSubnetsSquare := range vpc.(*VpcTreeNode).groupSubnetsSquares { + resolveSquareLocation(groupSubnetsSquare, 0, false) + } } } ly.resolvePublicNetworkLocations() resolveSquareLocation(ly.network, 1, false) + if ly.subnetMode { + ly.setGroupedSubnetsOffset() + } } // //////////////////////////////////////////////////////////////////////////////////////// diff --git a/pkg/drawio/layoutOverlap.go b/pkg/drawio/layoutOverlap.go index 2841c89cb..6cb17b269 100644 --- a/pkg/drawio/layoutOverlap.go +++ b/pkg/drawio/layoutOverlap.go @@ -91,7 +91,7 @@ func (lyO *layoutOverlap) handleGroupingLinesOverBorders() { continue } line := n.(LineTreeNodeInterface) - if !line.Src().IsGroupingPoint() || !line.Dst().IsGroupingPoint() { + if !line.Src().(IconTreeNodeInterface).IsGroupingPoint() || !line.Dst().(IconTreeNodeInterface).IsGroupingPoint() { continue } src, dst := line.Src().(*GroupPointTreeNode), line.Dst().(*GroupPointTreeNode) @@ -137,8 +137,8 @@ func (lyO *layoutOverlap) handleLinesOverLines() { if len(line1.Points()) != 0 || len(line2.Points()) != 0 { continue } - srcPoint := iconCenterPoint(line1.Src()) - dstPoint := iconCenterPoint(line1.Dst()) + srcPoint := iconCenterPoint(line1.Src().(IconTreeNodeInterface)) + dstPoint := iconCenterPoint(line1.Dst().(IconTreeNodeInterface)) middlePoint := point{(srcPoint.X + dstPoint.X) / 2, (srcPoint.Y + dstPoint.Y) / 2} BP := lyO.getBypassPoint(srcPoint, dstPoint, middlePoint, line1) if BP != noPoint { @@ -264,11 +264,11 @@ func (lyO *layoutOverlap) getOverlappedIcon(p1, p2 point, line LineTreeNodeInter // some methods to convert absolute point to relative, and vis versa: func getLineAbsolutePoints(line LineTreeNodeInterface) []point { - absPoints := []point{iconCenterPoint(line.Src())} + absPoints := []point{iconCenterPoint(line.Src().(IconTreeNodeInterface))} for _, p := range line.Points() { absPoints = append(absPoints, getAbsolutePoint(line, p)) } - absPoints = append(absPoints, iconCenterPoint(line.Dst())) + absPoints = append(absPoints, iconCenterPoint(line.Dst().(IconTreeNodeInterface))) return absPoints } diff --git a/pkg/drawio/lineTreeNode.go b/pkg/drawio/lineTreeNode.go index e017ac400..06b5d0197 100644 --- a/pkg/drawio/lineTreeNode.go +++ b/pkg/drawio/lineTreeNode.go @@ -11,8 +11,8 @@ type point struct { // ////////////////////////////////////////////////////////////////////////////// type LineTreeNodeInterface interface { TreeNodeInterface - Src() IconTreeNodeInterface - Dst() IconTreeNodeInterface + Src() TreeNodeInterface + Dst() TreeNodeInterface SrcID() uint DstID() uint Points() []point @@ -24,8 +24,8 @@ type LineTreeNodeInterface interface { type abstractLineTreeNode struct { abstractTreeNode - src IconTreeNodeInterface - dst IconTreeNodeInterface + src TreeNodeInterface + dst TreeNodeInterface router IconTreeNodeInterface points []point } @@ -34,10 +34,10 @@ func (tn *abstractLineTreeNode) IsLine() bool { return true } -func (tn *abstractLineTreeNode) SrcID() uint { return tn.src.ID() } -func (tn *abstractLineTreeNode) DstID() uint { return tn.dst.ID() } -func (tn *abstractLineTreeNode) Src() IconTreeNodeInterface { return tn.src } -func (tn *abstractLineTreeNode) Dst() IconTreeNodeInterface { return tn.dst } +func (tn *abstractLineTreeNode) SrcID() uint { return tn.src.ID() } +func (tn *abstractLineTreeNode) DstID() uint { return tn.dst.ID() } +func (tn *abstractLineTreeNode) Src() TreeNodeInterface { return tn.src } +func (tn *abstractLineTreeNode) Dst() TreeNodeInterface { return tn.dst } func (tn *abstractLineTreeNode) DrawioParent() TreeNodeInterface { if tn.router != nil { @@ -86,29 +86,24 @@ func NewConnectivityLineTreeNode(network SquareTreeNodeInterface, src, dst TreeNodeInterface, directed bool, name string) *ConnectivityTreeNode { - var iconSrc, iconDst IconTreeNodeInterface - if src.IsSquare() { - iconSrc = NewGroupPointTreeNode(src.(SquareTreeNodeInterface), directed, true, "") - } else { - iconSrc = src.(IconTreeNodeInterface) + if src.IsSquare() && src.(SquareTreeNodeInterface).IsGroupingSquare() { + src = NewGroupPointTreeNode(src.(SquareTreeNodeInterface), directed, true, "") } - if dst.IsSquare() { - iconDst = NewGroupPointTreeNode(dst.(SquareTreeNodeInterface), directed, false, "") - } else { - iconDst = dst.(IconTreeNodeInterface) + if dst.IsSquare() && dst.(SquareTreeNodeInterface).IsGroupingSquare() { + dst = NewGroupPointTreeNode(dst.(SquareTreeNodeInterface), directed, false, "") } - if iconSrc.IsGroupingPoint() { - iconSrc.(*GroupPointTreeNode).setColleague(iconDst) + if src.IsIcon() && src.(IconTreeNodeInterface).IsGroupingPoint() { + src.(*GroupPointTreeNode).setColleague(dst.(IconTreeNodeInterface)) } - if iconDst.IsGroupingPoint() { - iconDst.(*GroupPointTreeNode).setColleague(iconSrc) + if dst.IsIcon() && dst.(IconTreeNodeInterface).IsGroupingPoint() { + dst.(*GroupPointTreeNode).setColleague(src.(IconTreeNodeInterface)) } conn := ConnectivityTreeNode{ abstractLineTreeNode: abstractLineTreeNode{ abstractTreeNode: newAbstractTreeNode(network, name), - src: iconSrc, - dst: iconDst}, + src: src, + dst: dst}, directed: directed} network.addLineTreeNode(&conn) return &conn diff --git a/pkg/drawio/squareTreeNode.go b/pkg/drawio/squareTreeNode.go index 64def812b..fdbf4be2a 100644 --- a/pkg/drawio/squareTreeNode.go +++ b/pkg/drawio/squareTreeNode.go @@ -8,7 +8,9 @@ type SquareTreeNodeInterface interface { IconTreeNodes() []IconTreeNodeInterface TagID() uint DecoreID() uint + IsSubnet() bool IsGroupingSquare() bool + IsGroupSubnetsSquare() bool } type abstractSquareTreeNode struct { @@ -35,7 +37,9 @@ func (tn *abstractSquareTreeNode) IsSquare() bool { return true } func (tn *abstractSquareTreeNode) TagID() uint { return tn.id + tagID } func (tn *abstractSquareTreeNode) DecoreID() uint { return tn.id + decoreID } -func (tn *abstractSquareTreeNode) IsGroupingSquare() bool { return false } +func (tn *abstractSquareTreeNode) IsSubnet() bool { return false } +func (tn *abstractSquareTreeNode) IsGroupingSquare() bool { return false } +func (tn *abstractSquareTreeNode) IsGroupSubnetsSquare() bool { return false } func calculateSquareGeometry(tn SquareTreeNodeInterface) { location := tn.Location() @@ -108,8 +112,9 @@ func (tn *CloudTreeNode) children() ([]SquareTreeNodeInterface, []IconTreeNodeIn // //////////////////////////////////////////////////////////////////////////////////////// type VpcTreeNode struct { abstractSquareTreeNode - zones []SquareTreeNodeInterface - sgs []SquareTreeNodeInterface + zones []SquareTreeNodeInterface + sgs []SquareTreeNodeInterface + groupSubnetsSquares []SquareTreeNodeInterface } func NewVpcTreeNode(parent *CloudTreeNode, name string) *VpcTreeNode { @@ -118,7 +123,7 @@ func NewVpcTreeNode(parent *CloudTreeNode, name string) *VpcTreeNode { return &vpc } func (tn *VpcTreeNode) children() ([]SquareTreeNodeInterface, []IconTreeNodeInterface, []LineTreeNodeInterface) { - return append(tn.zones, tn.sgs...), tn.elements, tn.connections + return append(append(tn.zones, tn.sgs...), tn.groupSubnetsSquares...), tn.elements, tn.connections } /////////////////////////////////////////////////////////////////////// @@ -200,6 +205,7 @@ func (tn *SubnetTreeNode) children() ([]SquareTreeNodeInterface, []IconTreeNodeI func (tn *SubnetTreeNode) Label() string { return labels2Table([]string{tn.name, tn.cidr, tn.acl}) } +func (tn *SubnetTreeNode) IsSubnet() bool { return true } func (tn *SubnetTreeNode) SetACL(acl string) { tn.acl = acl } @@ -264,3 +270,46 @@ func (tn *GroupSquareTreeNode) setVisibility(visibility groupSquareVisibility) { func (tn *GroupSquareTreeNode) children() ([]SquareTreeNodeInterface, []IconTreeNodeInterface, []LineTreeNodeInterface) { return nil, append(tn.elements, tn.groupedIcons...), tn.connections } + +// //////////////////////////////////////////////////////////////////////////// +type GroupSubnetsSquareTreeNode struct { + abstractSquareTreeNode + groupedSubnets []SquareTreeNodeInterface +} + +func GroupedSubnetsSquare(parent *VpcTreeNode, groupedSubnets []SquareTreeNodeInterface) SquareTreeNodeInterface { + sameZone, sameVpc := true, true + zone := groupedSubnets[0].Parent().(*ZoneTreeNode) + vpc := groupedSubnets[0].Parent().Parent().(*VpcTreeNode) + for _, subnet := range groupedSubnets { + if zone != subnet.Parent() { + sameZone = false + } + if vpc != subnet.Parent().Parent() { + sameVpc = false + } + } + if sameVpc { + allVpcSubnets := []SquareTreeNodeInterface{} + for _, z := range vpc.zones { + allVpcSubnets = append(allVpcSubnets, z.(*ZoneTreeNode).subnets...) + } + if len(groupedSubnets) == len(allVpcSubnets) { + return vpc + } + } + if sameZone && len(groupedSubnets) == len(zone.subnets) { + return zone + } + return newGroupSubnetsSquareTreeNode(parent, groupedSubnets) +} + +func newGroupSubnetsSquareTreeNode(parent *VpcTreeNode, groupedSubnets []SquareTreeNodeInterface) *GroupSubnetsSquareTreeNode { + gs := GroupSubnetsSquareTreeNode{newAbstractSquareTreeNode(parent, ""), groupedSubnets} + parent.groupSubnetsSquares = append(parent.groupSubnetsSquares, &gs) + return &gs +} +func (tn *GroupSubnetsSquareTreeNode) children() ([]SquareTreeNodeInterface, []IconTreeNodeInterface, []LineTreeNodeInterface) { + return tn.groupedSubnets, tn.elements, tn.connections +} +func (tn *GroupSubnetsSquareTreeNode) IsGroupSubnetsSquare() bool { return true } diff --git a/pkg/drawio/styles.go b/pkg/drawio/styles.go index a891ce1e3..5e119a609 100644 --- a/pkg/drawio/styles.go +++ b/pkg/drawio/styles.go @@ -7,6 +7,7 @@ import ( ) const ( + groupSquareStyle = "rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#82b366;strokeWidth=6;perimeterSpacing=0;arcSize=12;gradientColor=none;opacity=70;" niStyle = "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+CjxkZWZzPgo8c3R5bGU+LmNscy0xe2ZpbGw6I2VlNTM5Njt9LmNscy0ye2ZpbGw6bm9uZTt9LmNscy0ze2ZpbGw6I2ZmZjt9PC9zdHlsZT4KPC9kZWZzPg0KPHJlY3QgY2xhc3M9ImNscy0xIiB4PSIwLjUiIHk9IjAuNSIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ii8+CjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iMTQuNSIgeT0iMTQuNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIi8+DQo8dGV4dCBmb250LXNpemU9IjMwIiBmaWxsPSJ3aGl0ZSIgeD0iOCIgeT0iMzUiPk5JPC90ZXh0Pgo8L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;" vsiStyle = "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxOTgwMzg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIvPjxjaXJjbGUgY2xhc3M9ImNscy0yIiBjeD0iMTguODgiIGN5PSIyOC44OCIgcj0iMC42MyIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iMTUuNzUiIHk9IjE4LjI1IiB3aWR0aD0iMi41IiBoZWlnaHQ9IjEuMjUiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjE5LjUiIHk9IjE4LjI1IiB3aWR0aD0iMi41IiBoZWlnaHQ9IjEuMjUiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjIzLjI1IiB5PSIxOC4yNSIgd2lkdGg9IjIuNSIgaGVpZ2h0PSIxLjI1Ii8+PHJlY3QgY2xhc3M9ImNscy0yIiB4PSIyNyIgeT0iMTguMjUiIHdpZHRoPSIyLjUiIGhlaWdodD0iMS4yNSIvPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iMzAuNzUiIHk9IjE4LjI1IiB3aWR0aD0iMi41IiBoZWlnaHQ9IjEuMjUiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zMiwzMkgxN2ExLjI1LDEuMjUsMCwwLDEtMS4yNS0xLjI1VjI3QTEuMjUsMS4yNSwwLDAsMSwxNywyNS43NUgzMkExLjI1LDEuMjUsMCwwLDEsMzMuMjUsMjd2My43NUExLjI1LDEuMjUsMCwwLDEsMzIsMzJaTTE3LDI3djMuNzVIMzJWMjdaIi8+PHJlY3QgY2xhc3M9ImNscy0zIiB4PSIxNC41IiB5PSIxNC41IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiLz48cmVjdCBjbGFzcz0iY2xzLTIiIHg9IjE1Ljc1IiB5PSIyMiIgd2lkdGg9IjE3LjUiIGhlaWdodD0iMS4yNSIvPjwvc3ZnPg==;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingTop=-7;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;" resIPStyle = "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+CjxkZWZzPgo8c3R5bGU+LmNscy0xe2ZpbGw6I2VlNTM5Njt9LmNscy0ye2ZpbGw6bm9uZTt9LmNscy0ze2ZpbGw6I2ZmZjt9PC9zdHlsZT4KPC9kZWZzPg0KPHJlY3QgY2xhc3M9ImNscy0xIiB4PSIwLjUiIHk9IjAuNSIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ii8+CjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iMTQuNSIgeT0iMTQuNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIi8+DQo8dGV4dCBmb250LXNpemU9IjIwIiBmaWxsPSJ3aGl0ZSIgeD0iNSIgeT0iMzIiPnJlc0lQPC90ZXh0Pgo8L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;" @@ -21,23 +22,24 @@ const ( ) var styles = map[reflect.Type]string{ - reflect.TypeOf(PublicNetworkTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", - reflect.TypeOf(CloudTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", - reflect.TypeOf(VpcTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", - reflect.TypeOf(ZoneTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#878d96;fillColor=none;", - reflect.TypeOf(PartialSGTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;fillColor=none;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#FA4D56;strokeWidth=1;", - reflect.TypeOf(SubnetTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", - reflect.TypeOf(GroupSquareTreeNode{}): "rounded=1;whiteSpace=wrap;html=1;fillColor=none;strokeColor=#006633;strokeWidth=1;perimeterSpacing=0;arcSize=12;", - reflect.TypeOf(NITreeNode{}): vsiStyle, - reflect.TypeOf(VsiTreeNode{}): vsiStyle, - reflect.TypeOf(ResIPTreeNode{}): vpeStyle, - reflect.TypeOf(VpeTreeNode{}): vpeStyle, - reflect.TypeOf(GroupPointTreeNode{}): "ellipse;whiteSpace=wrap;html=1;aspect=fixed;", - reflect.TypeOf(UserTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOm5vbmU7fS5jbHMtMntmaWxsOiNmZmY7ZmlsbC1ydWxlOmV2ZW5vZGQ7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IHg9IjAuNSIgeT0iMC41IiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHJ4PSIyNCIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTQuNSIgeT0iMTQuNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIi8+PHBhdGggaWQ9IkZpbGwtMyIgY2xhc3M9ImNscy0yIiBkPSJNMzAuOCwzMy44N0gyOVYyOS41OUEyLjYzLDIuNjMsMCwwLDAsMjYuMywyN0gyMi43QTIuNjMsMi42MywwLDAsMCwyMCwyOS41OXY0LjI4SDE4LjJWMjkuNTlhNC40MSw0LjQxLDAsMCwxLDQuNS00LjI4aDMuNmE0LjQxLDQuNDEsMCwwLDEsNC41LDQuMjhaIi8+PHBhdGggaWQ9IkZpbGwtNSIgY2xhc3M9ImNscy0yIiBkPSJNMjQuNSwxNS4wNUE0LjM5LDQuMzksMCwwLDAsMjAsMTkuMzNhNC41MSw0LjUxLDAsMCwwLDksMCw0LjM5LDQuMzksMCwwLDAtNC41LTQuMjhtMCwxLjcxYTIuNTcsMi41NywwLDEsMS0yLjcsMi41NywyLjY0LDIuNjQsMCwwLDEsMi43LTIuNTciLz48L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;", - reflect.TypeOf(GatewayTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgcng9IjgiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zMy41MSwyNS4zOGExLjIzLDEuMjMsMCwwLDAsMC0xLjc2TDI5Ljg5LDIwbDEuODEtMS43OWExLjI1LDEuMjUsMCwxLDAtLjU4LTIuMDksMS4yMiwxLjIyLDAsMCwwLS4zMiwxLjIyTDI5LDE5LjEybC0zLjYzLTMuNjNhMS4yMywxLjIzLDAsMCwwLTEuNzYsMEwyMCwxOS4xMWwtMS43OS0xLjgyYTEuMjQsMS4yNCwwLDEsMC0yLjA5LjU5LDEuMjIsMS4yMiwwLDAsMCwxLjIyLjMyTDE5LjEyLDIwbC0zLjYzLDMuNjNhMS4yMywxLjIzLDAsMCwwLDAsMS43NkwxOS4xMiwyOSwxNy4zNCwzMC44YTEuMjIsMS4yMiwwLDAsMC0xLjIyLjMyLDEuMjQsMS4yNCwwLDEsMCwyLjA5LjU5TDIwLDI5Ljg5bDMuNjIsMy42MmExLjIzLDEuMjMsMCwwLDAsMS43NiwwTDI5LDI5Ljg4bDEuNzksMS43OGExLjIyLDEuMjIsMCwwLDAsLjMyLDEuMjIsMS4yNCwxLjI0LDAsMSwwLC41OC0yLjA5TDI5Ljg5LDI5Wm0tOSw3LjI0TDE2LjM4LDI0LjVsOC4xMi04LjEyLDguMTIsOC4xMloiLz48cmVjdCBjbGFzcz0iY2xzLTMiIHg9IjE0LjUiIHk9IjE0LjUiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTI2LjM4LDIzLjI1SDIzLjI1VjIyYTEuMjUsMS4yNSwwLDAsMSwyLjUsMEgyN2EyLjUsMi41LDAsMCwwLTUsMHYxLjQyYTEuMjYsMS4yNiwwLDAsMC0uNjIsMS4wOHYzLjEyYTEuMjYsMS4yNiwwLDAsMCwxLjI0LDEuMjZoMy43NmExLjI2LDEuMjYsMCwwLDAsMS4yNC0xLjI2VjI0LjVBMS4yNSwxLjI1LDAsMCwwLDI2LjM4LDIzLjI1Wm0wLDQuMzdIMjIuNjJWMjQuNWgzLjc2WiIvPjwvc3ZnPg==;fontSize=14;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;spacingTop=-7;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;", - reflect.TypeOf(InternetTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgcng9IjgiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNC41LDE1Ljc1YTguNzUsOC43NSwwLDEsMCw4Ljc1LDguNzVBOC43NSw4Ljc1LDAsMCwwLDI0LjUsMTUuNzVaTTMyLDIzLjg4SDI4LjI1YTE1LjE5LDE1LjE5LDAsMCwwLTEuNzQtNi42QTcuNSw3LjUsMCwwLDEsMzIsMjMuODhaTTI0LjUsMzJoLS40MkExMy43MiwxMy43MiwwLDAsMSwyMiwyNS4xMmg1QTEzLjYzLDEzLjYzLDAsMCwxLDI0Ljk0LDMyWk0yMiwyMy44OEExMy42MywxMy42MywwLDAsMSwyNC4wNiwxN2EzLjkzLDMuOTMsMCwwLDEsLjg0LDBBMTMuNjQsMTMuNjQsMCwwLDEsMjcsMjMuODhabS40OC02LjZhMTUuMTgsMTUuMTgsMCwwLDAtMS43Myw2LjZIMTdhNy41LDcuNSwwLDAsMSw1LjQ5LTYuNlpNMTcsMjUuMTJoMy43NWExNS4yLDE1LjIsMCwwLDAsMS43Miw2LjZBNy41Miw3LjUyLDAsMCwxLDE3LDI1LjEyWm05LjQ4LDYuNmExNS4xOSwxNS4xOSwwLDAsMCwxLjc0LTYuNkgzMkE3LjUsNy41LDAsMCwxLDI2LjUxLDMxLjcyWiIvPjxyZWN0IGlkPSJfVHJhbnNwYXJlbnRfUmVjdGFuZ2xlXyIgZGF0YS1uYW1lPSIgVHJhbnNwYXJlbnQgUmVjdGFuZ2xlICIgY2xhc3M9ImNscy0zIiB4PSIxNC41IiB5PSIxNC41IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiLz48L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;", - reflect.TypeOf(InternetServiceTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxLjg3LDIwLjc1YTYuMjUsNi4yNSwwLDAsMC0xMi4yNi4wOCw0LjY4LDQuNjgsMCwwLDAsLjgzLDkuMjloLjk0VjI4Ljg3aC0uOTRBMy40MywzLjQzLDAsMCwxLDIwLjIsMjJsLjUyLDAsLjA2LS41MmE1LDUsMCwwLDEsOS44MS0uNzFaIi8+PHJlY3QgY2xhc3M9ImNscy0zIiB4PSIxNC41IiB5PSIxNC41IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zMS4zNywyOS41YTEuODQsMS44NCwwLDAsMC0xLjIuNDVsLTIuNTYtMS41NGMwLS4wNSwwLS4xLDAtLjE2czAtLjExLDAtLjE2bDIuNTYtMS41NGExLjg2LDEuODYsMCwwLDAsMS4yLjQ1LDEuODgsMS44OCwwLDEsMC0xLjg3LTEuODgsMS40MiwxLjQyLDAsMCwwLDAsLjM2bC0yLjQ1LDEuNDZhMS44NiwxLjg2LDAsMCwwLTEuMzQtLjU3LDEuODgsMS44OCwwLDAsMCwwLDMuNzUsMS44NSwxLjg1LDAsMCwwLDEuMzQtLjU2TDI5LjU0LDMxYTEuNDUsMS40NSwwLDAsMCwwLC4zNSwxLjg4LDEuODgsMCwxLDAsMS44Ny0xLjg3Wm0wLTVhLjYzLjYzLDAsMSwxLS42Mi42MkEuNjMuNjMsMCwwLDEsMzEuMzcsMjQuNVptLTUuNjIsNC4zN2EuNjMuNjMsMCwwLDEtLjYzLS42Mi42NC42NCwwLDAsMSwuNjMtLjYzLjYzLjYzLDAsMCwxLC42Mi42M0EuNjIuNjIsMCwwLDEsMjUuNzUsMjguODdaTTMxLjM3LDMyYS42My42MywwLDEsMSwuNjMtLjYzQS42My42MywwLDAsMSwzMS4zNywzMloiLz48L3N2Zz4=;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;spacingTop=-6;", - reflect.TypeOf(LogicalLineTreeNode{}): "html=1;verticalAlign=middle;startArrow=oval;startFill=1;endArrow=oval;startSize=6;strokeColor=#000000;align=center;dashed=1;strokeWidth=2;horizontal=1;labelPosition=center;verticalLabelPosition=middle;endFill=1;rounded=0;", + reflect.TypeOf(PublicNetworkTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", + reflect.TypeOf(CloudTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", + reflect.TypeOf(VpcTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", + reflect.TypeOf(ZoneTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#878d96;fillColor=none;", + reflect.TypeOf(PartialSGTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;fillColor=none;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#FA4D56;strokeWidth=1;", + reflect.TypeOf(SubnetTreeNode{}): "rounded=0;whiteSpace=wrap;html=1;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;spacingBottom=-28;spacingTop=0;labelPosition=-100;verticalLabelPosition=top;align=center;verticalAlign=bottom;spacingLeft=9;spacing=0;expand=0;recursiveResize=0;spacingRight=0;container=1;collapsible=0;strokeColor=#1192E8;fillColor=none;", + reflect.TypeOf(GroupSquareTreeNode{}): groupSquareStyle, + reflect.TypeOf(GroupSubnetsSquareTreeNode{}): groupSquareStyle, + reflect.TypeOf(NITreeNode{}): vsiStyle, + reflect.TypeOf(VsiTreeNode{}): vsiStyle, + reflect.TypeOf(ResIPTreeNode{}): vpeStyle, + reflect.TypeOf(VpeTreeNode{}): vpeStyle, + reflect.TypeOf(GroupPointTreeNode{}): "ellipse;whiteSpace=wrap;html=1;aspect=fixed;", + reflect.TypeOf(UserTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOm5vbmU7fS5jbHMtMntmaWxsOiNmZmY7ZmlsbC1ydWxlOmV2ZW5vZGQ7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IHg9IjAuNSIgeT0iMC41IiB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHJ4PSIyNCIvPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMTQuNSIgeT0iMTQuNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIi8+PHBhdGggaWQ9IkZpbGwtMyIgY2xhc3M9ImNscy0yIiBkPSJNMzAuOCwzMy44N0gyOVYyOS41OUEyLjYzLDIuNjMsMCwwLDAsMjYuMywyN0gyMi43QTIuNjMsMi42MywwLDAsMCwyMCwyOS41OXY0LjI4SDE4LjJWMjkuNTlhNC40MSw0LjQxLDAsMCwxLDQuNS00LjI4aDMuNmE0LjQxLDQuNDEsMCwwLDEsNC41LDQuMjhaIi8+PHBhdGggaWQ9IkZpbGwtNSIgY2xhc3M9ImNscy0yIiBkPSJNMjQuNSwxNS4wNUE0LjM5LDQuMzksMCwwLDAsMjAsMTkuMzNhNC41MSw0LjUxLDAsMCwwLDksMCw0LjM5LDQuMzksMCwwLDAtNC41LTQuMjhtMCwxLjcxYTIuNTcsMi41NywwLDEsMS0yLjcsMi41NywyLjY0LDIuNjQsMCwwLDEsMi43LTIuNTciLz48L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;", + reflect.TypeOf(GatewayTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgcng9IjgiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zMy41MSwyNS4zOGExLjIzLDEuMjMsMCwwLDAsMC0xLjc2TDI5Ljg5LDIwbDEuODEtMS43OWExLjI1LDEuMjUsMCwxLDAtLjU4LTIuMDksMS4yMiwxLjIyLDAsMCwwLS4zMiwxLjIyTDI5LDE5LjEybC0zLjYzLTMuNjNhMS4yMywxLjIzLDAsMCwwLTEuNzYsMEwyMCwxOS4xMWwtMS43OS0xLjgyYTEuMjQsMS4yNCwwLDEsMC0yLjA5LjU5LDEuMjIsMS4yMiwwLDAsMCwxLjIyLjMyTDE5LjEyLDIwbC0zLjYzLDMuNjNhMS4yMywxLjIzLDAsMCwwLDAsMS43NkwxOS4xMiwyOSwxNy4zNCwzMC44YTEuMjIsMS4yMiwwLDAsMC0xLjIyLjMyLDEuMjQsMS4yNCwwLDEsMCwyLjA5LjU5TDIwLDI5Ljg5bDMuNjIsMy42MmExLjIzLDEuMjMsMCwwLDAsMS43NiwwTDI5LDI5Ljg4bDEuNzksMS43OGExLjIyLDEuMjIsMCwwLDAsLjMyLDEuMjIsMS4yNCwxLjI0LDAsMSwwLC41OC0yLjA5TDI5Ljg5LDI5Wm0tOSw3LjI0TDE2LjM4LDI0LjVsOC4xMi04LjEyLDguMTIsOC4xMloiLz48cmVjdCBjbGFzcz0iY2xzLTMiIHg9IjE0LjUiIHk9IjE0LjUiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTI2LjM4LDIzLjI1SDIzLjI1VjIyYTEuMjUsMS4yNSwwLDAsMSwyLjUsMEgyN2EyLjUsMi41LDAsMCwwLTUsMHYxLjQyYTEuMjYsMS4yNiwwLDAsMC0uNjIsMS4wOHYzLjEyYTEuMjYsMS4yNiwwLDAsMCwxLjI0LDEuMjZoMy43NmExLjI2LDEuMjYsMCwwLDAsMS4yNC0xLjI2VjI0LjVBMS4yNSwxLjI1LDAsMCwwLDI2LjM4LDIzLjI1Wm0wLDQuMzdIMjIuNjJWMjQuNWgzLjc2WiIvPjwvc3ZnPg==;fontSize=14;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;spacingTop=-7;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;", + reflect.TypeOf(InternetTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgcng9IjgiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0yNC41LDE1Ljc1YTguNzUsOC43NSwwLDEsMCw4Ljc1LDguNzVBOC43NSw4Ljc1LDAsMCwwLDI0LjUsMTUuNzVaTTMyLDIzLjg4SDI4LjI1YTE1LjE5LDE1LjE5LDAsMCwwLTEuNzQtNi42QTcuNSw3LjUsMCwwLDEsMzIsMjMuODhaTTI0LjUsMzJoLS40MkExMy43MiwxMy43MiwwLDAsMSwyMiwyNS4xMmg1QTEzLjYzLDEzLjYzLDAsMCwxLDI0Ljk0LDMyWk0yMiwyMy44OEExMy42MywxMy42MywwLDAsMSwyNC4wNiwxN2EzLjkzLDMuOTMsMCwwLDEsLjg0LDBBMTMuNjQsMTMuNjQsMCwwLDEsMjcsMjMuODhabS40OC02LjZhMTUuMTgsMTUuMTgsMCwwLDAtMS43Myw2LjZIMTdhNy41LDcuNSwwLDAsMSw1LjQ5LTYuNlpNMTcsMjUuMTJoMy43NWExNS4yLDE1LjIsMCwwLDAsMS43Miw2LjZBNy41Miw3LjUyLDAsMCwxLDE3LDI1LjEyWm05LjQ4LDYuNmExNS4xOSwxNS4xOSwwLDAsMCwxLjc0LTYuNkgzMkE3LjUsNy41LDAsMCwxLDI2LjUxLDMxLjcyWiIvPjxyZWN0IGlkPSJfVHJhbnNwYXJlbnRfUmVjdGFuZ2xlXyIgZGF0YS1uYW1lPSIgVHJhbnNwYXJlbnQgUmVjdGFuZ2xlICIgY2xhc3M9ImNscy0zIiB4PSIxNC41IiB5PSIxNC41IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiLz48L3N2Zz4=;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;spacingTop=-7;", + reflect.TypeOf(InternetServiceTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTMxLjg3LDIwLjc1YTYuMjUsNi4yNSwwLDAsMC0xMi4yNi4wOCw0LjY4LDQuNjgsMCwwLDAsLjgzLDkuMjloLjk0VjI4Ljg3aC0uOTRBMy40MywzLjQzLDAsMCwxLDIwLjIsMjJsLjUyLDAsLjA2LS41MmE1LDUsMCwwLDEsOS44MS0uNzFaIi8+PHJlY3QgY2xhc3M9ImNscy0zIiB4PSIxNC41IiB5PSIxNC41IiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0zMS4zNywyOS41YTEuODQsMS44NCwwLDAsMC0xLjIuNDVsLTIuNTYtMS41NGMwLS4wNSwwLS4xLDAtLjE2czAtLjExLDAtLjE2bDIuNTYtMS41NGExLjg2LDEuODYsMCwwLDAsMS4yLjQ1LDEuODgsMS44OCwwLDEsMC0xLjg3LTEuODgsMS40MiwxLjQyLDAsMCwwLDAsLjM2bC0yLjQ1LDEuNDZhMS44NiwxLjg2LDAsMCwwLTEuMzQtLjU3LDEuODgsMS44OCwwLDAsMCwwLDMuNzUsMS44NSwxLjg1LDAsMCwwLDEuMzQtLjU2TDI5LjU0LDMxYTEuNDUsMS40NSwwLDAsMCwwLC4zNSwxLjg4LDEuODgsMCwxLDAsMS44Ny0xLjg3Wm0wLTVhLjYzLjYzLDAsMSwxLS42Mi42MkEuNjMuNjMsMCwwLDEsMzEuMzcsMjQuNVptLTUuNjIsNC4zN2EuNjMuNjMsMCwwLDEtLjYzLS42Mi42NC42NCwwLDAsMSwuNjMtLjYzLjYzLjYzLDAsMCwxLC42Mi42M0EuNjIuNjIsMCwwLDEsMjUuNzUsMjguODdaTTMxLjM3LDMyYS42My42MywwLDEsMSwuNjMtLjYzQS42My42MywwLDAsMSwzMS4zNywzMloiLz48L3N2Zz4=;fontSize=14;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;spacingTop=-6;", + reflect.TypeOf(LogicalLineTreeNode{}): "html=1;verticalAlign=middle;startArrow=oval;startFill=1;endArrow=oval;startSize=6;strokeColor=#000000;align=center;dashed=1;strokeWidth=2;horizontal=1;labelPosition=center;verticalLabelPosition=middle;endFill=1;rounded=0;", // reflect.TypeOf(EndPointTreeNode{}): "shape=image;aspect=fixed;image=data:image/svg+xml,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OSA0OSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMxMTkyZTg7fS5jbHMtMntmaWxsOiNmZmY7fS5jbHMtM3tmaWxsOm5vbmU7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIvPjxwYXRoIGlkPSJ2cGNfZ3JhZGllbnRfYm90dG9tIiBkYXRhLW5hbWU9InZwYyBncmFkaWVudCBib3R0b20iIGNsYXNzPSJjbHMtMiIgZD0iTTI3LDMxLjM4SDE4Ljg4YTEuMjcsMS4yNywwLDAsMS0xLjI2LTEuMjVWMjJoMS4yNnY4LjEzSDI3WiIvPjxwYXRoIGlkPSJ2cGNfZ3JhZGllbnRfdG9wIiBkYXRhLW5hbWU9InZwYyBncmFkaWVudCB0b3AiIGNsYXNzPSJjbHMtMiIgZD0iTTMwLjEyLDI3aDEuMjZWMTguODhhMS4yNiwxLjI2LDAsMCwwLTEuMjYtMS4yNUgyMnYxLjI1aDguMTJaIi8+PHBhdGggaWQ9ImVuZHBvaW50cyIgY2xhc3M9ImNscy0yIiBkPSJNMjkuMTIsMjguMjVsLTIuNS0yLjVBMi4yNiwyLjI2LDAsMCwwLDI3LDI0LjUsMi41MSwyLjUxLDAsMCwwLDI0LjUsMjJhMi4xOSwyLjE5LDAsMCwwLTEuMjUuMzhsLTIuNS0yLjVWMTUuNzVoLTV2NWg0LjEzbDIuNSwyLjVBMi4yNiwyLjI2LDAsMCwwLDIyLDI0LjUsMi41MSwyLjUxLDAsMCwwLDI0LjUsMjdhMi4yNiwyLjI2LDAsMCwwLDEuMjUtLjM4bDIuNSwyLjV2NC4xM2g1di01Wk0xOS41LDE5LjVIMTdWMTdoMi41Wm01LDYuMjVhMS4yNSwxLjI1LDAsMSwxLDEuMjUtMS4yNUExLjI1LDEuMjUsMCwwLDEsMjQuNSwyNS43NVpNMzIsMzJIMjkuNVYyOS41SDMyWiIvPjxyZWN0IGNsYXNzPSJjbHMtMyIgeD0iMTQuNSIgeT0iMTQuNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIi8+PC9zdmc+;fontSize=14;fontFamily=IBM Plex Sans;fontSource=fonts%2FIBMPlexSans-Regular.woff;spacingTop=-7;labelPosition=center;verticalLabelPosition=bottom;align=center;verticalAlign=top;", } var miniStyles = map[reflect.Type]string{ @@ -96,10 +98,10 @@ func connectivityStyle(con *ConnectivityTreeNode) string { if con.directed { endArrow = errorEndEdge } - if con.Src().IsGroupingPoint() && !con.Src().(*GroupPointTreeNode).hasShownSquare() { + if con.Src().IsIcon() && con.Src().(IconTreeNodeInterface).IsGroupingPoint() && !con.Src().(*GroupPointTreeNode).hasShownSquare() { startArrow = noneEndEdge } - if con.Dst().IsGroupingPoint() && !con.Dst().(*GroupPointTreeNode).hasShownSquare() { + if con.IsIcon() && con.Dst().(IconTreeNodeInterface).IsGroupingPoint() && !con.Dst().(*GroupPointTreeNode).hasShownSquare() { endArrow = noneEndEdge } if con.router != nil { diff --git a/pkg/drawio/subnetsLayout.go b/pkg/drawio/subnetsLayout.go new file mode 100644 index 000000000..e509f76b7 --- /dev/null +++ b/pkg/drawio/subnetsLayout.go @@ -0,0 +1,773 @@ +package drawio + +import ( + "maps" + "sort" + + "github.com/np-guard/vpc-network-config-analyzer/pkg/common" +) + +// ////////////////////////////////////////////////////////////////////////////////////////////// +// subnetsLayout struct is a struct for layout subnets when the network is in subnet mode. +// the input of the layout algorithm is the groups of subnets, +// and the output is a matrix of subnets, representing the subnets location on the drawio canvas. +// the location of the groups squares is determinate later by the location of the subnets. +// the layout algorithm should make sure that all the group subnets, and only the group subnets should be inside the group squares. +// since some of the subnets can be in the more than one group, the solution is not trivial. +// and is some cases we must split a group to smaller groups, and delete the original group. +// +// the algorithm uses the concept of miniGroup: +// a miniGroup is a set of subnets. all the subnets in a miniGroup are sharing same groups, and the same zone. +// all the subnet in the miniGroup will be alongside each other, so instead of layout the subnets, we layout miniGroups +// a group is not a set of subnets, but a set of miniGroups +// +// the main phases of the layout algorithm: +// 1. sort the subnets to miniGroups, sort the miniGroups to their groups: +// the output is a list of groups, each group has a list of miniGroups +// 2. create a tree of groups - the children of a group are set of groups that do not intersect, and hold only miniGroups of the group +// (in this phase new groups are created, by splitting groups to smaller groups) +// 3. layout the groups +// 4. create new treeNodes of the new groupSquares and new connectors +// ////////////////////////////////////////////////////////////////////////////////////////////// + +type subnetSet = common.GenericSet[TreeNodeInterface] +type groupTnSet = common.GenericSet[TreeNodeInterface] +type groupSet = common.GenericSet[*groupDataS] +type miniGroupSet = common.GenericSet[*miniGroupDataS] +type setAsKey = common.SetAsKey + +///////////////////////////////////////////////////////////////// + +type miniGroupDataS struct { + subnets subnetSet + zone TreeNodeInterface + located bool +} + +// /////////////////////////////////////////////////////////////////// +// groupsDataS is the struct representing a group. +// they are creating if the first step, when sorting the miniGroups to groups. +// some more are created when groups are split to smaller group +// ///////////////////////////////////////////////////////////////// +type groupDataS struct { + // miniGroups - set of the miniGroups of the group + // subnets - set of all the subnets of the group + miniGroups miniGroupSet + subnets subnetSet + // treeNode - the relevant treeNode of the group, for most groups we already have a treeNode, for new groups, we create a new treeNode + treeNode TreeNodeInterface + // children - the children in the tree of groups + children groupSet + // toSplitGroups - toSplitGroups are all the subgroups of the group that will be split. these groups will not be te the groups tree + toSplitGroups groupSet + // splitFrom - if a group was created during splitting, splitFrom is the groups that the group was split from + // splitTo - if a group was split, splitTo is the groups that the group was split to + splitFrom groupSet + splitTo groupSet +} + +// fakeSubnet and fakeMiniGroup are used as space holders in the matrixes +var fakeSubnet TreeNodeInterface = &SubnetTreeNode{} +var fakeMiniGroup *miniGroupDataS = &miniGroupDataS{subnets: subnetSet{}} + +func newGroupDataS(miniGroups miniGroupSet, tn TreeNodeInterface) *groupDataS { + subnets := subnetSet{} + for miniGroup := range miniGroups { + for subnet := range miniGroup.subnets { + subnets[subnet] = true + } + } + return &groupDataS{ + miniGroups: miniGroups, + subnets: subnets, + treeNode: tn, + children: groupSet{}, + toSplitGroups: groupSet{}, + splitFrom: groupSet{}, + splitTo: groupSet{}, + } +} + +// ///////////////////////////////////////////////////////////////// +func (group *groupDataS) isInnerGroup(subGroup *groupDataS) bool { + if len(group.miniGroups) == len(subGroup.miniGroups) { + return false + } + for mg := range subGroup.miniGroups { + if !group.miniGroups[mg] { + return false + } + } + return true +} + +func (group *groupDataS) getVpc() *VpcTreeNode { + if group.treeNode != nil { + return group.treeNode.Parent().(*VpcTreeNode) + } + for g := range group.splitFrom { + return g.getVpc() + } + return nil +} + +func (group *groupDataS) reunion() { + for gr := range group.splitTo { + delete(gr.splitFrom, group) + } + group.splitTo = groupSet{} +} + +// //////////////////////////////////////////////////////////////////////// +type indexes struct { + row, col int +} +type subnetsLayout struct { + network SquareTreeNodeInterface + groups []*groupDataS + miniGroups miniGroupSet + miniGroupsMatrix [][]*miniGroupDataS + subnetMatrix [][]TreeNodeInterface + subnetsIndexes map[TreeNodeInterface]indexes + zonesCol map[TreeNodeInterface]int + treeNodesToGroups map[TreeNodeInterface]*groupDataS + topFakeGroup *groupDataS +} + +func newSubnetsLayout(network SquareTreeNodeInterface) *subnetsLayout { + return &subnetsLayout{ + network: network, + miniGroups: miniGroupSet{}, + subnetsIndexes: map[TreeNodeInterface]indexes{}, + zonesCol: map[TreeNodeInterface]int{}, + treeNodesToGroups: map[TreeNodeInterface]*groupDataS{}, + } +} + +// layout() - the top function, with the four steps of the algorithm: +func (ly *subnetsLayout) layout() { + // create a list of groups and miniGroups: + ly.createGroupsDataS() + ly.topFakeGroup = newGroupDataS(ly.miniGroups, nil) + // create the group tree: + ly.createGroupSubTree(ly.topFakeGroup) + // layout the groups: + ly.layoutGroups() + // create the new treeNodes: + ly.createNewTreeNodes() +} + +// ////////////////////////////////////////////////////////// +// createGroupsDataS() - sorting the subnets to miniGroups and groups +// the output is a list of miniGroups and list of groups. (each group holds a set of miniGroups) +// a miniGroup is a set of subnets from the same zone, each subnet in the set are at the same set of groups. +// phases: +// 1. sort subnets to groups (map: subnet -> set of groups) +// 2. sort sets of groups to set of subnets (map: group set -> subnet Set) +// 3. create miniGroups +// 4. create the groups +func (ly *subnetsLayout) createGroupsDataS() { + subnetToGroups := ly.sortSubnets() + groupSetToSubnetSet := sortSubnetsByZoneAndGroups(subnetToGroups) + ly.createMiniGroups(groupSetToSubnetSet) + ly.createGroups(subnetToGroups) + sort.Slice(ly.groups, func(i, j int) bool { + return len(ly.groups[i].miniGroups) > len(ly.groups[j].miniGroups) + }) +} + +///////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////// +// createGroupSubTree() is a recursive method to create the tree of groups. +// the root of the tree is a fake group that holds all the miniGroups +// the creation of the tree might involve splitting groups to smaller new groups. +// (the split group is not in the tree) +// for example, consider the following list of groups: +// (m1,m2,m3), (m2,m3,m4,m5), (m4,m5,m6), (m4, m5) +// the algorithm will split (m2,m3,m4,m5). it will create a new group (m2,m3) +// the tree will look like: +// (m1,m2,m3,m4,m5,m6) -> (m1,m2,m3), (m4,m5,m6) +// (m1,m2,m3) -> (m2,m3) +// (m4,m5,m6) -> (m4,m5) + +// the main challenge of createGroupSubTree() is to choose which groups to split +// the candidates are all the subgroups of the group, which are not a subgroup of other subgroup (aka nonSplitNotInnerGroups). +// each iteration of the loop: +// - updates nonSplitNotInnerGroups, +// - choose a sub group to split, +// - continue the loop till all nonSplitNotInnerGroups do not intersect each other. +// the subgroups that remained at nonSplitNotInnerGroups, are set to be the children of the group +// the sub groups that was chosen to be split, are split to new groups at createGroupsFromSplitGroups() + +func (ly *subnetsLayout) createGroupSubTree(group *groupDataS) { + nonSplitGroups := ly.innerGroupsOfAGroup(group) + for innerGroup := range nonSplitGroups { + if len(innerGroup.splitTo) > 0 { + delete(nonSplitGroups, innerGroup) + } + } + for { + nonSplitNotInnerGroups := nonInnerGroups(nonSplitGroups) + intersectGroups := intersectGroups(nonSplitNotInnerGroups) + mostSharedGroup := chooseGroupToSplit(intersectGroups) + if mostSharedGroup == nil { + group.children = nonSplitNotInnerGroups + break + } + group.toSplitGroups[mostSharedGroup] = true + delete(nonSplitGroups, mostSharedGroup) + } + + if len(group.toSplitGroups) > 0 { + ly.createGroupsFromSplitGroups(group) + } + for topInnerGroup := range group.children { + ly.createGroupSubTree(topInnerGroup) + } +} + +// /////////////////////////////////////////////////////////////////////////// +// /////////////////////////////////////////////////////////////////////////// +// the output of the layout algorithm is a matrix of miniGroups, later converted to a matrix of subnets +// each column is dedicated to a different zone +// layoutGroups() has two main steps: +// 1. calc the zone order - try to put zones that share the same group next to each other +// 2. layout groups - a recursive call to set the miniGroups locations on the matrix +// see documentation of calcZoneOrder() and layoutGroup() +func (ly *subnetsLayout) layoutGroups() { + ly.calcZoneOrder() + ly.createMatrixes() + ly.layoutGroup(ly.topFakeGroup, 0) + ly.setSubnetsMatrix() +} + +// /////////////////////////////////////////////////////////////////////////// +// /////////////////////////////////////////////////////////////////////////// +// createNewTreeNodes() do the follows: +// check if a split group can somehow be shown on the canvas, if not, mark them as doNotShowOnDrawio +// created treeNodes for the groups that was created during the splitting +// creates new connections that replace the connections of groups that was split +func (ly *subnetsLayout) createNewTreeNodes() { + ly.doNotShowSplitGroups() + ly.createNewGroupsTreeNodes() + ly.createNewLinesTreeNodes() +} + +// //////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////// + +func (ly *subnetsLayout) sortSubnets() map[TreeNodeInterface]groupTnSet { + subnetToGroups := map[TreeNodeInterface]groupTnSet{} + for group := range ly.groupsTreeNodes() { + for _, subnet := range group.(*GroupSubnetsSquareTreeNode).groupedSubnets { + if _, ok := subnetToGroups[subnet]; !ok { + subnetToGroups[subnet] = groupTnSet{} + } + subnetToGroups[subnet][group] = true + } + } + return subnetToGroups +} + +func (ly *subnetsLayout) groupsTreeNodes() groupTnSet { + allGroups := groupTnSet{} + for _, tn := range getAllNodes(ly.network) { + if tn.IsSquare() && tn.(SquareTreeNodeInterface).IsGroupSubnetsSquare() { + allGroups[tn] = true + } + } + return allGroups +} + +func sortSubnetsByZoneAndGroups(subnetToGroups map[TreeNodeInterface]groupTnSet) map[setAsKey]map[TreeNodeInterface]subnetSet { + groupSetToSubnetSet := map[setAsKey]map[TreeNodeInterface]subnetSet{} + for subnet, groups := range subnetToGroups { + if _, ok := groupSetToSubnetSet[groups.AsKey()]; !ok { + groupSetToSubnetSet[groups.AsKey()] = map[TreeNodeInterface]subnetSet{} + } + zone := subnet.Parent() + if _, ok := groupSetToSubnetSet[groups.AsKey()][zone]; !ok { + groupSetToSubnetSet[groups.AsKey()][subnet.Parent()] = subnetSet{} + } + groupSetToSubnetSet[groups.AsKey()][zone][subnet] = true + } + return groupSetToSubnetSet +} + +func (ly *subnetsLayout) createMiniGroups(groupSetToSubnetSet map[setAsKey]map[TreeNodeInterface]subnetSet) { + for _, zoneMiniGroup := range groupSetToSubnetSet { + for zone, miniGroup := range zoneMiniGroup { + miniGroupData := miniGroupDataS{subnets: miniGroup, zone: zone} + ly.miniGroups[&miniGroupData] = true + } + } +} +func (ly *subnetsLayout) createGroups(subnetToGroups map[TreeNodeInterface]groupTnSet) { + groupToMiniGroups := map[TreeNodeInterface]miniGroupSet{} + for miniGroup := range ly.miniGroups { + for subnet := range miniGroup.subnets { + for group := range subnetToGroups[subnet] { + if _, ok := groupToMiniGroups[group]; !ok { + groupToMiniGroups[group] = miniGroupSet{} + } + groupToMiniGroups[group][miniGroup] = true + } + } + } + for groupTn, miniGroups := range groupToMiniGroups { + groupData := newGroupDataS(miniGroups, groupTn) + ly.treeNodesToGroups[groupTn] = groupData + ly.groups = append(ly.groups, groupData) + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////// + +func (ly *subnetsLayout) innerGroupsOfAGroup(group *groupDataS) groupSet { + allInnerGroups := groupSet{} + for _, group1 := range ly.groups { + if group.isInnerGroup(group1) { + allInnerGroups[group1] = true + } + } + return allInnerGroups +} + +func nonInnerGroups(groups groupSet) groupSet { + nonInnerGroups := maps.Clone(groups) + for group1 := range groups { + for group2 := range groups { + if group2.isInnerGroup(group1) { + delete(nonInnerGroups, group1) + } + } + } + return nonInnerGroups +} + +func intersectGroups(groups groupSet) map[*groupDataS]groupSet { + intersectGroups := map[*groupDataS]groupSet{} + + for group1 := range groups { + for group2 := range groups { + if group1 != group2 { + if group1.miniGroups.IsIntersect(group2.miniGroups) { + if _, ok := intersectGroups[group1]; !ok { + intersectGroups[group1] = groupSet{} + } + intersectGroups[group1][group2] = true + } + } + } + } + return intersectGroups +} + +func chooseGroupToSplit(intersectGroups map[*groupDataS]groupSet) *groupDataS { + bestSharingScore := 0 + var mostSharedGroup *groupDataS + for sharedGroup, sharedGroups := range intersectGroups { + if len(sharedGroups) > bestSharingScore || + (len(sharedGroups) == bestSharingScore && len(sharedGroup.miniGroups) < len(mostSharedGroup.miniGroups)) { + bestSharingScore = len(sharedGroups) + mostSharedGroup = sharedGroup + } + } + return mostSharedGroup +} + +// //////////////////////////////////////////////////////////////////////// +func (ly *subnetsLayout) createGroupsFromSplitGroups(group *groupDataS) { + miniGroupToGroupSet := ly.sortSplitMiniGroupsByGroupSet(group) + groupSetToMiniGroups, keysToGroupSet := groupSetToMiniGroups(miniGroupToGroupSet) + + for groupsKey, miniGroups := range groupSetToMiniGroups { + groups := keysToGroupSet[groupsKey] + ly.newGroupFromSplitMiniGroups(group, miniGroups, groups) + } +} + +func (ly *subnetsLayout) sortSplitMiniGroupsByGroupSet(group *groupDataS) map[*miniGroupDataS]groupSet { + splitMiniGroups := miniGroupSet{} + for splitGroup := range group.toSplitGroups { + for mn := range splitGroup.miniGroups { + splitMiniGroups[mn] = true + } + } + miniGroupToGroupSet := map[*miniGroupDataS]groupSet{} + for group := range ly.innerGroupsOfAGroup(group) { + for miniGroup := range group.miniGroups { + if splitMiniGroups[miniGroup] { + if _, ok := miniGroupToGroupSet[miniGroup]; !ok { + miniGroupToGroupSet[miniGroup] = groupSet{} + } + miniGroupToGroupSet[miniGroup][group] = true + } + } + } + return miniGroupToGroupSet +} + +func groupSetToMiniGroups(miniGroupToGroupSet map[*miniGroupDataS]groupSet) ( + groupSetToMiniGroups map[setAsKey]miniGroupSet, + keysToGroupSet map[setAsKey]groupSet) { + groupSetToMiniGroups = map[setAsKey]miniGroupSet{} + keysToGroupSet = map[setAsKey]groupSet{} + for miniGroup, groupSet := range miniGroupToGroupSet { + if _, ok := groupSetToMiniGroups[groupSet.AsKey()]; !ok { + groupSetToMiniGroups[groupSet.AsKey()] = miniGroupSet{} + } + groupSetToMiniGroups[groupSet.AsKey()][miniGroup] = true + keysToGroupSet[groupSet.AsKey()] = groupSet + } + return groupSetToMiniGroups, keysToGroupSet +} + +func (ly *subnetsLayout) newGroupFromSplitMiniGroups(group *groupDataS, miniGroups miniGroupSet, groups groupSet) { + var newGroup *groupDataS + for _, gr := range ly.groups { + if maps.Equal(gr.miniGroups, miniGroups) { + newGroup = gr + break + } + } + if newGroup == nil { + newGroup = newGroupDataS(miniGroups, nil) + ly.groups = append(ly.groups, newGroup) + + inTopGroup := false + for topGroup := range group.children { + if groups[topGroup] { + inTopGroup = true + break + } + } + if !inTopGroup { + group.children[newGroup] = true + } + } + for splitGroup := range group.toSplitGroups { + if groups[splitGroup] { + splitGroup.splitTo[newGroup] = true + newGroup.splitFrom[splitGroup] = true + } + } +} + +/////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////// +// calcZoneOrder() set the order of the zones in the matrix (every zone has one column) +// the output of the is a list of zones +// 1. for every pair of zones, calc the score of the pair +// 2. start a slice of a size of two, with the pair that has the highest score +// 3. in a loop - choose the best pair (z1,z2) such as z1 not in the slice and z2 in beginning/end of the slice. add z1 to the slice + +func (ly *subnetsLayout) calcZoneOrder() { + zonesScores := ly.calcZonePairScores() + zoneOrder := []TreeNodeInterface{} + for len(zonesScores) > 0 { + zoneToAdd, addToRight := chooseZoneToAdd(zonesScores, zoneOrder) + if addToRight == 1 { + zoneOrder = append(zoneOrder, zoneToAdd) + } else { + zoneOrder = append([]TreeNodeInterface{zoneToAdd}, zoneOrder...) + } + + if len(zoneOrder) > 2 { + if addToRight == 1 { + delete(zonesScores, zoneOrder[len(zoneOrder)-2]) + } else { + delete(zonesScores, zoneOrder[1]) + } + } + for _, zScores := range zonesScores { + delete(zScores, zoneToAdd) + } + for z, score := range zonesScores { + if len(score) == 0 { + delete(zonesScores, z) + } + } + } + for i, z := range zoneOrder { + ly.zonesCol[z] = i + } + for miniGroup := range ly.miniGroups { + if _, ok := ly.zonesCol[miniGroup.zone]; !ok { + ly.zonesCol[miniGroup.zone] = len(ly.zonesCol) + } + } +} + +func (ly *subnetsLayout) calcZonePairScores() map[TreeNodeInterface]map[TreeNodeInterface]int { + zonesScores := map[TreeNodeInterface]map[TreeNodeInterface]int{} + for _, group := range ly.groups { + for miniGroup1 := range group.miniGroups { + for miniGroup2 := range group.miniGroups { + if miniGroup1.zone != miniGroup2.zone { + if _, ok := zonesScores[miniGroup1.zone]; !ok { + zonesScores[miniGroup1.zone] = map[TreeNodeInterface]int{} + } + zonesScores[miniGroup1.zone][miniGroup2.zone] += 1 + } + } + } + } + return zonesScores +} +func chooseZoneToAdd(zonesScores map[TreeNodeInterface]map[TreeNodeInterface]int, + zoneOrder []TreeNodeInterface) (zoneToAdd TreeNodeInterface, + addToRight int) { + addToRight = 1 + if len(zoneOrder) > 0 { + zonesAtEdges := []TreeNodeInterface{zoneOrder[0], zoneOrder[len(zoneOrder)-1]} + bestScores := []int{0, 0} + zonesWithBestScore := []TreeNodeInterface{nil, nil} + for i, zToChoose := range zonesAtEdges { + for z, score := range zonesScores[zToChoose] { + if bestScores[i] < score { + bestScores[i] = score + zonesWithBestScore[i] = z + } + } + } + if bestScores[0] > bestScores[1] { + addToRight = 0 + } + if bestScores[addToRight] > 0 { + zoneToAdd = zonesWithBestScore[addToRight] + } + } + if zoneToAdd == nil { + // in case the zoneOrder is empty. or there are no score with one of the edge zones + bestScore := 0 + for z, friendsScore := range zonesScores { + for _, score := range friendsScore { + if score > bestScore { + bestScore = score + zoneToAdd = z + } + } + } + } + return zoneToAdd, addToRight +} + +func (ly *subnetsLayout) createMatrixes() { + ly.miniGroupsMatrix = make([][]*miniGroupDataS, len(ly.miniGroups)) + for i := range ly.miniGroupsMatrix { + ly.miniGroupsMatrix[i] = make([]*miniGroupDataS, len(ly.zonesCol)) + } + ly.subnetMatrix = make([][]TreeNodeInterface, len(ly.topFakeGroup.subnets)) + for i := range ly.subnetMatrix { + ly.subnetMatrix[i] = make([]TreeNodeInterface, len(ly.zonesCol)) + } +} + +// ///////////////////////////////////////////////////////////////////////// +// ///////////////////////////////////////////////////////////////////////// +// layoutGroup() is a recursive method to layout the group +// phases: +// 1. calc min and max Clos +// 2. calc the first row of the group +// 3. layout all the children +// 4. layout the miniGroups of the group +// 5. calc the last group. +// 6. fill the square [firstRow-lastRow, minCol-maxCol] with a fake miniGroups, as space holders +func (ly *subnetsLayout) layoutGroup(group *groupDataS, parentFirstRow int) { + childrenOrder := group.children.AsList() + sort.Slice(childrenOrder, func(i, j int) bool { + return len(childrenOrder[i].miniGroups) > len(childrenOrder[j].miniGroups) + }) + + minZoneCol, maxZoneCol, firstRow := ly.calcGroupLayoutBorders(group, parentFirstRow) + for _, child := range childrenOrder { + ly.layoutGroup(child, firstRow) + } + for miniGroup := range group.miniGroups { + if miniGroup.located { + continue + } + emptyCellRow := firstRow + for ly.miniGroupsMatrix[emptyCellRow][ly.zonesCol[miniGroup.zone]] != nil { + emptyCellRow++ + } + ly.miniGroupsMatrix[emptyCellRow][ly.zonesCol[miniGroup.zone]] = miniGroup + miniGroup.located = true + } + + if group != ly.topFakeGroup { + lastRow := parentFirstRow + for rIndex := lastRow; rIndex < len(ly.miniGroupsMatrix); rIndex++ { + for cIndex := minZoneCol; cIndex <= maxZoneCol; cIndex++ { + if ly.miniGroupsMatrix[rIndex][cIndex] != nil { + lastRow = rIndex + } + } + } + for rIndex := firstRow; rIndex <= lastRow; rIndex++ { + for cIndex := minZoneCol; cIndex <= maxZoneCol; cIndex++ { + if ly.miniGroupsMatrix[rIndex][cIndex] == nil { + ly.miniGroupsMatrix[rIndex][cIndex] = fakeMiniGroup + } + } + } + } +} + +func (ly *subnetsLayout) calcGroupLayoutBorders(group *groupDataS, parentFirstRow int) (minZoneCol, maxZoneCol, firstRow int) { + minZoneCol, maxZoneCol = len(ly.zonesCol), -1 + for mg := range group.miniGroups { + if minZoneCol > ly.zonesCol[mg.zone] { + minZoneCol = ly.zonesCol[mg.zone] + } + if maxZoneCol < ly.zonesCol[mg.zone] { + maxZoneCol = ly.zonesCol[mg.zone] + } + } + firstRow = parentFirstRow + for rIndex := firstRow; rIndex < len(ly.miniGroupsMatrix); rIndex++ { + for cIndex := minZoneCol; cIndex <= maxZoneCol; cIndex++ { + if ly.miniGroupsMatrix[rIndex][cIndex] != nil { + firstRow = rIndex + 1 + } + } + } + return minZoneCol, maxZoneCol, firstRow +} + +func (ly *subnetsLayout) setSubnetsMatrix() { + rIndex := 0 + for _, row := range ly.miniGroupsMatrix { + rowSize := 0 + for colIndex, miniGroup := range row { + if miniGroup == nil { + continue + } + i := 0 + if rowSize < len(miniGroup.subnets) { + rowSize = len(miniGroup.subnets) + } + for s := range miniGroup.subnets { + ly.subnetMatrix[rIndex+i][colIndex] = s + ly.subnetsIndexes[s] = indexes{rIndex + i, colIndex} + i++ + } + } + for colIndex, miniGroup := range row { + if miniGroup == nil { + continue + } + for i := 0; i < rowSize; i++ { + if ly.subnetMatrix[rIndex+i][colIndex] == nil { + ly.subnetMatrix[rIndex+i][colIndex] = fakeSubnet + } + } + } + rIndex += rowSize + } +} + +func (ly *subnetsLayout) doNotShowSplitGroups() { + for _, group := range ly.groups { + if len(group.splitTo) != 0 && group.treeNode != nil { + if ly.canShowGroup(group) { + group.reunion() + continue + } + group.treeNode.SetNotShownInDrawio() + } + } +} + +func (ly *subnetsLayout) createNewGroupsTreeNodes() { + for _, group := range ly.groups { + if len(group.splitTo) == 0 && group.treeNode == nil && len(group.splitFrom) > 0 { + subnets := []SquareTreeNodeInterface{} + for miniGroup := range group.miniGroups { + for subnet := range miniGroup.subnets { + subnets = append(subnets, subnet.(SquareTreeNodeInterface)) + } + } + if len(subnets) == 1 { + group.treeNode = subnets[0] + } else { + group.treeNode = GroupedSubnetsSquare(group.getVpc(), subnets) + } + ly.treeNodesToGroups[group.treeNode] = group + } + } +} + +func (ly *subnetsLayout) createNewLinesTreeNodes() { + for _, con := range getAllNodes(ly.network) { + if !con.IsLine() { + continue + } + srcTn, dstTn := con.(LineTreeNodeInterface).Src(), con.(LineTreeNodeInterface).Dst() + if !srcTn.NotShownInDrawio() && !dstTn.NotShownInDrawio() { + continue + } + allSrcTns, allDstTns := ly.allSplitTreeNodes(srcTn), ly.allSplitTreeNodes(dstTn) + handledSrcTns := groupTnSet{} + for sTn := range allSrcTns { + for dTn := range allDstTns { + switch { + case allSrcTns[dTn] && allDstTns[sTn] && handledSrcTns[dTn]: + case allSrcTns[dTn] && allDstTns[sTn]: + NewConnectivityLineTreeNode(ly.network, sTn, dTn, false, con.(*ConnectivityTreeNode).name) + default: + NewConnectivityLineTreeNode(ly.network, sTn, dTn, con.(*ConnectivityTreeNode).directed, con.(*ConnectivityTreeNode).name) + } + } + handledSrcTns[sTn] = true + } + con.SetNotShownInDrawio() + } +} + +func (ly *subnetsLayout) allSplitTreeNodes(tn TreeNodeInterface) groupTnSet { + group := ly.treeNodesToGroups[tn] + allTns := groupTnSet{tn: true} + if group != nil && len(group.splitTo) > 0 { + allTns = groupTnSet{} + for gr := range group.splitTo { + allTns[gr.treeNode] = true + } + } + return allTns +} + +func (ly *subnetsLayout) canShowGroup(group *groupDataS) bool { + firstRow, firstCol, lastRow, lastCol := len(ly.subnetMatrix), len(ly.subnetMatrix[0]), -1, -1 + for subnet := range group.subnets { + subnetIndexes := ly.subnetsIndexes[subnet] + if firstRow > subnetIndexes.row { + firstRow = subnetIndexes.row + } + if firstCol > subnetIndexes.col { + firstCol = subnetIndexes.col + } + if lastRow < subnetIndexes.row { + lastRow = subnetIndexes.row + } + if lastCol < subnetIndexes.col { + lastCol = subnetIndexes.col + } + } + nSubnets := 0 + for r := firstRow; r <= lastRow; r++ { + for c := firstCol; c <= lastCol; c++ { + subnet := ly.subnetMatrix[r][c] + if subnet != nil && subnet != fakeSubnet { + if !group.subnets[subnet] { + return false + } + nSubnets++ + } + } + } + + return nSubnets == len(group.subnets) +} diff --git a/pkg/drawio/treeNodeInterface.go b/pkg/drawio/treeNodeInterface.go index ce9369182..b60191f62 100644 --- a/pkg/drawio/treeNodeInterface.go +++ b/pkg/drawio/treeNodeInterface.go @@ -52,6 +52,7 @@ type TreeNodeInterface interface { setParent(TreeNodeInterface) setLocation(location *Location) NotShownInDrawio() bool + SetNotShownInDrawio() setID() ///////////////////////////// diff --git a/pkg/ibmvpc/analysis_output_test.go b/pkg/ibmvpc/analysis_output_test.go index 83b4bcc6f..ffcfd6bb4 100644 --- a/pkg/ibmvpc/analysis_output_test.go +++ b/pkg/ibmvpc/analysis_output_test.go @@ -156,6 +156,18 @@ var tests = []*vpcGeneralTest{ useCases: []vpcmodel.OutputUseCase{vpcmodel.AllSubnets}, format: vpcmodel.Text, }, + { + name: "acl_testing5", + useCases: []vpcmodel.OutputUseCase{vpcmodel.AllSubnets}, + grouping: true, + format: vpcmodel.DRAWIO, + }, + { + name: "demo_with_instances", + useCases: []vpcmodel.OutputUseCase{vpcmodel.AllSubnets}, + grouping: true, + format: vpcmodel.DRAWIO, + }, // batch1: cover all use-cases, with text output format , no grouping { name: "acl_testing3", @@ -387,20 +399,20 @@ var formatsAvoidComparison = map[vpcmodel.OutFormat]bool{vpcmodel.ARCHDRAWIO: tr /*var formatsAvoidOutputGeneration = map[vpcmodel.OutFormat]bool{vpcmodel.ARCHDRAWIO: true, vpcmodel.DRAWIO: true} func TestAllWithGeneration(t *testing.T) { - // tests is the list of tests to run - for testIdx := range tests { - tt := tests[testIdx] - // todo - remove the following if when drawio is stable - if formatsAvoidOutputGeneration[tt.format] { - tt.mode = outputIgnore - } else { - tt.mode = outputGeneration - } +// tests is the list of tests to run +for testIdx := range tests { +tt := tests[testIdx] +// todo - remove the following if when drawio is stable +if formatsAvoidOutputGeneration[tt.format] { +tt.mode = outputIgnore +} else { +tt.mode = outputGeneration +} t.Run(tt.name, func(t *testing.T) { - t.Parallel() - tt.runTest(t) - }) - } +t.Parallel() +tt.runTest(t) +}) +} fmt.Println("done") }*/ diff --git a/pkg/ibmvpc/ibmDrawioGenerator.go b/pkg/ibmvpc/ibmDrawioGenerator.go index 926998845..e442b7185 100644 --- a/pkg/ibmvpc/ibmDrawioGenerator.go +++ b/pkg/ibmvpc/ibmDrawioGenerator.go @@ -5,6 +5,22 @@ import ( "github.com/np-guard/vpc-network-config-analyzer/pkg/vpcmodel" ) +func (v *VPC) ShowOnSubnetMode() bool { return true } +func (z *Zone) ShowOnSubnetMode() bool { return true } +func (s *Subnet) ShowOnSubnetMode() bool { return true } +func (sgl *SecurityGroupLayer) ShowOnSubnetMode() bool { return false } +func (nl *NaclLayer) ShowOnSubnetMode() bool { return true } +func (ni *NetworkInterface) ShowOnSubnetMode() bool { return false } +func (n *IKSNode) ShowOnSubnetMode() bool { return false } +func (r *ReservedIP) ShowOnSubnetMode() bool { return false } +func (v *Vsi) ShowOnSubnetMode() bool { return false } +func (v *Vpe) ShowOnSubnetMode() bool { return false } +func (pgw *PublicGateway) ShowOnSubnetMode() bool { return true } +func (fip *FloatingIP) ShowOnSubnetMode() bool { return false } + +// todo - support TransitGateway? +func (tgw *TransitGateway) ShowOnSubnetMode() bool { return false } + // implementations of the GenerateDrawioTreeNode() for resource defined in ibmvpc: func (v *VPC) GenerateDrawioTreeNode(gen *vpcmodel.DrawioGenerator) drawio.TreeNodeInterface { return drawio.NewVpcTreeNode(gen.Cloud(), v.Name()) diff --git a/pkg/vpcmodel/drawioGenerator.go b/pkg/vpcmodel/drawioGenerator.go index d6e628908..ddb78bb0b 100644 --- a/pkg/vpcmodel/drawioGenerator.go +++ b/pkg/vpcmodel/drawioGenerator.go @@ -8,6 +8,7 @@ import ( type DrawioResourceIntf interface { GenerateDrawioTreeNode(gen *DrawioGenerator) drawio.TreeNodeInterface IsExternal() bool + ShowOnSubnetMode() bool } // DrawioGenerator is the struct that generate the drawio tree. @@ -55,11 +56,23 @@ func (gen *DrawioGenerator) TreeNode(res DrawioResourceIntf) drawio.TreeNodeInte func (exn *ExternalNetwork) GenerateDrawioTreeNode(gen *DrawioGenerator) drawio.TreeNodeInterface { return drawio.NewInternetTreeNode(gen.PublicNetwork(), exn.CidrStr) } +func (exn *ExternalNetwork) ShowOnSubnetMode() bool { return true } +func (g *groupedEndpointsElems) ShowOnSubnetMode() bool { return true } +func (g *groupedExternalNodes) ShowOnSubnetMode() bool { return true } +func (e *edgeInfo) ShowOnSubnetMode() bool { return true } func (g *groupedEndpointsElems) GenerateDrawioTreeNode(gen *DrawioGenerator) drawio.TreeNodeInterface { if len(*g) == 1 { return gen.TreeNode((*g)[0]) } + if gen.TreeNode((*g)[0]).IsSquare() && gen.TreeNode((*g)[0]).(drawio.SquareTreeNodeInterface).IsSubnet() { + groupedSubnetsTNs := make([]drawio.SquareTreeNodeInterface, len(*g)) + for i, node := range *g { + groupedSubnetsTNs[i] = gen.TreeNode(node).(drawio.SquareTreeNodeInterface) + } + vpcTn := groupedSubnetsTNs[0].Parent().Parent().(*drawio.VpcTreeNode) + return drawio.GroupedSubnetsSquare(vpcTn, groupedSubnetsTNs) + } groupedIconsTNs := make([]drawio.IconTreeNodeInterface, len(*g)) for i, node := range *g { groupedIconsTNs[i] = gen.TreeNode(node).(drawio.IconTreeNodeInterface) diff --git a/pkg/vpcmodel/drawioOutput.go b/pkg/vpcmodel/drawioOutput.go index 72063f36b..0179f3574 100644 --- a/pkg/vpcmodel/drawioOutput.go +++ b/pkg/vpcmodel/drawioOutput.go @@ -26,22 +26,34 @@ func (e *edgeInfo) IsExternal() bool { type DrawioOutputFormatter struct { cConfig *VPCConfig - conn *VPCConnectivity + conn *GroupConnLines gen *DrawioGenerator routers map[drawio.TreeNodeInterface]drawio.IconTreeNodeInterface + uc OutputUseCase } -func (d *DrawioOutputFormatter) init(cConfig *VPCConfig, conn *VPCConnectivity) { +func (d *DrawioOutputFormatter) init(cConfig *VPCConfig, conn *GroupConnLines, uc OutputUseCase) { d.cConfig = cConfig d.conn = conn + d.uc = uc d.gen = NewDrawioGenerator(cConfig.CloudName) d.routers = map[drawio.TreeNodeInterface]drawio.IconTreeNodeInterface{} } +func (d *DrawioOutputFormatter) writeOutputGeneric(outFile string) ( + *SingleAnalysisOutput, error) { + d.createDrawioTree() + err := drawio.CreateDrawioConnectivityMapFile(d.gen.Network(), outFile, d.uc == AllSubnets) + return &SingleAnalysisOutput{}, err +} + func (d *DrawioOutputFormatter) createDrawioTree() { d.createNodeSets() - d.createNodes() - d.createFilters() + if d.uc != AllSubnets { + // todo - support filters on subnet mode + d.createNodes() + d.createFilters() + } d.createRouters() if d.conn != nil { d.createEdges() @@ -50,13 +62,15 @@ func (d *DrawioOutputFormatter) createDrawioTree() { func (d *DrawioOutputFormatter) createNodeSets() { for _, ns := range d.cConfig.NodeSets { - d.gen.TreeNode(ns) + if d.showResource(ns) { + d.gen.TreeNode(ns) + } } } func (d *DrawioOutputFormatter) createNodes() { for _, n := range d.cConfig.Nodes { - if !n.IsExternal() { + if !n.IsExternal() && d.showResource(n) { d.gen.TreeNode(n) } } @@ -64,16 +78,21 @@ func (d *DrawioOutputFormatter) createNodes() { func (d *DrawioOutputFormatter) createFilters() { for _, fl := range d.cConfig.FilterResources { - d.gen.TreeNode(fl) + if d.showResource(fl) { + d.gen.TreeNode(fl) + } } } func (d *DrawioOutputFormatter) createRouters() { for _, r := range d.cConfig.RoutingResources { - rTn := d.gen.TreeNode(r) - - for _, ni := range r.Src() { - d.routers[d.gen.TreeNode(ni)] = rTn.(drawio.IconTreeNodeInterface) + if d.showResource(r) { + rTn := d.gen.TreeNode(r) + for _, ni := range r.Src() { + if d.showResource(ni) { + d.routers[d.gen.TreeNode(ni)] = rTn.(drawio.IconTreeNodeInterface) + } + } } } } @@ -85,7 +104,7 @@ func (d *DrawioOutputFormatter) createEdges() { label string } isEdgeDirected := map[edgeKey]bool{} - for _, line := range d.conn.GroupedConnectivity.GroupedLines { + for _, line := range d.conn.GroupedLines { src := line.src dst := line.dst e := edgeKey{src, dst, line.ConnLabel()} @@ -99,16 +118,22 @@ func (d *DrawioOutputFormatter) createEdges() { } for e, directed := range isEdgeDirected { ei := &edgeInfo{e.src, e.dst, e.label, directed} - cn := d.gen.TreeNode(ei).(*drawio.ConnectivityTreeNode) - if d.routers[cn.Src()] != nil && e.dst.IsExternal() { - cn.SetRouter(d.routers[cn.Src()], false) - } - if d.routers[cn.Dst()] != nil && e.src.IsExternal() { - cn.SetRouter(d.routers[cn.Dst()], true) + if d.showResource(ei) { + cn := d.gen.TreeNode(ei).(*drawio.ConnectivityTreeNode) + if d.routers[cn.Src()] != nil && e.dst.IsExternal() { + cn.SetRouter(d.routers[cn.Src()], false) + } + if d.routers[cn.Dst()] != nil && e.src.IsExternal() { + cn.SetRouter(d.routers[cn.Dst()], true) + } } } } +func (d *DrawioOutputFormatter) showResource(res DrawioResourceIntf) bool { + return d.uc != AllSubnets || res.ShowOnSubnetMode() +} + func (d *DrawioOutputFormatter) WriteOutput(c1, c2 *VPCConfig, conn *VPCConnectivity, subnetsConn *VPCsubnetConnectivity, @@ -116,16 +141,23 @@ func (d *DrawioOutputFormatter) WriteOutput(c1, c2 *VPCConfig, outFile string, grouping bool, uc OutputUseCase) (*SingleAnalysisOutput, error) { - var err error switch uc { case AllEndpoints: - d.init(c1, conn) - d.createDrawioTree() - err = drawio.CreateDrawioConnectivityMapFile(d.gen.Network(), outFile) - case AllSubnets, SingleSubnet: - err = errors.New("SubnetLevel/SingleSubnet use case not supported for draw.io format") + var gConn *GroupConnLines + if conn != nil { + gConn = conn.GroupedConnectivity + } + d.init(c1, gConn, uc) + case AllSubnets: + var gConn *GroupConnLines + if subnetsConn != nil { + gConn = subnetsConn.GroupedConnectivity + } + d.init(subnetsConn.VPCConfig, gConn, uc) + default: + return &SingleAnalysisOutput{}, errors.New("use case is not currently supported for draw.io format") } - return &SingleAnalysisOutput{}, err + return d.writeOutputGeneric(outFile) } // ///////////////////////////////////////////////////////////////// @@ -144,11 +176,5 @@ func (d *ArchDrawioOutputFormatter) WriteOutput(c1, c2 *VPCConfig, outFile string, grouping bool, uc OutputUseCase) (*SingleAnalysisOutput, error) { - switch uc { - case AllEndpoints: - return d.DrawioOutputFormatter.WriteOutput(c1, c2, nil, nil, nil, outFile, grouping, uc) - case AllSubnets, SingleSubnet: - return d.DrawioOutputFormatter.WriteOutput(nil, c2, nil, nil, nil, outFile, grouping, uc) - } - return &SingleAnalysisOutput{}, nil + return d.DrawioOutputFormatter.WriteOutput(c1, c2, nil, nil, nil, outFile, grouping, uc) } diff --git a/pkg/vpcmodel/grouping_test.go b/pkg/vpcmodel/grouping_test.go index 852a34e70..8bd37cdf8 100644 --- a/pkg/vpcmodel/grouping_test.go +++ b/pkg/vpcmodel/grouping_test.go @@ -40,7 +40,8 @@ func (m *mockNetIntf) ZoneName() string { func (m *mockNetIntf) GenerateDrawioTreeNode(gen *DrawioGenerator) drawio.TreeNodeInterface { return nil } -func (m *mockNetIntf) IsExternal() bool { return m.isPublic } +func (m *mockNetIntf) IsExternal() bool { return m.isPublic } +func (m *mockNetIntf) ShowOnSubnetMode() bool { return false } func (m *mockNetIntf) VPC() VPCResourceIntf { return nil @@ -77,7 +78,8 @@ func (m *mockSubnet) ZoneName() string { func (m *mockSubnet) GenerateDrawioTreeNode(gen *DrawioGenerator) drawio.TreeNodeInterface { return nil } -func (m *mockSubnet) IsExternal() bool { return false } +func (m *mockSubnet) IsExternal() bool { return false } +func (m *mockSubnet) ShowOnSubnetMode() bool { return true } func (m *mockSubnet) VPC() VPCResourceIntf { return nil }