mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-06-26 08:14:42 +00:00
Service Map (#623)
* debug builds and gcflags * update dockerfile for debug * service map routes and controller * service map graph structure * service map interface and new methods * adding service map edges from mizu entries * new service map count methods * implementing the status endpoint * ServiceMapResponse and ServiceMapEdge models * service map get endpoint logic * reset logic and endpoint * fixed service map get status * improvements to graph node structure * front-end implementation and service map buttons * new render endpoint to render the graph in real time * spinner sass * new ServiceMapModal component * testing react-force-graph-2d lib * Improvements to service map graph structure, added node id and updated edge source/destination type * Revert "testing react-force-graph-2d lib" This reverts commit1153938386
. * testing react-graph-vis lib * updated to work with react-graph-vis lib * removed render endpoint * go mod tidy * serviceMap config flag * using the serviceMap config flag * passing mizu config to service map as a dependency * service map tests * Removed print functions * finished service map tests * new service property * service map controller tests * moved service map reset button to service map modal reset closes the modal * service map modal refresh button and logic * reset button resets data and refresh * service map modal close button * node size/edge size based on the count value edge color based on protocol * nodes and edges shadow * enabled physics to avoid node overlap, changed kafka protocol color to dark green * showing edges count values and fixed bidirectional edges overlap * go mod tidy * removed console.log * Using the destination node protocol instead of the source node protocol * Revert "debug builds and gcflags" Addressed by #624 and #626 This reverts commit17ecaece3e
. * Revert "update dockerfile for debug" Addressed by #635 This reverts commit5dfc15b140
. * using the entire tap Protocol struct instead of only the protocol name * using the backend protocol background color for node colors * fixed test, the node list order can change * re-factoring to get 100% coverage * using protocol colors just for edges * re-factored service map to use TCP Entry data. Node key is the entry ip-address instead of the name * fallback to ip-address when entry name is unresolved * re-factored front-end * adjustment to main div style * added support for multiple protocols for the same edge * using the item protocol instead of the extension variable * fixed controller tests * displaying service name and ip-address on graph nodes * fixed service map test, we cannot guarantee the slice order * auth middleware * created a new pkg for the service map * re-factoring * re-factored front-end * reverting the import order as previous * Aligning with other UI feature flags handling * we don't need to get the status anymore, we have window["isServiceMapEnabled"] * small adjustments * renamed from .tsx to .ts * button styles and minor improvements * moved service map modal from trafficPage to app component Co-authored-by: Igor Gov <igor.govorov1@gmail.com>
This commit is contained in:
parent
7477f867f9
commit
d5fd2ff1da
@ -19,6 +19,7 @@ require (
|
|||||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||||
github.com/ory/kratos-client-go v0.8.2-alpha.1
|
github.com/ory/kratos-client-go v0.8.2-alpha.1
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/up9inc/basenine/client/go v0.0.0-20220110083745-04fbc6c2068d
|
github.com/up9inc/basenine/client/go v0.0.0-20220110083745-04fbc6c2068d
|
||||||
github.com/up9inc/mizu/shared v0.0.0
|
github.com/up9inc/mizu/shared v0.0.0
|
||||||
github.com/up9inc/mizu/tap v0.0.0
|
github.com/up9inc/mizu/tap v0.0.0
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"mizuserver/pkg/models"
|
"mizuserver/pkg/models"
|
||||||
"mizuserver/pkg/oas"
|
"mizuserver/pkg/oas"
|
||||||
"mizuserver/pkg/routes"
|
"mizuserver/pkg/routes"
|
||||||
|
"mizuserver/pkg/servicemap"
|
||||||
"mizuserver/pkg/up9"
|
"mizuserver/pkg/up9"
|
||||||
"mizuserver/pkg/utils"
|
"mizuserver/pkg/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -153,6 +154,9 @@ func enableExpFeatureIfNeeded() {
|
|||||||
if config.Config.OAS {
|
if config.Config.OAS {
|
||||||
oas.GetOasGeneratorInstance().Start()
|
oas.GetOasGeneratorInstance().Start()
|
||||||
}
|
}
|
||||||
|
if config.Config.ServiceMap {
|
||||||
|
servicemap.GetInstance().SetConfig(config.Config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureBasenineServer(host string, port string) {
|
func configureBasenineServer(host string, port string) {
|
||||||
@ -254,10 +258,12 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
|||||||
routes.UserRoutes(app)
|
routes.UserRoutes(app)
|
||||||
routes.InstallRoutes(app)
|
routes.InstallRoutes(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Config.OAS {
|
if config.Config.OAS {
|
||||||
routes.OASRoutes(app)
|
routes.OASRoutes(app)
|
||||||
}
|
}
|
||||||
|
if config.Config.ServiceMap {
|
||||||
|
routes.ServiceMapRoutes(app)
|
||||||
|
}
|
||||||
|
|
||||||
routes.QueryRoutes(app)
|
routes.QueryRoutes(app)
|
||||||
routes.EntriesRoutes(app)
|
routes.EntriesRoutes(app)
|
||||||
@ -286,6 +292,7 @@ func setUIFlags() error {
|
|||||||
|
|
||||||
replacedContent := strings.Replace(string(read), "__IS_STANDALONE__", strconv.FormatBool(config.Config.StandaloneMode), 1)
|
replacedContent := strings.Replace(string(read), "__IS_STANDALONE__", strconv.FormatBool(config.Config.StandaloneMode), 1)
|
||||||
replacedContent = strings.Replace(replacedContent, "__IS_OAS_ENABLED__", strconv.FormatBool(config.Config.OAS), 1)
|
replacedContent = strings.Replace(replacedContent, "__IS_OAS_ENABLED__", strconv.FormatBool(config.Config.OAS), 1)
|
||||||
|
replacedContent = strings.Replace(replacedContent, "__IS_SERVICE_MAP_ENABLED__", strconv.FormatBool(config.Config.ServiceMap), 1)
|
||||||
|
|
||||||
err = ioutil.WriteFile(uiIndexPath, []byte(replacedContent), 0)
|
err = ioutil.WriteFile(uiIndexPath, []byte(replacedContent), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"mizuserver/pkg/servicemap"
|
||||||
|
|
||||||
"github.com/google/martian/har"
|
"github.com/google/martian/har"
|
||||||
"github.com/up9inc/mizu/shared"
|
"github.com/up9inc/mizu/shared"
|
||||||
"github.com/up9inc/mizu/shared/logger"
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
@ -146,6 +148,8 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
connection.SendText(string(data))
|
connection.SendText(string(data))
|
||||||
|
|
||||||
|
servicemap.GetInstance().NewTCPEntry(mizuEntry.Source, mizuEntry.Destination, &item.Protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
agent/pkg/controllers/service_map_controller.go
Normal file
36
agent/pkg/controllers/service_map_controller.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mizuserver/pkg/servicemap"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceMapController struct {
|
||||||
|
service servicemap.ServiceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServiceMapController() *ServiceMapController {
|
||||||
|
return &ServiceMapController{
|
||||||
|
service: servicemap.GetInstance(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapController) Status(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, s.service.GetStatus())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapController) Get(c *gin.Context) {
|
||||||
|
response := &servicemap.ServiceMapResponse{
|
||||||
|
Status: s.service.GetStatus(),
|
||||||
|
Nodes: s.service.GetNodes(),
|
||||||
|
Edges: s.service.GetEdges(),
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapController) Reset(c *gin.Context) {
|
||||||
|
s.service.Reset()
|
||||||
|
s.Status(c)
|
||||||
|
}
|
147
agent/pkg/controllers/service_map_controller_test.go
Normal file
147
agent/pkg/controllers/service_map_controller_test.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"mizuserver/pkg/servicemap"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
a = "aService"
|
||||||
|
b = "bService"
|
||||||
|
Ip = "127.0.0.1"
|
||||||
|
Port = "80"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TCPEntryA = &tapApi.TCP{
|
||||||
|
Name: a,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, a),
|
||||||
|
}
|
||||||
|
TCPEntryB = &tapApi.TCP{
|
||||||
|
Name: b,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, b),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var ProtocolHttp = &tapApi.Protocol{
|
||||||
|
Name: "http",
|
||||||
|
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
|
||||||
|
Abbreviation: "HTTP",
|
||||||
|
Macro: "http",
|
||||||
|
Version: "1.1",
|
||||||
|
BackgroundColor: "#205cf5",
|
||||||
|
ForegroundColor: "#ffffff",
|
||||||
|
FontSize: 12,
|
||||||
|
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
||||||
|
Ports: []string{"80", "443", "8080"},
|
||||||
|
Priority: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMapControllerSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
c *ServiceMapController
|
||||||
|
w *httptest.ResponseRecorder
|
||||||
|
g *gin.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapControllerSuite) SetupTest() {
|
||||||
|
s.c = NewServiceMapController()
|
||||||
|
s.c.service.SetConfig(&shared.MizuAgentConfig{
|
||||||
|
ServiceMap: true,
|
||||||
|
})
|
||||||
|
s.c.service.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||||
|
|
||||||
|
s.w = httptest.NewRecorder()
|
||||||
|
s.g, _ = gin.CreateTestContext(s.w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapControllerSuite) TestGetStatus() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
s.c.Status(s.g)
|
||||||
|
assert.Equal(http.StatusOK, s.w.Code)
|
||||||
|
|
||||||
|
var status servicemap.ServiceMapStatus
|
||||||
|
err := json.Unmarshal(s.w.Body.Bytes(), &status)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal("enabled", status.Status)
|
||||||
|
assert.Equal(1, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(2, status.NodeCount)
|
||||||
|
assert.Equal(1, status.EdgeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapControllerSuite) TestGet() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
s.c.Get(s.g)
|
||||||
|
assert.Equal(http.StatusOK, s.w.Code)
|
||||||
|
|
||||||
|
var response servicemap.ServiceMapResponse
|
||||||
|
err := json.Unmarshal(s.w.Body.Bytes(), &response)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
// response status
|
||||||
|
assert.Equal("enabled", response.Status.Status)
|
||||||
|
assert.Equal(1, response.Status.EntriesProcessedCount)
|
||||||
|
assert.Equal(2, response.Status.NodeCount)
|
||||||
|
assert.Equal(1, response.Status.EdgeCount)
|
||||||
|
|
||||||
|
// response nodes
|
||||||
|
aNode := servicemap.ServiceMapNode{
|
||||||
|
Id: 1,
|
||||||
|
Name: TCPEntryA.IP,
|
||||||
|
Entry: TCPEntryA,
|
||||||
|
Count: 1,
|
||||||
|
}
|
||||||
|
bNode := servicemap.ServiceMapNode{
|
||||||
|
Id: 2,
|
||||||
|
Name: TCPEntryB.IP,
|
||||||
|
Entry: TCPEntryB,
|
||||||
|
Count: 1,
|
||||||
|
}
|
||||||
|
assert.Contains(response.Nodes, aNode)
|
||||||
|
assert.Contains(response.Nodes, bNode)
|
||||||
|
assert.Len(response.Nodes, 2)
|
||||||
|
|
||||||
|
// response edges
|
||||||
|
assert.Equal([]servicemap.ServiceMapEdge{
|
||||||
|
{
|
||||||
|
Source: aNode,
|
||||||
|
Destination: bNode,
|
||||||
|
Protocol: ProtocolHttp,
|
||||||
|
Count: 1,
|
||||||
|
},
|
||||||
|
}, response.Edges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapControllerSuite) TestGetReset() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
s.c.Reset(s.g)
|
||||||
|
assert.Equal(http.StatusOK, s.w.Code)
|
||||||
|
|
||||||
|
var status servicemap.ServiceMapStatus
|
||||||
|
err := json.Unmarshal(s.w.Body.Bytes(), &status)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal("enabled", status.Status)
|
||||||
|
assert.Equal(0, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(0, status.NodeCount)
|
||||||
|
assert.Equal(0, status.EdgeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceMapControllerSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ServiceMapControllerSuite))
|
||||||
|
}
|
19
agent/pkg/routes/service_map_routes.go
Normal file
19
agent/pkg/routes/service_map_routes.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
"mizuserver/pkg/middlewares"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServiceMapRoutes(ginApp *gin.Engine) {
|
||||||
|
routeGroup := ginApp.Group("/servicemap")
|
||||||
|
routeGroup.Use(middlewares.RequiresAuth())
|
||||||
|
|
||||||
|
controller := controllers.NewServiceMapController()
|
||||||
|
|
||||||
|
routeGroup.GET("/status", controller.Status)
|
||||||
|
routeGroup.GET("/get", controller.Get)
|
||||||
|
routeGroup.GET("/reset", controller.Reset)
|
||||||
|
}
|
32
agent/pkg/servicemap/models.go
Normal file
32
agent/pkg/servicemap/models.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package servicemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceMapStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
EntriesProcessedCount int `json:"entriesProcessedCount"`
|
||||||
|
NodeCount int `json:"nodeCount"`
|
||||||
|
EdgeCount int `json:"edgeCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMapResponse struct {
|
||||||
|
Status ServiceMapStatus `json:"status"`
|
||||||
|
Nodes []ServiceMapNode `json:"nodes"`
|
||||||
|
Edges []ServiceMapEdge `json:"edges"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMapNode struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Entry *tapApi.TCP `json:"entry"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMapEdge struct {
|
||||||
|
Source ServiceMapNode `json:"source"`
|
||||||
|
Destination ServiceMapNode `json:"destination"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Protocol *tapApi.Protocol `json:"protocol"`
|
||||||
|
}
|
271
agent/pkg/servicemap/servicemap.go
Normal file
271
agent/pkg/servicemap/servicemap.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package servicemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceMapEnabled = "enabled"
|
||||||
|
ServiceMapDisabled = "disabled"
|
||||||
|
UnresolvedNodeName = "unresolved"
|
||||||
|
)
|
||||||
|
|
||||||
|
var instance *serviceMap
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func GetInstance() ServiceMap {
|
||||||
|
once.Do(func() {
|
||||||
|
instance = newServiceMap()
|
||||||
|
logger.Log.Debug("Service Map Initialized")
|
||||||
|
})
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceMap struct {
|
||||||
|
config *shared.MizuAgentConfig
|
||||||
|
graph *graph
|
||||||
|
entriesProcessed int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMap interface {
|
||||||
|
SetConfig(config *shared.MizuAgentConfig)
|
||||||
|
IsEnabled() bool
|
||||||
|
NewTCPEntry(source *tapApi.TCP, destination *tapApi.TCP, protocol *tapApi.Protocol)
|
||||||
|
GetStatus() ServiceMapStatus
|
||||||
|
GetNodes() []ServiceMapNode
|
||||||
|
GetEdges() []ServiceMapEdge
|
||||||
|
GetEntriesProcessedCount() int
|
||||||
|
GetNodesCount() int
|
||||||
|
GetEdgesCount() int
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServiceMap() *serviceMap {
|
||||||
|
return &serviceMap{
|
||||||
|
config: nil,
|
||||||
|
entriesProcessed: 0,
|
||||||
|
graph: newDirectedGraph(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type key string
|
||||||
|
|
||||||
|
type entryData struct {
|
||||||
|
key key
|
||||||
|
entry *tapApi.TCP
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeData struct {
|
||||||
|
id int
|
||||||
|
entry *tapApi.TCP
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type edgeProtocol struct {
|
||||||
|
protocol *tapApi.Protocol
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type edgeData struct {
|
||||||
|
data map[key]*edgeProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
type graph struct {
|
||||||
|
Nodes map[key]*nodeData
|
||||||
|
Edges map[key]map[key]*edgeData
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDirectedGraph() *graph {
|
||||||
|
return &graph{
|
||||||
|
Nodes: make(map[key]*nodeData),
|
||||||
|
Edges: make(map[key]map[key]*edgeData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNodeData(id int, e *tapApi.TCP) *nodeData {
|
||||||
|
return &nodeData{
|
||||||
|
id: id,
|
||||||
|
entry: e,
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEdgeData(p *tapApi.Protocol) *edgeData {
|
||||||
|
return &edgeData{
|
||||||
|
data: map[key]*edgeProtocol{
|
||||||
|
key(p.Name): {
|
||||||
|
protocol: p,
|
||||||
|
count: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) nodeExists(k key) (*nodeData, bool) {
|
||||||
|
n, ok := s.graph.Nodes[k]
|
||||||
|
return n, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) addNode(k key, e *tapApi.TCP) (*nodeData, bool) {
|
||||||
|
nd, exists := s.nodeExists(k)
|
||||||
|
if !exists {
|
||||||
|
s.graph.Nodes[k] = newNodeData(len(s.graph.Nodes)+1, e)
|
||||||
|
return s.graph.Nodes[k], true
|
||||||
|
}
|
||||||
|
return nd, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) addEdge(u, v *entryData, p *tapApi.Protocol) {
|
||||||
|
if n, ok := s.addNode(u.key, u.entry); !ok {
|
||||||
|
n.count++
|
||||||
|
}
|
||||||
|
if n, ok := s.addNode(v.key, v.entry); !ok {
|
||||||
|
n.count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.graph.Edges[u.key]; !ok {
|
||||||
|
s.graph.Edges[u.key] = make(map[key]*edgeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// new edge u -> v pair
|
||||||
|
// protocol is the same for u and v
|
||||||
|
if e, ok := s.graph.Edges[u.key][v.key]; ok {
|
||||||
|
// edge data already exists for u -> v pair
|
||||||
|
// we have a new protocol for this u -> v pair
|
||||||
|
|
||||||
|
k := key(p.Name)
|
||||||
|
if pd, pOk := e.data[k]; pOk {
|
||||||
|
// protocol key already exists, just increment the count
|
||||||
|
pd.count++
|
||||||
|
} else {
|
||||||
|
// new protocol key
|
||||||
|
e.data[k] = &edgeProtocol{
|
||||||
|
protocol: p,
|
||||||
|
count: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// new edge data for u -> v pair
|
||||||
|
s.graph.Edges[u.key][v.key] = newEdgeData(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.entriesProcessed++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) SetConfig(config *shared.MizuAgentConfig) {
|
||||||
|
s.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) IsEnabled() bool {
|
||||||
|
if s.config != nil && s.config.ServiceMap {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) NewTCPEntry(src *tapApi.TCP, dst *tapApi.TCP, p *tapApi.Protocol) {
|
||||||
|
if !s.IsEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
srcEntry := &entryData{
|
||||||
|
key: key(src.IP),
|
||||||
|
entry: src,
|
||||||
|
}
|
||||||
|
if len(srcEntry.entry.Name) == 0 {
|
||||||
|
srcEntry.entry.Name = UnresolvedNodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
dstEntry := &entryData{
|
||||||
|
key: key(dst.IP),
|
||||||
|
entry: dst,
|
||||||
|
}
|
||||||
|
if len(dstEntry.entry.Name) == 0 {
|
||||||
|
dstEntry.entry.Name = UnresolvedNodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
s.addEdge(srcEntry, dstEntry, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetStatus() ServiceMapStatus {
|
||||||
|
status := ServiceMapDisabled
|
||||||
|
if s.IsEnabled() {
|
||||||
|
status = ServiceMapEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServiceMapStatus{
|
||||||
|
Status: status,
|
||||||
|
EntriesProcessedCount: s.entriesProcessed,
|
||||||
|
NodeCount: s.GetNodesCount(),
|
||||||
|
EdgeCount: s.GetEdgesCount(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetNodes() []ServiceMapNode {
|
||||||
|
var nodes []ServiceMapNode
|
||||||
|
for i, n := range s.graph.Nodes {
|
||||||
|
nodes = append(nodes, ServiceMapNode{
|
||||||
|
Id: n.id,
|
||||||
|
Name: string(i),
|
||||||
|
Entry: n.entry,
|
||||||
|
Count: n.count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetEdges() []ServiceMapEdge {
|
||||||
|
var edges []ServiceMapEdge
|
||||||
|
for u, m := range s.graph.Edges {
|
||||||
|
for v := range m {
|
||||||
|
for _, p := range s.graph.Edges[u][v].data {
|
||||||
|
edges = append(edges, ServiceMapEdge{
|
||||||
|
Source: ServiceMapNode{
|
||||||
|
Id: s.graph.Nodes[u].id,
|
||||||
|
Name: string(u),
|
||||||
|
Entry: s.graph.Nodes[u].entry,
|
||||||
|
Count: s.graph.Nodes[u].count,
|
||||||
|
},
|
||||||
|
Destination: ServiceMapNode{
|
||||||
|
Id: s.graph.Nodes[v].id,
|
||||||
|
Name: string(v),
|
||||||
|
Entry: s.graph.Nodes[v].entry,
|
||||||
|
Count: s.graph.Nodes[v].count,
|
||||||
|
},
|
||||||
|
Count: p.count,
|
||||||
|
Protocol: p.protocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edges
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetEntriesProcessedCount() int {
|
||||||
|
return s.entriesProcessed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetNodesCount() int {
|
||||||
|
return len(s.graph.Nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) GetEdgesCount() int {
|
||||||
|
var count int
|
||||||
|
for u, m := range s.graph.Edges {
|
||||||
|
for v := range m {
|
||||||
|
for range s.graph.Edges[u][v].data {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceMap) Reset() {
|
||||||
|
s.entriesProcessed = 0
|
||||||
|
s.graph = newDirectedGraph()
|
||||||
|
}
|
405
agent/pkg/servicemap/servicemap_test.go
Normal file
405
agent/pkg/servicemap/servicemap_test.go
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package servicemap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
a = "aService"
|
||||||
|
b = "bService"
|
||||||
|
c = "cService"
|
||||||
|
d = "dService"
|
||||||
|
Ip = "127.0.0.1"
|
||||||
|
Port = "80"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TCPEntryA = &tapApi.TCP{
|
||||||
|
Name: a,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, a),
|
||||||
|
}
|
||||||
|
TCPEntryB = &tapApi.TCP{
|
||||||
|
Name: b,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, b),
|
||||||
|
}
|
||||||
|
TCPEntryC = &tapApi.TCP{
|
||||||
|
Name: c,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, c),
|
||||||
|
}
|
||||||
|
TCPEntryD = &tapApi.TCP{
|
||||||
|
Name: d,
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, d),
|
||||||
|
}
|
||||||
|
TCPEntryUnresolved = &tapApi.TCP{
|
||||||
|
Name: "",
|
||||||
|
Port: Port,
|
||||||
|
IP: Ip,
|
||||||
|
}
|
||||||
|
TCPEntryUnresolved2 = &tapApi.TCP{
|
||||||
|
Name: "",
|
||||||
|
Port: Port,
|
||||||
|
IP: fmt.Sprintf("%s.%s", Ip, UnresolvedNodeName),
|
||||||
|
}
|
||||||
|
ProtocolHttp = &tapApi.Protocol{
|
||||||
|
Name: "http",
|
||||||
|
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
|
||||||
|
Abbreviation: "HTTP",
|
||||||
|
Macro: "http",
|
||||||
|
Version: "1.1",
|
||||||
|
BackgroundColor: "#205cf5",
|
||||||
|
ForegroundColor: "#ffffff",
|
||||||
|
FontSize: 12,
|
||||||
|
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
||||||
|
Ports: []string{"80", "443", "8080"},
|
||||||
|
Priority: 0,
|
||||||
|
}
|
||||||
|
ProtocolRedis = &tapApi.Protocol{
|
||||||
|
Name: "redis",
|
||||||
|
LongName: "Redis Serialization Protocol",
|
||||||
|
Abbreviation: "REDIS",
|
||||||
|
Macro: "redis",
|
||||||
|
Version: "3.x",
|
||||||
|
BackgroundColor: "#a41e11",
|
||||||
|
ForegroundColor: "#ffffff",
|
||||||
|
FontSize: 11,
|
||||||
|
ReferenceLink: "https://redis.io/topics/protocol",
|
||||||
|
Ports: []string{"6379"},
|
||||||
|
Priority: 3,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceMapDisabledSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
instance ServiceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceMapEnabledSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
instance ServiceMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) SetupTest() {
|
||||||
|
s.instance = GetInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapEnabledSuite) SetupTest() {
|
||||||
|
s.instance = GetInstance()
|
||||||
|
s.instance.SetConfig(&shared.MizuAgentConfig{
|
||||||
|
ServiceMap: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) TestServiceMapInstance() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
assert.NotNil(s.instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) TestServiceMapSingletonInstance() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
instance2 := GetInstance()
|
||||||
|
|
||||||
|
assert.NotNil(s.instance)
|
||||||
|
assert.NotNil(instance2)
|
||||||
|
assert.Equal(s.instance, instance2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) TestServiceMapIsEnabledShouldReturnFalseByDefault() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
enabled := s.instance.IsEnabled()
|
||||||
|
|
||||||
|
assert.False(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) TestGetStatusShouldReturnDisabledByDefault() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
status := s.instance.GetStatus()
|
||||||
|
|
||||||
|
assert.Equal("disabled", status.Status)
|
||||||
|
assert.Equal(0, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(0, status.NodeCount)
|
||||||
|
assert.Equal(0, status.EdgeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapDisabledSuite) TestNewTCPEntryShouldDoNothingWhenDisabled() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||||
|
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
|
||||||
|
status := s.instance.GetStatus()
|
||||||
|
|
||||||
|
assert.Equal("disabled", status.Status)
|
||||||
|
assert.Equal(0, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(0, status.NodeCount)
|
||||||
|
assert.Equal(0, status.EdgeCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled
|
||||||
|
|
||||||
|
func (s *ServiceMapEnabledSuite) TestServiceMapIsEnabled() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
enabled := s.instance.IsEnabled()
|
||||||
|
|
||||||
|
assert.True(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceMapEnabledSuite) TestServiceMap() {
|
||||||
|
assert := s.Assert()
|
||||||
|
|
||||||
|
// A -> B - HTTP
|
||||||
|
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||||
|
|
||||||
|
nodes := s.instance.GetNodes()
|
||||||
|
edges := s.instance.GetEdges()
|
||||||
|
|
||||||
|
// Counts for the first entry
|
||||||
|
assert.Equal(1, s.instance.GetEntriesProcessedCount())
|
||||||
|
assert.Equal(2, s.instance.GetNodesCount())
|
||||||
|
assert.Equal(2, len(nodes))
|
||||||
|
assert.Equal(1, s.instance.GetEdgesCount())
|
||||||
|
assert.Equal(1, len(edges))
|
||||||
|
//http protocol
|
||||||
|
assert.Equal(1, edges[0].Count)
|
||||||
|
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
|
||||||
|
|
||||||
|
// same A -> B - HTTP, http protocol count should be 2, edges count should be 1
|
||||||
|
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||||
|
|
||||||
|
nodes = s.instance.GetNodes()
|
||||||
|
edges = s.instance.GetEdges()
|
||||||
|
|
||||||
|
// Counts for a second entry
|
||||||
|
assert.Equal(2, s.instance.GetEntriesProcessedCount())
|
||||||
|
assert.Equal(2, s.instance.GetNodesCount())
|
||||||
|
assert.Equal(2, len(nodes))
|
||||||
|
// edges count should still be 1, but http protocol count should be 2
|
||||||
|
assert.Equal(1, s.instance.GetEdgesCount())
|
||||||
|
assert.Equal(1, len(edges))
|
||||||
|
// http protocol
|
||||||
|
assert.Equal(2, edges[0].Count) //http
|
||||||
|
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
|
||||||
|
|
||||||
|
// same A -> B - REDIS, http protocol count should be 2 and redis protocol count should 1, edges count should be 2
|
||||||
|
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolRedis)
|
||||||
|
|
||||||
|
nodes = s.instance.GetNodes()
|
||||||
|
edges = s.instance.GetEdges()
|
||||||
|
|
||||||
|
// Counts after second entry
|
||||||
|
assert.Equal(3, s.instance.GetEntriesProcessedCount())
|
||||||
|
assert.Equal(2, s.instance.GetNodesCount())
|
||||||
|
assert.Equal(2, len(nodes))
|
||||||
|
// edges count should be 2, http protocol count should be 2 and redis protocol should be 1
|
||||||
|
assert.Equal(2, s.instance.GetEdgesCount())
|
||||||
|
assert.Equal(2, len(edges))
|
||||||
|
// http and redis protocols
|
||||||
|
httpIndex := -1
|
||||||
|
redisIndex := -1
|
||||||
|
for i, e := range edges {
|
||||||
|
if e.Protocol.Name == ProtocolHttp.Name {
|
||||||
|
httpIndex = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.Protocol.Name == ProtocolRedis.Name {
|
||||||
|
redisIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.NotEqual(-1, httpIndex)
|
||||||
|
assert.NotEqual(-1, redisIndex)
|
||||||
|
// http protocol
|
||||||
|
assert.Equal(2, edges[httpIndex].Count)
|
||||||
|
assert.Equal(ProtocolHttp.Name, edges[httpIndex].Protocol.Name)
|
||||||
|
// redis protocol
|
||||||
|
assert.Equal(1, edges[redisIndex].Count)
|
||||||
|
assert.Equal(ProtocolRedis.Name, edges[redisIndex].Protocol.Name)
|
||||||
|
|
||||||
|
// other entries
|
||||||
|
s.instance.NewTCPEntry(TCPEntryUnresolved, TCPEntryA, ProtocolHttp)
|
||||||
|
s.instance.NewTCPEntry(TCPEntryB, TCPEntryUnresolved2, ProtocolHttp)
|
||||||
|
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
|
||||||
|
s.instance.NewTCPEntry(TCPEntryA, TCPEntryC, ProtocolHttp)
|
||||||
|
|
||||||
|
status := s.instance.GetStatus()
|
||||||
|
nodes = s.instance.GetNodes()
|
||||||
|
edges = s.instance.GetEdges()
|
||||||
|
expectedEntriesProcessedCount := 7
|
||||||
|
expectedNodeCount := 6
|
||||||
|
expectedEdgeCount := 6
|
||||||
|
|
||||||
|
// Counts after all entries
|
||||||
|
assert.Equal(expectedEntriesProcessedCount, s.instance.GetEntriesProcessedCount())
|
||||||
|
assert.Equal(expectedNodeCount, s.instance.GetNodesCount())
|
||||||
|
assert.Equal(expectedNodeCount, len(nodes))
|
||||||
|
assert.Equal(expectedEdgeCount, s.instance.GetEdgesCount())
|
||||||
|
assert.Equal(expectedEdgeCount, len(edges))
|
||||||
|
|
||||||
|
// Status
|
||||||
|
assert.Equal("enabled", status.Status)
|
||||||
|
assert.Equal(expectedEntriesProcessedCount, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(expectedNodeCount, status.NodeCount)
|
||||||
|
assert.Equal(expectedEdgeCount, status.EdgeCount)
|
||||||
|
|
||||||
|
// Nodes
|
||||||
|
aNode := -1
|
||||||
|
bNode := -1
|
||||||
|
cNode := -1
|
||||||
|
dNode := -1
|
||||||
|
unresolvedNode := -1
|
||||||
|
unresolvedNode2 := -1
|
||||||
|
var validateNode = func(node ServiceMapNode, entryName string, count int) int {
|
||||||
|
// id
|
||||||
|
assert.GreaterOrEqual(node.Id, 1)
|
||||||
|
assert.LessOrEqual(node.Id, expectedNodeCount)
|
||||||
|
|
||||||
|
// entry
|
||||||
|
// node.Name is the key of the node, key = entry.IP
|
||||||
|
// entry.Name is the name of the service and could be unresolved
|
||||||
|
assert.Equal(node.Name, node.Entry.IP)
|
||||||
|
assert.Equal(Port, node.Entry.Port)
|
||||||
|
assert.Equal(entryName, node.Entry.Name)
|
||||||
|
|
||||||
|
// count
|
||||||
|
assert.Equal(count, node.Count)
|
||||||
|
|
||||||
|
return node.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range nodes {
|
||||||
|
if strings.HasSuffix(v.Name, a) {
|
||||||
|
aNode = validateNode(v, a, 5)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(v.Name, b) {
|
||||||
|
bNode = validateNode(v, b, 4)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(v.Name, c) {
|
||||||
|
cNode = validateNode(v, c, 2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(v.Name, d) {
|
||||||
|
dNode = validateNode(v, d, 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Name == Ip {
|
||||||
|
unresolvedNode = validateNode(v, UnresolvedNodeName, 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(v.Name, UnresolvedNodeName) {
|
||||||
|
unresolvedNode2 = validateNode(v, UnresolvedNodeName, 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we found all the nodes
|
||||||
|
nodeIds := [...]int{aNode, bNode, cNode, dNode, unresolvedNode, unresolvedNode2}
|
||||||
|
for _, v := range nodeIds {
|
||||||
|
assert.NotEqual(-1, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edges
|
||||||
|
abEdge := -1
|
||||||
|
uaEdge := -1
|
||||||
|
buEdge := -1
|
||||||
|
cdEdge := -1
|
||||||
|
acEdge := -1
|
||||||
|
var validateEdge = func(edge ServiceMapEdge, sourceEntryName string, destEntryName string, protocolName string, protocolCount int) {
|
||||||
|
// source
|
||||||
|
assert.Contains(nodeIds, edge.Source.Id)
|
||||||
|
assert.LessOrEqual(edge.Source.Id, expectedNodeCount)
|
||||||
|
assert.Equal(edge.Source.Name, edge.Source.Entry.IP)
|
||||||
|
assert.Equal(sourceEntryName, edge.Source.Entry.Name)
|
||||||
|
|
||||||
|
// destination
|
||||||
|
assert.Contains(nodeIds, edge.Destination.Id)
|
||||||
|
assert.LessOrEqual(edge.Destination.Id, expectedNodeCount)
|
||||||
|
assert.Equal(edge.Destination.Name, edge.Destination.Entry.IP)
|
||||||
|
assert.Equal(destEntryName, edge.Destination.Entry.Name)
|
||||||
|
|
||||||
|
// protocol
|
||||||
|
assert.Equal(protocolName, edge.Protocol.Name)
|
||||||
|
assert.Equal(protocolCount, edge.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, v := range edges {
|
||||||
|
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "http" {
|
||||||
|
validateEdge(v, a, b, ProtocolHttp.Name, 2)
|
||||||
|
abEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "redis" {
|
||||||
|
validateEdge(v, a, b, ProtocolRedis.Name, 1)
|
||||||
|
abEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Source.Entry.Name == UnresolvedNodeName && v.Destination.Entry.Name == a {
|
||||||
|
validateEdge(v, UnresolvedNodeName, a, ProtocolHttp.Name, 1)
|
||||||
|
uaEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Source.Entry.Name == b && v.Destination.Entry.Name == UnresolvedNodeName {
|
||||||
|
validateEdge(v, b, UnresolvedNodeName, ProtocolHttp.Name, 1)
|
||||||
|
buEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Source.Entry.Name == c && v.Destination.Entry.Name == d {
|
||||||
|
validateEdge(v, c, d, ProtocolHttp.Name, 1)
|
||||||
|
cdEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if v.Source.Entry.Name == a && v.Destination.Entry.Name == c {
|
||||||
|
validateEdge(v, a, c, ProtocolHttp.Name, 1)
|
||||||
|
acEdge = i
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we found all the edges
|
||||||
|
for _, v := range [...]int{abEdge, uaEdge, buEdge, cdEdge, acEdge} {
|
||||||
|
assert.NotEqual(-1, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
s.instance.Reset()
|
||||||
|
status = s.instance.GetStatus()
|
||||||
|
nodes = s.instance.GetNodes()
|
||||||
|
edges = s.instance.GetEdges()
|
||||||
|
|
||||||
|
// Counts after reset
|
||||||
|
assert.Equal(0, s.instance.GetEntriesProcessedCount())
|
||||||
|
assert.Equal(0, s.instance.GetNodesCount())
|
||||||
|
assert.Equal(0, s.instance.GetEdgesCount())
|
||||||
|
|
||||||
|
// Status after reset
|
||||||
|
assert.Equal("enabled", status.Status)
|
||||||
|
assert.Equal(0, status.EntriesProcessedCount)
|
||||||
|
assert.Equal(0, status.NodeCount)
|
||||||
|
assert.Equal(0, status.EdgeCount)
|
||||||
|
|
||||||
|
// Nodes after reset
|
||||||
|
assert.Equal([]ServiceMapNode(nil), nodes)
|
||||||
|
|
||||||
|
// Edges after reset
|
||||||
|
assert.Equal([]ServiceMapEdge(nil), edges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceMapSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ServiceMapDisabledSuite))
|
||||||
|
suite.Run(t, new(ServiceMapEnabledSuite))
|
||||||
|
}
|
29
ui/package-lock.json
generated
29
ui/package-lock.json
generated
@ -14479,6 +14479,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
||||||
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
||||||
},
|
},
|
||||||
|
"react-graph-vis": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-graph-vis/-/react-graph-vis-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-FI35zlBMKU22JEvG1ukd1DDwW185y4YrDvHm6Bom9EGdA+UNMrZrIV/lyPIRWPcRkzbKaA1w1NvOYcRApD4KdQ==",
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"uuid": "^2.0.1",
|
||||||
|
"vis-data": "^7.1.2",
|
||||||
|
"vis-network": "^9.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@ -17902,6 +17921,16 @@
|
|||||||
"unist-util-stringify-position": "^3.0.0"
|
"unist-util-stringify-position": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vis-data": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw=="
|
||||||
|
},
|
||||||
|
"vis-network": {
|
||||||
|
"version": "9.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.0.tgz",
|
||||||
|
"integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw=="
|
||||||
|
},
|
||||||
"vm-browserify": {
|
"vm-browserify": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-copy-to-clipboard": "^5.0.3",
|
"react-copy-to-clipboard": "^5.0.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-graph-vis": "^1.0.7",
|
||||||
"react-lowlight": "^3.0.0",
|
"react-lowlight": "^3.0.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"react-scrollable-feed-virtualized": "^1.4.9",
|
"react-scrollable-feed-virtualized": "^1.4.9",
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
// Injected from server
|
// Injected from server
|
||||||
window.isEnt = __IS_STANDALONE__
|
window.isEnt = __IS_STANDALONE__
|
||||||
window.isOasEnabled = __IS_OAS_ENABLED__
|
window.isOasEnabled = __IS_OAS_ENABLED__
|
||||||
|
window.isServiceMapEnabled = __IS_SERVICE_MAP_ENABLED__
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import './App.sass';
|
|||||||
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
||||||
import {Header} from "./components/Header/Header";
|
import {Header} from "./components/Header/Header";
|
||||||
import {TrafficPage} from "./components/TrafficPage";
|
import {TrafficPage} from "./components/TrafficPage";
|
||||||
|
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ const App = () => {
|
|||||||
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
||||||
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
||||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||||
|
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||||
|
|
||||||
const onTLSDetected = (destAddress: string) => {
|
const onTLSDetected = (destAddress: string) => {
|
||||||
addressesWithTLS.add(destAddress);
|
addressesWithTLS.add(destAddress);
|
||||||
@ -22,14 +24,19 @@ const App = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mizuApp">
|
<div className="mizuApp">
|
||||||
<Header analyzeStatus={analyzeStatus}/>
|
<Header analyzeStatus={analyzeStatus} />
|
||||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />
|
||||||
<TLSWarning showTLSWarning={showTLSWarning}
|
<TLSWarning showTLSWarning={showTLSWarning}
|
||||||
setShowTLSWarning={setShowTLSWarning}
|
setShowTLSWarning={setShowTLSWarning}
|
||||||
addressesWithTLS={addressesWithTLS}
|
addressesWithTLS={addressesWithTLS}
|
||||||
setAddressesWithTLS={setAddressesWithTLS}
|
setAddressesWithTLS={setAddressesWithTLS}
|
||||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>
|
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
|
||||||
|
{window["isServiceMapEnabled"] && <ServiceMapModal
|
||||||
|
isOpen={openServiceMapModal}
|
||||||
|
onOpen={() => setOpenServiceMapModal(true)}
|
||||||
|
onClose={() => setOpenServiceMapModal(false)}
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import LoadingOverlay from "./components/LoadingOverlay";
|
|||||||
import AuthPageBase from './components/AuthPageBase';
|
import AuthPageBase from './components/AuthPageBase';
|
||||||
import entPageAtom, {Page} from "./recoil/entPage";
|
import entPageAtom, {Page} from "./recoil/entPage";
|
||||||
import {useRecoilState} from "recoil";
|
import {useRecoilState} from "recoil";
|
||||||
|
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ const EntApp = () => {
|
|||||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||||
const [entPage, setEntPage] = useRecoilState(entPageAtom);
|
const [entPage, setEntPage] = useRecoilState(entPageAtom);
|
||||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||||
|
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||||
|
|
||||||
const determinePage = useCallback(async () => { // TODO: move to state management
|
const determinePage = useCallback(async () => { // TODO: move to state management
|
||||||
try {
|
try {
|
||||||
@ -59,7 +61,7 @@ const EntApp = () => {
|
|||||||
|
|
||||||
switch (entPage) { // TODO: move to state management / proper routing
|
switch (entPage) { // TODO: move to state management / proper routing
|
||||||
case Page.Traffic:
|
case Page.Traffic:
|
||||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
|
pageComponent = <TrafficPage onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />;
|
||||||
break;
|
break;
|
||||||
case Page.Setup:
|
case Page.Setup:
|
||||||
pageComponent = <AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>;
|
pageComponent = <AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>;
|
||||||
@ -77,14 +79,19 @@ const EntApp = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mizuApp">
|
<div className="mizuApp">
|
||||||
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
|
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin} />}
|
||||||
{pageComponent}
|
{pageComponent}
|
||||||
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
||||||
setShowTLSWarning={setShowTLSWarning}
|
setShowTLSWarning={setShowTLSWarning}
|
||||||
addressesWithTLS={addressesWithTLS}
|
addressesWithTLS={addressesWithTLS}
|
||||||
setAddressesWithTLS={setAddressesWithTLS}
|
setAddressesWithTLS={setAddressesWithTLS}
|
||||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
|
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />}
|
||||||
|
{entPage === Page.Traffic && window["isServiceMapEnabled"] && <ServiceMapModal
|
||||||
|
isOpen={openServiceMapModal}
|
||||||
|
onOpen={() => setOpenServiceMapModal(true)}
|
||||||
|
onClose={() => setOpenServiceMapModal(false)}
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
216
ui/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
216
ui/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import Api from "../../helpers/api";
|
||||||
|
import spinnerStyle from '../style/Spinner.module.sass';
|
||||||
|
import spinnerImg from '../assets/spinner.svg';
|
||||||
|
import Graph from "react-graph-vis";
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import ServiceMapOptions from './ServiceMapOptions'
|
||||||
|
import { useCommonStyles } from "../../helpers/commonStyle";
|
||||||
|
|
||||||
|
interface GraphData {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
id: number;
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
title?: string;
|
||||||
|
color?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Edge {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
title?: string;
|
||||||
|
color?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceMapNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
entry: Entry;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceMapEdge {
|
||||||
|
source: ServiceMapNode;
|
||||||
|
destination: ServiceMapNode;
|
||||||
|
count: number;
|
||||||
|
protocol: Protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceMapGraph {
|
||||||
|
nodes: ServiceMapNode[];
|
||||||
|
edges: ServiceMapEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Entry {
|
||||||
|
ip: string;
|
||||||
|
port: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Protocol {
|
||||||
|
name: string;
|
||||||
|
abbr: string;
|
||||||
|
macro: string;
|
||||||
|
version: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
foregroundColor: string;
|
||||||
|
fontSize: number;
|
||||||
|
referenceLink: string;
|
||||||
|
ports: string[];
|
||||||
|
priority: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceMapModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpen: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, 0%)',
|
||||||
|
width: '80vw',
|
||||||
|
height: '80vh',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: '5px',
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 4,
|
||||||
|
color: '#000',
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = Api.getInstance();
|
||||||
|
|
||||||
|
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen, onClose }) => {
|
||||||
|
const commonClasses = useCommonStyles();
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
|
||||||
|
|
||||||
|
const getServiceMapData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const serviceMapData: ServiceMapGraph = await api.serviceMapData()
|
||||||
|
const newGraphData: GraphData = { nodes: [], edges: [] }
|
||||||
|
|
||||||
|
if (serviceMapData.nodes) {
|
||||||
|
newGraphData.nodes = serviceMapData.nodes.map(node => {
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
value: node.count,
|
||||||
|
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
|
||||||
|
title: "Count: " + node.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceMapData.edges) {
|
||||||
|
newGraphData.edges = serviceMapData.edges.map(edge => {
|
||||||
|
return {
|
||||||
|
from: edge.source.id,
|
||||||
|
to: edge.destination.id,
|
||||||
|
value: edge.count,
|
||||||
|
label: edge.count.toString(),
|
||||||
|
color: {
|
||||||
|
color: edge.protocol.backgroundColor,
|
||||||
|
highlight: edge.protocol.backgroundColor
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setGraphData(newGraphData)
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
toast.error("An error occurred while loading Mizu Service Map, see console for mode details");
|
||||||
|
console.error(ex);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getServiceMapData()
|
||||||
|
}, [getServiceMapData])
|
||||||
|
|
||||||
|
const resetServiceMap = debounce(async () => {
|
||||||
|
try {
|
||||||
|
const serviceMapResetResponse = await api.serviceMapReset();
|
||||||
|
if (serviceMapResetResponse["status"] === "enabled") {
|
||||||
|
refreshServiceMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
toast.error("An error occurred while resetting Mizu Service Map, see console for mode details");
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const refreshServiceMap = debounce(() => {
|
||||||
|
getServiceMapData();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="transition-modal-title"
|
||||||
|
aria-describedby="transition-modal-description"
|
||||||
|
open={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
closeAfterTransition
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
BackdropProps={{
|
||||||
|
timeout: 500,
|
||||||
|
}}
|
||||||
|
style={{ overflow: 'auto' }}
|
||||||
|
>
|
||||||
|
<Fade in={isOpen}>
|
||||||
|
<Box sx={modalStyle}>
|
||||||
|
{isLoading && <div className={spinnerStyle.spinnerContainer}>
|
||||||
|
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
||||||
|
</div>}
|
||||||
|
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button}
|
||||||
|
style={{ marginRight: 25 }}
|
||||||
|
onClick={() => onClose()}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button}
|
||||||
|
style={{ marginRight: 25 }}
|
||||||
|
onClick={resetServiceMap}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button}
|
||||||
|
onClick={refreshServiceMap}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Graph
|
||||||
|
graph={graphData}
|
||||||
|
options={ServiceMapOptions}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
83
ui/src/components/ServiceMapModal/ServiceMapOptions.ts
Normal file
83
ui/src/components/ServiceMapModal/ServiceMapOptions.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const ServiceMapOptions = {
|
||||||
|
physics: {
|
||||||
|
enabled: true,
|
||||||
|
solver: 'barnesHut',
|
||||||
|
barnesHut: {
|
||||||
|
theta: 0.5,
|
||||||
|
gravitationalConstant: -2000,
|
||||||
|
centralGravity: 0.3,
|
||||||
|
springLength: 180,
|
||||||
|
springConstant: 0.04,
|
||||||
|
damping: 0.09,
|
||||||
|
avoidOverlap: 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
hierarchical: false,
|
||||||
|
randomSeed: 1 // always on node 1
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
shape: 'dot',
|
||||||
|
chosen: true,
|
||||||
|
color: {
|
||||||
|
background: '#27AE60',
|
||||||
|
border: '#000000',
|
||||||
|
highlight: {
|
||||||
|
background: '#27AE60',
|
||||||
|
border: '#000000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
color: '#343434',
|
||||||
|
size: 14, // px
|
||||||
|
face: 'arial',
|
||||||
|
background: 'none',
|
||||||
|
strokeWidth: 0, // px
|
||||||
|
strokeColor: '#ffffff',
|
||||||
|
align: 'center',
|
||||||
|
multi: false,
|
||||||
|
},
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderWidthSelected: 2.5,
|
||||||
|
labelHighlightBold: true,
|
||||||
|
opacity: 1,
|
||||||
|
shadow: true,
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
chosen: true,
|
||||||
|
dashes: false,
|
||||||
|
arrowStrikethrough: false,
|
||||||
|
arrows: {
|
||||||
|
to: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
middle: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
smooth: {
|
||||||
|
enabled: true,
|
||||||
|
type: 'dynamic',
|
||||||
|
roundness: 1.0
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
color: '#343434',
|
||||||
|
size: 12, // px
|
||||||
|
face: 'arial',
|
||||||
|
background: 'none',
|
||||||
|
strokeWidth: 2, // px
|
||||||
|
strokeColor: '#ffffff',
|
||||||
|
align: 'horizontal',
|
||||||
|
multi: false,
|
||||||
|
},
|
||||||
|
labelHighlightBold: true,
|
||||||
|
selectionWidth: 1,
|
||||||
|
shadow: true,
|
||||||
|
},
|
||||||
|
autoResize: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceMapOptions
|
@ -19,6 +19,7 @@ import focusedEntryIdAtom from "../recoil/focusedEntryId";
|
|||||||
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
||||||
import queryAtom from "../recoil/query";
|
import queryAtom from "../recoil/query";
|
||||||
import OasModal from "./OasModal/OasModal";
|
import OasModal from "./OasModal/OasModal";
|
||||||
|
import {useCommonStyles} from "../helpers/commonStyle"
|
||||||
|
|
||||||
const useLayoutStyles = makeStyles(() => ({
|
const useLayoutStyles = makeStyles(() => ({
|
||||||
details: {
|
details: {
|
||||||
@ -43,11 +44,13 @@ const useLayoutStyles = makeStyles(() => ({
|
|||||||
interface TrafficPageProps {
|
interface TrafficPageProps {
|
||||||
setAnalyzeStatus?: (status: any) => void;
|
setAnalyzeStatus?: (status: any) => void;
|
||||||
onTLSDetected: (destAddress: string) => void;
|
onTLSDetected: (destAddress: string) => void;
|
||||||
|
setOpenServiceMapModal?: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected}) => {
|
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected, setOpenServiceMapModal}) => {
|
||||||
|
const commonClasses = useCommonStyles();
|
||||||
const classes = useLayoutStyles();
|
const classes = useLayoutStyles();
|
||||||
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
|
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
|
||||||
const [entries, setEntries] = useRecoilState(entriesAtom);
|
const [entries, setEntries] = useRecoilState(entriesAtom);
|
||||||
@ -239,79 +242,84 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const openServiceMapModalDebounce = debounce(() => {
|
||||||
<div className="TrafficPage">
|
setOpenServiceMapModal(true)
|
||||||
<div className="TrafficPageHeader">
|
}, 500);
|
||||||
<div className="TrafficPageStreamStatus">
|
|
||||||
<img className="playPauseIcon" style={{visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
|
return (
|
||||||
src={pauseIcon} onClick={toggleConnection}/>
|
<div className="TrafficPage">
|
||||||
<img className="playPauseIcon" style={{position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
|
<div className="TrafficPageHeader">
|
||||||
src={playIcon} onClick={toggleConnection}/>
|
<div className="TrafficPageStreamStatus">
|
||||||
<div className="connectionText">
|
<img className="playPauseIcon" style={{ visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden" }} alt="pause"
|
||||||
{getConnectionTitle()}
|
src={pauseIcon} onClick={toggleConnection} />
|
||||||
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
<img className="playPauseIcon" style={{ position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible" }} alt="play"
|
||||||
<div className={"indicator " + getConnectionStatusClass(false)}/>
|
src={playIcon} onClick={toggleConnection} />
|
||||||
</div>
|
<div className="connectionText">
|
||||||
</div>
|
{getConnectionTitle()}
|
||||||
</div>
|
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||||
{window["isOasEnabled"] && <div>
|
<div className={"indicator " + getConnectionStatusClass(false)} />
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
style={{
|
|
||||||
margin: "2px 0px 0px 0px",
|
|
||||||
backgroundColor: variables.blueColor,
|
|
||||||
fontWeight: 600,
|
|
||||||
borderRadius: "4px",
|
|
||||||
color: "#fff",
|
|
||||||
textTransform: "none",
|
|
||||||
}}
|
|
||||||
onClick={handleOpenModal}
|
|
||||||
>
|
|
||||||
Show OAS
|
|
||||||
</Button>
|
|
||||||
</div>}
|
|
||||||
</div>
|
</div>
|
||||||
{window["isOasEnabled"] && <OasModal
|
</div>
|
||||||
openModal={openOasModal}
|
|
||||||
handleCloseModal={handleCloseModal}
|
|
||||||
/>}
|
|
||||||
{<div className="TrafficPage-Container">
|
|
||||||
<div className="TrafficPage-ListContainer">
|
|
||||||
<Filters
|
|
||||||
backgroundColor={queryBackgroundColor}
|
|
||||||
ws={ws.current}
|
|
||||||
openWebSocket={openWebSocket}
|
|
||||||
/>
|
|
||||||
<div className={styles.container}>
|
|
||||||
<EntriesList
|
|
||||||
listEntryREF={listEntry}
|
|
||||||
onSnapBrokenEvent={onSnapBrokenEvent}
|
|
||||||
isSnappedToBottom={isSnappedToBottom}
|
|
||||||
setIsSnappedToBottom={setIsSnappedToBottom}
|
|
||||||
queriedCurrent={queriedCurrent}
|
|
||||||
setQueriedCurrent={setQueriedCurrent}
|
|
||||||
queriedTotal={queriedTotal}
|
|
||||||
setQueriedTotal={setQueriedTotal}
|
|
||||||
startTime={startTime}
|
|
||||||
noMoreDataTop={noMoreDataTop}
|
|
||||||
setNoMoreDataTop={setNoMoreDataTop}
|
|
||||||
leftOffTop={leftOffTop}
|
|
||||||
setLeftOffTop={setLeftOffTop}
|
|
||||||
ws={ws.current}
|
|
||||||
openWebSocket={openWebSocket}
|
|
||||||
leftOffBottom={leftOffBottom}
|
|
||||||
truncatedTimestamp={truncatedTimestamp}
|
|
||||||
setTruncatedTimestamp={setTruncatedTimestamp}
|
|
||||||
scrollableRef={scrollableRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={classes.details}>
|
|
||||||
{focusedEntryId && <EntryDetailed/>}
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
{tappingStatus && !openOasModal && <StatusBar/>}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
{window["isOasEnabled"] && <Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button}
|
||||||
|
style={{ marginRight: 25 }}
|
||||||
|
onClick={handleOpenModal}
|
||||||
|
>
|
||||||
|
Show OAS
|
||||||
|
</Button>}
|
||||||
|
{window["isServiceMapEnabled"] && <Button
|
||||||
|
variant="contained"
|
||||||
|
className={commonClasses.button}
|
||||||
|
onClick={openServiceMapModalDebounce}
|
||||||
|
>
|
||||||
|
Service Map
|
||||||
|
</Button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{window["isOasEnabled"] && <OasModal
|
||||||
|
openModal={openOasModal}
|
||||||
|
handleCloseModal={handleCloseModal}
|
||||||
|
/>}
|
||||||
|
{<div className="TrafficPage-Container">
|
||||||
|
<div className="TrafficPage-ListContainer">
|
||||||
|
<Filters
|
||||||
|
backgroundColor={queryBackgroundColor}
|
||||||
|
ws={ws.current}
|
||||||
|
openWebSocket={openWebSocket}
|
||||||
|
/>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<EntriesList
|
||||||
|
listEntryREF={listEntry}
|
||||||
|
onSnapBrokenEvent={onSnapBrokenEvent}
|
||||||
|
isSnappedToBottom={isSnappedToBottom}
|
||||||
|
setIsSnappedToBottom={setIsSnappedToBottom}
|
||||||
|
queriedCurrent={queriedCurrent}
|
||||||
|
setQueriedCurrent={setQueriedCurrent}
|
||||||
|
queriedTotal={queriedTotal}
|
||||||
|
setQueriedTotal={setQueriedTotal}
|
||||||
|
startTime={startTime}
|
||||||
|
noMoreDataTop={noMoreDataTop}
|
||||||
|
setNoMoreDataTop={setNoMoreDataTop}
|
||||||
|
leftOffTop={leftOffTop}
|
||||||
|
setLeftOffTop={setLeftOffTop}
|
||||||
|
ws={ws.current}
|
||||||
|
openWebSocket={openWebSocket}
|
||||||
|
leftOffBottom={leftOffBottom}
|
||||||
|
truncatedTimestamp={truncatedTimestamp}
|
||||||
|
setTruncatedTimestamp={setTruncatedTimestamp}
|
||||||
|
scrollableRef={scrollableRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.details}>
|
||||||
|
{focusedEntryId && <EntryDetailed />}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
{tappingStatus && !openOasModal && <StatusBar />}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
7
ui/src/components/style/Spinner.module.sass
Normal file
7
ui/src/components/style/Spinner.module.sass
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
@import "../../variables.module"
|
||||||
|
|
||||||
|
.spinnerContainer
|
||||||
|
display: flex
|
||||||
|
justify-content: center
|
||||||
|
margin-bottom: 10px
|
||||||
|
|
@ -28,6 +28,21 @@ export default class Api {
|
|||||||
this.source = null;
|
this.source = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serviceMapStatus = async () => {
|
||||||
|
const response = await this.client.get("/servicemap/status");
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceMapData = async () => {
|
||||||
|
const response = await this.client.get(`/servicemap/get`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceMapReset = async () => {
|
||||||
|
const response = await this.client.get(`/servicemap/reset`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
tapStatus = async () => {
|
tapStatus = async () => {
|
||||||
const response = await this.client.get("/status/tap");
|
const response = await this.client.get("/status/tap");
|
||||||
return response.data;
|
return response.data;
|
||||||
|
Loading…
Reference in New Issue
Block a user