mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-02-16 19:10:17 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a2697dd0d | ||
|
|
2638672603 | ||
|
|
a702f0f93a | ||
|
|
18d90cdf36 | ||
|
|
9c665e664b | ||
|
|
54ea2afe71 | ||
|
|
50c89e6245 | ||
|
|
676e50b0b1 | ||
|
|
6bab381280 | ||
|
|
27dee4e09b | ||
|
|
b31af7214b | ||
|
|
d5fd2ff1da | ||
|
|
7477f867f9 | ||
|
|
cc4638afe6 | ||
|
|
acf3894824 |
4
.github/workflows/pr_validation.yml
vendored
4
.github/workflows/pr_validation.yml
vendored
@@ -49,10 +49,10 @@ jobs:
|
||||
name: Build UI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Node 14
|
||||
- name: Set up Node 16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
9
.github/workflows/publish.yml
vendored
9
.github/workflows/publish.yml
vendored
@@ -51,12 +51,19 @@ jobs:
|
||||
id: meta
|
||||
uses: crazy-max/ghaction-docker-meta@v2
|
||||
with:
|
||||
images: ${{ steps.base_image_step.outputs.image }}
|
||||
images: |
|
||||
${{ steps.base_image_step.outputs.image }}
|
||||
up9inc/mizu
|
||||
tags: |
|
||||
type=sha
|
||||
type=raw,${{ github.sha }}
|
||||
type=raw,${{ steps.versioning.outputs.version }}
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASS }}
|
||||
- name: Login to GCR
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: gcr.io
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:14-slim AS site-build
|
||||
FROM node:16-slim AS site-build
|
||||
|
||||
WORKDIR /app/ui-build
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Think TCPDump and Wireshark re-invented for Kubernetes.
|
||||
|
||||
- Simple and powerful CLI
|
||||
- Monitoring network traffic in real-time. Supported protocols:
|
||||
- [HTTP/1.1](https://datatracker.ietf.org/doc/html/rfc2616) (REST, etc.)
|
||||
- [HTTP/1.x](https://datatracker.ietf.org/doc/html/rfc2616) (REST, GraphQL, SOAP, etc.)
|
||||
- [HTTP/2](https://datatracker.ietf.org/doc/html/rfc7540) (gRPC)
|
||||
- [AMQP](https://www.rabbitmq.com/amqp-0-9-1-reference.html) (RabbitMQ, Apache Qpid, etc.)
|
||||
- [Apache Kafka](https://kafka.apache.org/protocol)
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
"viewportHeight": 1080,
|
||||
"video": false,
|
||||
"screenshotOnRunFailure": false,
|
||||
|
||||
"testFiles":
|
||||
["tests/GuiPort.js",
|
||||
"tests/MultipleNamespaces.js",
|
||||
"tests/Redact.js",
|
||||
"testFiles": [
|
||||
"tests/GuiPort.js",
|
||||
"tests/MultipleNamespaces.js",
|
||||
"tests/Redact.js",
|
||||
"tests/NoRedact.js",
|
||||
"tests/Regex.js"],
|
||||
"tests/Regex.js",
|
||||
"tests/RegexMasking.js"
|
||||
],
|
||||
|
||||
"env": {
|
||||
"testUrl": "http://localhost:8899/",
|
||||
"redactHeaderContent": "User-Header[REDACTED]",
|
||||
"redactBodyContent": "{ \"User\": \"[REDACTED]\" }"
|
||||
"redactBodyContent": "{ \"User\": \"[REDACTED]\" }",
|
||||
"regexMaskingBodyContent": "[REDACTED]"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"watchForFileChanges":false,
|
||||
"viewportWidth": 1920,
|
||||
"viewportHeight": 3500,
|
||||
"video": false,
|
||||
"screenshotOnRunFailure": false,
|
||||
"testFiles": [
|
||||
"tests/IgnoredUserAgents.js"
|
||||
],
|
||||
|
||||
"env": {
|
||||
"testUrl": "http://localhost:8899/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {isValueExistsInElement} from "../testHelpers/TrafficHelper";
|
||||
|
||||
it('Loading Mizu', function () {
|
||||
cy.visit(Cypress.env('testUrl'));
|
||||
});
|
||||
|
||||
it('going through each entry', function () {
|
||||
cy.get('#total-entries').then(number => {
|
||||
const getNum = () => {
|
||||
const numOfEntries = number.text();
|
||||
return parseInt(numOfEntries);
|
||||
};
|
||||
cy.wrap({ there: getNum }).invoke('there').should('be.gte', 25);
|
||||
|
||||
checkThatAllEntriesShown();
|
||||
|
||||
const entriesNum = getNum();
|
||||
[...Array(entriesNum).keys()].map(checkEntry);
|
||||
});
|
||||
});
|
||||
|
||||
function checkThatAllEntriesShown() {
|
||||
cy.get('#entries-length').then(number => {
|
||||
if (number.text() === '1')
|
||||
cy.get('[title="Fetch old records"]').click();
|
||||
});
|
||||
}
|
||||
|
||||
function checkEntry(entryIndex) {
|
||||
cy.get(`#entry-${entryIndex}`).click();
|
||||
cy.get('#tbody-Headers').should('be.visible');
|
||||
isValueExistsInElement(false, 'Ignored-User-Agent', '#tbody-Headers');
|
||||
}
|
||||
@@ -15,4 +15,3 @@ function doItFunc(number) {
|
||||
findLineAndCheck(getExpectedDetailsDict(podName, namespace));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import {isValueExistsInElement} from "../testHelpers/TrafficHelper";
|
||||
|
||||
it('Loading Mizu', function () {
|
||||
cy.visit(Cypress.env('testUrl'));
|
||||
})
|
||||
|
||||
isValueExistsInElement(true, Cypress.env('regexMaskingBodyContent'), '.hljs');
|
||||
@@ -138,7 +138,7 @@ func TestTapGuiPort(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/GuiPort.js\" --env port=%d", guiPort))
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/GuiPort.js\" --env port=%d --config-file cypress/integration/configurations/Default.json", guiPort))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ func TestTapAllNamespaces(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v",
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v --config-file cypress/integration/configurations/Default.json",
|
||||
expectedPods[0].Name, expectedPods[1].Name, expectedPods[2].Name, expectedPods[0].Namespace, expectedPods[1].Namespace, expectedPods[2].Namespace))
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ func TestTapMultipleNamespaces(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v",
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/MultipleNamespaces.js\" --env name1=%v,name2=%v,name3=%v,namespace1=%v,namespace2=%v,namespace3=%v --config-file cypress/integration/configurations/Default.json",
|
||||
expectedPods[0].Name, expectedPods[1].Name, expectedPods[2].Name, expectedPods[0].Namespace, expectedPods[1].Namespace, expectedPods[2].Namespace))
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ func TestTapRegex(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Regex.js\" --env name=%v,namespace=%v",
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Regex.js\" --env name=%v,namespace=%v --config-file cypress/integration/configurations/Default.json",
|
||||
expectedPods[0].Name, expectedPods[0].Namespace))
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ func TestTapRedact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Redact.js\""))
|
||||
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Redact.js\" --config-file cypress/integration/configurations/Default.json"))
|
||||
}
|
||||
|
||||
func TestTapNoRedact(t *testing.T) {
|
||||
@@ -429,7 +429,7 @@ func TestTapNoRedact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/NoRedact.js\"")
|
||||
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/NoRedact.js\" --config-file cypress/integration/configurations/Default.json")
|
||||
}
|
||||
|
||||
func TestTapRegexMasking(t *testing.T) {
|
||||
@@ -480,41 +480,8 @@ func TestTapRegexMasking(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
redactCheckFunc := func() error {
|
||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/RegexMasking.js\" --config-file cypress/integration/configurations/Default.json")
|
||||
|
||||
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = checkEntriesAtLeast(entries, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
firstEntry := entries[0]
|
||||
|
||||
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
||||
requestResult, requestErr := executeHttpGetRequest(entryUrl)
|
||||
if requestErr != nil {
|
||||
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||
}
|
||||
|
||||
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||
request := entry["request"].(map[string]interface{})
|
||||
|
||||
postData := request["postData"].(map[string]interface{})
|
||||
textData := postData["text"].(string)
|
||||
|
||||
if textData != "[REDACTED]" {
|
||||
return fmt.Errorf("unexpected result - body is not redacted")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestTapIgnoredUserAgents(t *testing.T) {
|
||||
@@ -575,45 +542,7 @@ func TestTapIgnoredUserAgents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ignoredUserAgentsCheckFunc := func() error {
|
||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
|
||||
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = checkEntriesAtLeast(entries, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entryInterface := range entries {
|
||||
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, entryInterface["id"])
|
||||
requestResult, requestErr := executeHttpGetRequest(entryUrl)
|
||||
if requestErr != nil {
|
||||
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||
}
|
||||
|
||||
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||
request := entry["request"].(map[string]interface{})
|
||||
|
||||
headers := request["_headers"].([]interface{})
|
||||
for _, headerInterface := range headers {
|
||||
header := headerInterface.(map[string]interface{})
|
||||
if header["name"].(string) != ignoredUserAgentCustomHeader {
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected result - user agent is not ignored")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if err := retriesExecute(shortRetriesCount, ignoredUserAgentsCheckFunc); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/IgnoredUserAgents.js\" --config-file cypress/integration/configurations/HugeMizu.json")
|
||||
}
|
||||
|
||||
func TestTapDumpLogs(t *testing.T) {
|
||||
|
||||
@@ -19,6 +19,7 @@ require (
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||
github.com/ory/kratos-client-go v0.8.2-alpha.1
|
||||
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/mizu/shared v0.0.0
|
||||
github.com/up9inc/mizu/tap v0.0.0
|
||||
|
||||
@@ -115,6 +115,7 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
@@ -378,6 +379,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"mizuserver/pkg/models"
|
||||
"mizuserver/pkg/oas"
|
||||
"mizuserver/pkg/routes"
|
||||
"mizuserver/pkg/servicemap"
|
||||
"mizuserver/pkg/up9"
|
||||
"mizuserver/pkg/utils"
|
||||
"net/http"
|
||||
@@ -153,6 +154,9 @@ func enableExpFeatureIfNeeded() {
|
||||
if config.Config.OAS {
|
||||
oas.GetOasGeneratorInstance().Start()
|
||||
}
|
||||
if config.Config.ServiceMap {
|
||||
servicemap.GetInstance().SetConfig(config.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func configureBasenineServer(host string, port string) {
|
||||
@@ -254,10 +258,12 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
||||
routes.UserRoutes(app)
|
||||
routes.InstallRoutes(app)
|
||||
}
|
||||
|
||||
if config.Config.OAS {
|
||||
routes.OASRoutes(app)
|
||||
}
|
||||
if config.Config.ServiceMap {
|
||||
routes.ServiceMapRoutes(app)
|
||||
}
|
||||
|
||||
routes.QueryRoutes(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(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)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mizuserver/pkg/servicemap"
|
||||
|
||||
"github.com/google/martian/har"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
@@ -146,6 +148,8 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
||||
panic(err)
|
||||
}
|
||||
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))
|
||||
}
|
||||
@@ -36,6 +36,16 @@ func NewProvider(url string, retries int, timeout time.Duration) *Provider {
|
||||
}
|
||||
}
|
||||
|
||||
func NewProviderWithoutRetries(url string, timeout time.Duration) *Provider {
|
||||
return &Provider{
|
||||
url: url,
|
||||
retries: 1,
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Provider) TestConnection() error {
|
||||
retriesLeft := provider.retries
|
||||
for retriesLeft > 0 {
|
||||
|
||||
@@ -6,18 +6,18 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/apiserver"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/errormessage"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||
"github.com/up9inc/mizu/cli/resources"
|
||||
"github.com/up9inc/mizu/cli/telemetry"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/errormessage"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"github.com/up9inc/mizu/shared/kubernetes"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
)
|
||||
@@ -27,14 +27,35 @@ func GetApiServerUrl() string {
|
||||
}
|
||||
|
||||
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.ProxyHost, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName)
|
||||
httpServer, err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.ProxyHost, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName, cancel)
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running k8s proxy %v\n"+
|
||||
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log.Debugf("proxy ended")
|
||||
apiProvider = apiserver.NewProviderWithoutRetries(GetApiServerUrl(), time.Second) // short check for proxy
|
||||
if err := apiProvider.TestConnection(); err != nil {
|
||||
logger.Log.Debugf("Couldn't connect using proxy, stopping proxy and trying to create port-forward")
|
||||
if err := httpServer.Shutdown(context.Background()); err != nil {
|
||||
logger.Log.Debugf("Error occurred while stopping proxy %v", errormessage.FormatError(err))
|
||||
}
|
||||
|
||||
if err := kubernetes.NewPortForward(kubernetesProvider, config.Config.MizuResourcesNamespace, kubernetes.ApiServerPodName, config.Config.Tap.GuiPort, cancel); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running port forward %v\n"+
|
||||
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
apiProvider = apiserver.NewProvider(GetApiServerUrl(), apiserver.DefaultRetries, apiserver.DefaultTimeout) // long check for port-forward
|
||||
if err := apiProvider.TestConnection(); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Couldn't connect to API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getKubernetesProviderForCli() (*kubernetes.Provider, error) {
|
||||
|
||||
@@ -46,7 +46,8 @@ func runMizuInstall() {
|
||||
|
||||
if err = resources.CreateInstallMizuResources(ctx, kubernetesProvider, serializedValidationRules,
|
||||
serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(),
|
||||
config.Config.MizuResourcesNamespace, config.Config.AgentImage,
|
||||
config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.BasenineImage,
|
||||
config.Config.KratosImage,
|
||||
nil, defaultMaxEntriesDBSizeBytes, defaultResources, config.Config.ImagePullPolicy(),
|
||||
config.Config.LogLevel(), false); err != nil {
|
||||
var statusError *k8serrors.StatusError
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/errormessage"
|
||||
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/kubernetes"
|
||||
@@ -124,7 +123,7 @@ func RunMizuTap() {
|
||||
}
|
||||
|
||||
logger.Log.Infof("Waiting for Mizu Agent to start...")
|
||||
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, getSyncEntriesConfig(), config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel()); err != nil {
|
||||
if state.mizuServiceAccountExists, err = resources.CreateTapMizuResources(ctx, kubernetesProvider, serializedValidationRules, serializedContract, serializedMizuConfig, config.Config.IsNsRestrictedMode(), config.Config.MizuResourcesNamespace, config.Config.AgentImage, config.Config.BasenineImage, getSyncEntriesConfig(), config.Config.Tap.MaxEntriesDBSizeBytes(), config.Config.Tap.ApiServerResources, config.Config.ImagePullPolicy(), config.Config.LogLevel()); err != nil {
|
||||
var statusError *k8serrors.StatusError
|
||||
if errors.As(err, &statusError) {
|
||||
if statusError.ErrStatus.Reason == metav1.StatusReasonAlreadyExists {
|
||||
@@ -416,20 +415,15 @@ func watchApiServerEvents(ctx context.Context, kubernetesProvider *kubernetes.Pr
|
||||
}
|
||||
|
||||
func postApiServerStarted(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, err error) {
|
||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
|
||||
url := GetApiServerUrl()
|
||||
if err := apiProvider.TestConnection(); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Couldn't connect to API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
options, _ := getMizuApiFilteringOptions()
|
||||
if err = startTapperSyncer(ctx, cancel, kubernetesProvider, state.targetNamespaces, *options, state.startTime); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error starting mizu tapper syncer: %v", err))
|
||||
cancel()
|
||||
}
|
||||
|
||||
url := GetApiServerUrl()
|
||||
logger.Log.Infof("Mizu is available at %s", url)
|
||||
if !config.Config.HeadlessMode {
|
||||
uiUtils.OpenBrowser(url)
|
||||
|
||||
@@ -47,8 +47,7 @@ func runMizuView() {
|
||||
return
|
||||
}
|
||||
logger.Log.Infof("Establishing connection to k8s cluster...")
|
||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
|
||||
startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
}
|
||||
|
||||
apiServerProvider := apiserver.NewProvider(url, apiserver.DefaultRetries, apiserver.DefaultTimeout)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/op/go-logging"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
"os"
|
||||
@@ -26,6 +27,8 @@ type ConfigStruct struct {
|
||||
Auth configStructs.AuthConfig `yaml:"auth"`
|
||||
Config configStructs.ConfigConfig `yaml:"config,omitempty"`
|
||||
AgentImage string `yaml:"agent-image,omitempty" readonly:""`
|
||||
BasenineImage string `yaml:"basenine-image,omitempty" readonly:""`
|
||||
KratosImage string `yaml:"kratos-image,omitempty" readonly:""`
|
||||
ImagePullPolicyStr string `yaml:"image-pull-policy" default:"Always"`
|
||||
MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"`
|
||||
Telemetry bool `yaml:"telemetry" default:"true"`
|
||||
@@ -47,6 +50,8 @@ func (config *ConfigStruct) validate() error {
|
||||
}
|
||||
|
||||
func (config *ConfigStruct) SetDefaults() {
|
||||
config.BasenineImage = fmt.Sprintf("%s:%s", shared.BasenineImageRepo, shared.BasenineImageTag)
|
||||
config.KratosImage = shared.KratosImageDefault
|
||||
config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer)
|
||||
config.ConfigFilePath = path.Join(mizu.GetMizuFolderPath(), "config.yaml")
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
@@ -336,6 +337,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
|
||||
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level) (bool, error) {
|
||||
func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, basenineImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level) (bool, error) {
|
||||
if !isNsRestrictedMode {
|
||||
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
|
||||
return false, err
|
||||
@@ -42,6 +42,8 @@ func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.
|
||||
Namespace: mizuResourcesNamespace,
|
||||
PodName: kubernetes.ApiServerPodName,
|
||||
PodImage: agentImage,
|
||||
BasenineImage: basenineImage,
|
||||
KratosImage: "",
|
||||
ServiceAccountName: serviceAccountName,
|
||||
IsNamespaceRestricted: isNsRestrictedMode,
|
||||
SyncEntriesConfig: syncEntriesConfig,
|
||||
@@ -65,7 +67,7 @@ func CreateTapMizuResources(ctx context.Context, kubernetesProvider *kubernetes.
|
||||
return mizuServiceAccountExists, nil
|
||||
}
|
||||
|
||||
func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, noPersistentVolumeClaim bool) error {
|
||||
func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, serializedValidationRules string, serializedContract string, serializedMizuConfig string, isNsRestrictedMode bool, mizuResourcesNamespace string, agentImage string, basenineImage string, kratosImage string, syncEntriesConfig *shared.SyncEntriesConfig, maxEntriesDBSizeBytes int64, apiServerResources shared.Resources, imagePullPolicy core.PullPolicy, logLevel logging.Level, noPersistentVolumeClaim bool) error {
|
||||
if err := createMizuNamespace(ctx, kubernetesProvider, mizuResourcesNamespace); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,6 +97,8 @@ func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kuberne
|
||||
Namespace: mizuResourcesNamespace,
|
||||
PodName: kubernetes.ApiServerPodName,
|
||||
PodImage: agentImage,
|
||||
BasenineImage: basenineImage,
|
||||
KratosImage: kratosImage,
|
||||
ServiceAccountName: serviceAccountName,
|
||||
IsNamespaceRestricted: isNsRestrictedMode,
|
||||
SyncEntriesConfig: syncEntriesConfig,
|
||||
@@ -113,7 +117,7 @@ func CreateInstallMizuResources(ctx context.Context, kubernetesProvider *kuberne
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Log.Infof("service/%v created", kubernetes.ApiServerPodName)
|
||||
logger.Log.Infof("service/%v created", kubernetes.ApiServerPodName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# creates image in which mizu agent is remotely debuggable using delve
|
||||
FROM node:14-slim AS site-build
|
||||
FROM node:16-slim AS site-build
|
||||
|
||||
WORKDIR /app/ui-build
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ const (
|
||||
BaseninePort = "9099"
|
||||
BasenineImageRepo = "ghcr.io/up9inc/basenine"
|
||||
BasenineImageTag = "v0.3.0"
|
||||
KratosImageDefault = "gcr.io/up9-docker-hub/mizu-kratos/stable:0.0.0"
|
||||
)
|
||||
|
||||
@@ -177,6 +177,8 @@ type ApiServerOptions struct {
|
||||
Namespace string
|
||||
PodName string
|
||||
PodImage string
|
||||
BasenineImage string
|
||||
KratosImage string
|
||||
ServiceAccountName string
|
||||
IsNamespaceRestricted bool
|
||||
SyncEntriesConfig *shared.SyncEntriesConfig
|
||||
@@ -184,6 +186,7 @@ type ApiServerOptions struct {
|
||||
Resources shared.Resources
|
||||
ImagePullPolicy core.PullPolicy
|
||||
LogLevel logging.Level
|
||||
|
||||
}
|
||||
|
||||
func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, mountVolumeClaim bool, volumeClaimName string, createAuthContainer bool) (*core.Pod, error) {
|
||||
@@ -280,7 +283,7 @@ func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, moun
|
||||
},
|
||||
{
|
||||
Name: "basenine",
|
||||
Image: fmt.Sprintf("%s:%s", shared.BasenineImageRepo, shared.BasenineImageTag),
|
||||
Image: opts.BasenineImage,
|
||||
ImagePullPolicy: opts.ImagePullPolicy,
|
||||
VolumeMounts: volumeMounts,
|
||||
ReadinessProbe: &core.Probe{
|
||||
@@ -313,7 +316,7 @@ func (provider *Provider) GetMizuApiServerPodObject(opts *ApiServerOptions, moun
|
||||
if createAuthContainer {
|
||||
containers = append(containers, core.Container{
|
||||
Name: "kratos",
|
||||
Image: "gcr.io/up9-docker-hub/mizu-kratos/stable:0.0.0",
|
||||
Image: opts.KratosImage,
|
||||
ImagePullPolicy: opts.ImagePullPolicy,
|
||||
VolumeMounts: volumeMounts,
|
||||
ReadinessProbe: &core.Probe{
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
"k8s.io/client-go/transport/spdy"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,8 +21,8 @@ import (
|
||||
const k8sProxyApiPrefix = "/"
|
||||
const mizuServicePort = 80
|
||||
|
||||
func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16, mizuNamespace string, mizuServiceName string) error {
|
||||
logger.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
|
||||
func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16, mizuNamespace string, mizuServiceName string, cancel context.CancelFunc) (*http.Server, error) {
|
||||
logger.Log.Debugf("Starting proxy using proxy method. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
|
||||
filter := &proxy.FilterServer{
|
||||
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
|
||||
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
|
||||
@@ -25,7 +32,7 @@ func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16,
|
||||
|
||||
proxyHandler, err := proxy.NewProxyHandler(k8sProxyApiPrefix, filter, &kubernetesProvider.clientConfig, time.Second*2)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(k8sProxyApiPrefix, getRerouteHttpHandlerMizuAPI(proxyHandler, mizuNamespace, mizuServiceName))
|
||||
@@ -33,14 +40,21 @@ func StartProxy(kubernetesProvider *Provider, proxyHost string, mizuPort uint16,
|
||||
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", proxyHost, int(mizuPort)))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
server := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
return server.Serve(l)
|
||||
go func() {
|
||||
if err := server.Serve(l); err != nil && err != http.ErrServerClosed {
|
||||
logger.Log.Errorf("Error creating proxy, %v", err)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func getMizuApiServerProxiedHostAndPath(mizuNamespace string, mizuServiceName string) string {
|
||||
@@ -69,3 +83,42 @@ func getRerouteHttpHandlerMizuStatic(proxyHandler http.Handler, mizuNamespace st
|
||||
proxyHandler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func NewPortForward(kubernetesProvider *Provider, namespace string, podName string, localPort uint16, cancel context.CancelFunc) error {
|
||||
logger.Log.Debugf("Starting proxy using port-forward method. namespace: [%v], service name: [%s], port: [%v]", namespace, podName, localPort)
|
||||
|
||||
dialer, err := getHttpDialer(kubernetesProvider, namespace, podName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1)
|
||||
out, errOut := new(bytes.Buffer), new(bytes.Buffer)
|
||||
|
||||
forwarder, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", localPort, shared.DefaultApiServerPort)}, stopChan, readyChan, out, errOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err = forwarder.ForwardPorts(); err != nil {
|
||||
logger.Log.Errorf("kubernetes port-forwarding error: %v", err)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHttpDialer(kubernetesProvider *Provider, namespace string, podName string) (httpstream.Dialer, error) {
|
||||
roundTripper, upgrader, err := spdy.RoundTripperFor(&kubernetesProvider.clientConfig)
|
||||
if err != nil {
|
||||
logger.Log.Errorf("Error creating http dialer")
|
||||
return nil, err
|
||||
}
|
||||
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName)
|
||||
hostIP := strings.TrimLeft(kubernetesProvider.clientConfig.Host, "htps:/") // no need specify "t" twice
|
||||
serverURL := url.URL{Scheme: "https", Path: path, Host: hostIP}
|
||||
|
||||
return spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, &serverURL), nil
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
|
||||
streamID,
|
||||
"HTTP2",
|
||||
)
|
||||
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime)
|
||||
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime, messageHTTP1.ProtoMinor)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.SrcIP,
|
||||
@@ -86,7 +86,7 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
|
||||
streamID,
|
||||
"HTTP2",
|
||||
)
|
||||
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime)
|
||||
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime, messageHTTP1.ProtoMinor)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.DstIP,
|
||||
@@ -135,7 +135,7 @@ func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
|
||||
counterPair.Request,
|
||||
"HTTP1",
|
||||
)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime, req.ProtoMinor)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.SrcIP,
|
||||
@@ -175,7 +175,7 @@ func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
|
||||
counterPair.Response,
|
||||
"HTTP1",
|
||||
)
|
||||
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime)
|
||||
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime, res.ProtoMinor)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.DstIP,
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/up9inc/mizu/tap/api"
|
||||
@@ -18,16 +19,55 @@ func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}
|
||||
return
|
||||
}
|
||||
|
||||
func mapSliceMergeRepeatedKeys(mapSlice []interface{}) (newMapSlice []interface{}) {
|
||||
newMapSlice = make([]interface{}, 0)
|
||||
valuesMap := make(map[string][]interface{})
|
||||
for _, item := range mapSlice {
|
||||
h := item.(map[string]interface{})
|
||||
key := h["name"].(string)
|
||||
valuesMap[key] = append(valuesMap[key], h["value"])
|
||||
}
|
||||
|
||||
for key, values := range valuesMap {
|
||||
h := make(map[string]interface{})
|
||||
h["name"] = key
|
||||
if len(values) == 1 {
|
||||
h["value"] = values[0]
|
||||
} else {
|
||||
h["value"] = values
|
||||
}
|
||||
newMapSlice = append(newMapSlice, h)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (representation string) {
|
||||
var table []api.TableData
|
||||
for _, item := range mapSlice {
|
||||
h := item.(map[string]interface{})
|
||||
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, h["name"].(string))
|
||||
table = append(table, api.TableData{
|
||||
Name: h["name"].(string),
|
||||
Value: h["value"],
|
||||
Selector: selector,
|
||||
})
|
||||
key := h["name"].(string)
|
||||
value := h["value"]
|
||||
switch reflect.TypeOf(value).Kind() {
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
for i, el := range value.([]interface{}) {
|
||||
selector := fmt.Sprintf("%s.%s[%d]", selectorPrefix, key, i)
|
||||
table = append(table, api.TableData{
|
||||
Name: fmt.Sprintf("%s [%d]", key, i),
|
||||
Value: el,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
default:
|
||||
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, key)
|
||||
table = append(table, api.TableData{
|
||||
Name: key,
|
||||
Value: value,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
obj, _ := json.Marshal(table)
|
||||
|
||||
@@ -235,8 +235,8 @@ func checkIsHTTP2ServerStream(b *bufio.Reader) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If response starts with this text, it is HTTP/1.x
|
||||
if bytes.Compare(buf, []byte("HTTP/1.0 ")) == 0 || bytes.Compare(buf, []byte("HTTP/1.1 ")) == 0 {
|
||||
// If response starts with HTTP/1. then it's not HTTP/2
|
||||
if bytes.HasPrefix(buf, []byte("HTTP/1.")) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,21 @@ import (
|
||||
"github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
var protocol api.Protocol = api.Protocol{
|
||||
var http10protocol api.Protocol = api.Protocol{
|
||||
Name: "http",
|
||||
LongName: "Hypertext Transfer Protocol -- HTTP/1.0",
|
||||
Abbreviation: "HTTP",
|
||||
Macro: "http",
|
||||
Version: "1.0",
|
||||
BackgroundColor: "#205cf5",
|
||||
ForegroundColor: "#ffffff",
|
||||
FontSize: 12,
|
||||
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc1945",
|
||||
Ports: []string{"80", "443", "8080"},
|
||||
Priority: 0,
|
||||
}
|
||||
|
||||
var http11protocol api.Protocol = api.Protocol{
|
||||
Name: "http",
|
||||
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
|
||||
Abbreviation: "HTTP",
|
||||
@@ -69,12 +83,12 @@ func init() {
|
||||
type dissecting string
|
||||
|
||||
func (d dissecting) Register(extension *api.Extension) {
|
||||
extension.Protocol = &protocol
|
||||
extension.Protocol = &http11protocol
|
||||
extension.MatcherMap = reqResMatcher.openMessagesMap
|
||||
}
|
||||
|
||||
func (d dissecting) Ping() {
|
||||
log.Printf("pong %s", protocol.Name)
|
||||
log.Printf("pong %s", http11protocol.Name)
|
||||
}
|
||||
|
||||
func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, superIdentifier *api.SuperIdentifier, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
|
||||
@@ -96,7 +110,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
http2Assembler = createHTTP2Assembler(b)
|
||||
}
|
||||
|
||||
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &protocol {
|
||||
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &http11protocol {
|
||||
return errors.New("Identified by another protocol")
|
||||
}
|
||||
|
||||
@@ -128,7 +142,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
tcpID.DstPort,
|
||||
"HTTP2",
|
||||
)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
|
||||
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime, req.ProtoMinor)
|
||||
if item != nil {
|
||||
item.ConnectionInfo = &api.ConnectionInfo{
|
||||
ClientIP: tcpID.SrcIP,
|
||||
@@ -154,7 +168,7 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
|
||||
if !dissected {
|
||||
return err
|
||||
}
|
||||
superIdentifier.Protocol = &protocol
|
||||
superIdentifier.Protocol = &http11protocol
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,7 +239,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
resDetails["cookies"] = mapSliceRebuildAsMap(resDetails["_cookies"].([]interface{}))
|
||||
|
||||
reqDetails["_queryString"] = reqDetails["queryString"]
|
||||
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["_queryString"].([]interface{}))
|
||||
reqDetails["_queryStringMerged"] = mapSliceMergeRepeatedKeys(reqDetails["_queryString"].([]interface{}))
|
||||
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["_queryStringMerged"].([]interface{}))
|
||||
|
||||
method := reqDetails["method"].(string)
|
||||
statusCode := int(resDetails["status"].(float64))
|
||||
@@ -322,7 +337,7 @@ func representRequest(request map[string]interface{}) (repRequest []interface{})
|
||||
repRequest = append(repRequest, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Query String",
|
||||
Data: representMapSliceAsTable(request["_queryString"].([]interface{}), `request.queryString`),
|
||||
Data: representMapSliceAsTable(request["_queryStringMerged"].([]interface{}), `request.queryString`),
|
||||
})
|
||||
|
||||
postData, _ := request["postData"].(map[string]interface{})
|
||||
@@ -442,9 +457,9 @@ func (d dissecting) Represent(request map[string]interface{}, response map[strin
|
||||
|
||||
func (d dissecting) Macros() map[string]string {
|
||||
return map[string]string{
|
||||
`http`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, protocol.Name, protocol.Version),
|
||||
`http2`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, protocol.Name, http2Protocol.Version),
|
||||
`grpc`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s" and proto.macro == "%s"`, protocol.Name, grpcProtocol.Version, grpcProtocol.Macro),
|
||||
`http`: fmt.Sprintf(`proto.name == "%s" and proto.version.startsWith("%c")`, http11protocol.Name, http11protocol.Version[0]),
|
||||
`http2`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s"`, http11protocol.Name, http2Protocol.Version),
|
||||
`grpc`: fmt.Sprintf(`proto.name == "%s" and proto.version == "%s" and proto.macro == "%s"`, http11protocol.Name, grpcProtocol.Version, grpcProtocol.Macro),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func createResponseRequestMatcher() requestResponseMatcher {
|
||||
return *newMatcher
|
||||
}
|
||||
|
||||
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time) *api.OutputChannelItem {
|
||||
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time, protoMinor int) *api.OutputChannelItem {
|
||||
split := splitIdent(ident)
|
||||
key := genKey(split)
|
||||
|
||||
@@ -41,14 +41,14 @@ func (matcher *requestResponseMatcher) registerRequest(ident string, request *ht
|
||||
if responseHTTPMessage.IsRequest {
|
||||
return nil
|
||||
}
|
||||
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage)
|
||||
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage, protoMinor)
|
||||
}
|
||||
|
||||
matcher.openMessagesMap.Store(key, &requestHTTPMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time) *api.OutputChannelItem {
|
||||
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time, protoMinor int) *api.OutputChannelItem {
|
||||
split := splitIdent(ident)
|
||||
key := genKey(split)
|
||||
|
||||
@@ -67,14 +67,18 @@ func (matcher *requestResponseMatcher) registerResponse(ident string, response *
|
||||
if !requestHTTPMessage.IsRequest {
|
||||
return nil
|
||||
}
|
||||
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage)
|
||||
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage, protoMinor)
|
||||
}
|
||||
|
||||
matcher.openMessagesMap.Store(key, &responseHTTPMessage)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *api.GenericMessage, responseHTTPMessage *api.GenericMessage) *api.OutputChannelItem {
|
||||
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *api.GenericMessage, responseHTTPMessage *api.GenericMessage, protoMinor int) *api.OutputChannelItem {
|
||||
protocol := http11protocol
|
||||
if protoMinor == 0 {
|
||||
protocol = http10protocol
|
||||
}
|
||||
return &api.OutputChannelItem{
|
||||
Protocol: protocol,
|
||||
Timestamp: requestHTTPMessage.CaptureTime.UnixNano() / int64(time.Millisecond),
|
||||
|
||||
31720
ui/package-lock.json
generated
31720
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,24 +14,31 @@
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@uiw/react-textarea-code-editor": "^1.4.12",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.25.0",
|
||||
"core-js": "^3.20.2",
|
||||
"highlight.js": "^11.3.1",
|
||||
"json-beautify": "^1.1.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
"marked": "^4.0.10",
|
||||
"material-ui-popup-state": "^2.0.0",
|
||||
"mobx": "^6.3.10",
|
||||
"moment": "^2.29.1",
|
||||
"node-sass": "^5.0.0",
|
||||
"node-fetch": "^3.1.1",
|
||||
"node-sass": "^6.0.0",
|
||||
"numeral": "^2.0.6",
|
||||
"protobuf-decoder": "^0.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-graph-vis": "^1.0.7",
|
||||
"react-lowlight": "^3.0.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scrollable-feed-virtualized": "^1.4.9",
|
||||
"react-syntax-highlighter": "^15.4.3",
|
||||
"react-toastify": "^8.0.3",
|
||||
"recoil": "^0.5.2",
|
||||
"redoc": "^2.0.0-rc.59",
|
||||
"styled-components": "^5.3.3",
|
||||
"typescript": "^4.2.4",
|
||||
"web-vitals": "^1.1.1",
|
||||
"xml-formatter": "^2.6.0"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
// Injected from server
|
||||
window.isEnt = __IS_STANDALONE__
|
||||
window.isOasEnabled = __IS_OAS_ENABLED__
|
||||
window.isServiceMapEnabled = __IS_SERVICE_MAP_ENABLED__
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import './App.sass';
|
||||
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
||||
import {Header} from "./components/Header/Header";
|
||||
import {TrafficPage} from "./components/TrafficPage";
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
|
||||
const App = () => {
|
||||
|
||||
@@ -10,6 +11,7 @@ const App = () => {
|
||||
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
||||
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||
|
||||
const onTLSDetected = (destAddress: string) => {
|
||||
addressesWithTLS.add(destAddress);
|
||||
@@ -22,14 +24,19 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<div className="mizuApp">
|
||||
<Header analyzeStatus={analyzeStatus}/>
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
||||
<Header analyzeStatus={analyzeStatus} />
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />
|
||||
<TLSWarning showTLSWarning={showTLSWarning}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
|
||||
{window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
isOpen={openServiceMapModal}
|
||||
onOpen={() => setOpenServiceMapModal(true)}
|
||||
onClose={() => setOpenServiceMapModal(false)}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import LoadingOverlay from "./components/LoadingOverlay";
|
||||
import AuthPageBase from './components/AuthPageBase';
|
||||
import entPageAtom, {Page} from "./recoil/entPage";
|
||||
import {useRecoilState} from "recoil";
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
@@ -22,6 +23,7 @@ const EntApp = () => {
|
||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||
const [entPage, setEntPage] = useRecoilState(entPageAtom);
|
||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||
|
||||
const determinePage = useCallback(async () => { // TODO: move to state management
|
||||
try {
|
||||
@@ -59,7 +61,7 @@ const EntApp = () => {
|
||||
|
||||
switch (entPage) { // TODO: move to state management / proper routing
|
||||
case Page.Traffic:
|
||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
|
||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />;
|
||||
break;
|
||||
case Page.Setup:
|
||||
pageComponent = <AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>;
|
||||
@@ -77,14 +79,19 @@ const EntApp = () => {
|
||||
|
||||
return (
|
||||
<div className="mizuApp">
|
||||
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
|
||||
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin} />}
|
||||
{pageComponent}
|
||||
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />}
|
||||
{entPage === Page.Traffic && window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
isOpen={openServiceMapModal}
|
||||
onOpen={() => setOpenServiceMapModal(true)}
|
||||
onClose={() => setOpenServiceMapModal(false)}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import background from "./assets/authBackground.png";
|
||||
import logo from './assets/MizuEntLogoFull.svg';
|
||||
import logo from './assets/MizuEntLogoNoPowBy.svg';
|
||||
import poweredBy from './assets/powered-by.svg'
|
||||
import "./style/AuthBasePage.sass";
|
||||
|
||||
|
||||
@@ -10,6 +11,9 @@ export const AuthPageBase: React.FC = ({children}) => {
|
||||
<img alt="logo" src={logo}/>
|
||||
</div>
|
||||
{children}
|
||||
<div className="authFooter">
|
||||
<img alt="logo" src={poweredBy}/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
.resolvedName
|
||||
text-overflow: ellipsis
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
color: $secondary-font-color
|
||||
padding-left: 4px
|
||||
|
||||
@@ -140,7 +140,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
|
||||
setFocusedEntryId(entry.id.toString());
|
||||
}}
|
||||
style={{
|
||||
border: isSelected ? `1px ${entry.proto.backgroundColor} solid` : "1px transparent solid",
|
||||
border: isSelected && !headingMode ? `1px ${entry.proto.backgroundColor} solid` : "1px transparent solid",
|
||||
position: !headingMode ? "absolute" : "unset",
|
||||
top: style['top'],
|
||||
marginTop: !headingMode ? style['marginTop'] : "10px",
|
||||
@@ -162,7 +162,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
style={{marginTop: "-4px", overflow: "visible"}}
|
||||
iconStyle={!headingMode ? {marginTop: "4px", left: "68px", position: "absolute"} :
|
||||
iconStyle={!headingMode ? {marginTop: "4px", right: "16px", position: "relative"} :
|
||||
entry.proto.name === "http" ? {marginTop: "4px", left: "calc(50vw + 41px)", position: "absolute"} :
|
||||
{marginTop: "4px", left: "calc(50vw - 9px)", position: "absolute"}}
|
||||
>
|
||||
@@ -172,16 +172,16 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
|
||||
{entry.src.name ? entry.src.name : "[Unresolved]"}
|
||||
</span>
|
||||
</Queryable>
|
||||
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
|
||||
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px",marginLeft:"5px",marginRight:"5px"}}></SwapHorizIcon>
|
||||
<Queryable
|
||||
query={`dst.name == "${entry.dst.name}"`}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
style={{marginTop: "-4px"}}
|
||||
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
|
||||
iconStyle={{marginTop: "4px", marginLeft: "-2px",right: "11px", position: "relative"}}
|
||||
>
|
||||
<span
|
||||
title="Destination Name"
|
||||
>
|
||||
title="Destination Name">
|
||||
{entry.dst.name ? entry.dst.name : "[Unresolved]"}
|
||||
</span>
|
||||
</Queryable>
|
||||
|
||||
6
ui/src/components/OasModal/OasModal.sass
Normal file
6
ui/src/components/OasModal/OasModal.sass
Normal file
@@ -0,0 +1,6 @@
|
||||
@import '../../variables.module.scss'
|
||||
|
||||
.NotSelectedMessage
|
||||
margin-left: 41%
|
||||
padding-top: 3%
|
||||
font-size: large
|
||||
95
ui/src/components/OasModal/OasModal.tsx
Normal file
95
ui/src/components/OasModal/OasModal.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Box, Fade, FormControl, MenuItem, Modal } from "@material-ui/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RedocStandalone } from "redoc";
|
||||
import Api from "../../helpers/api";
|
||||
import { Select } from "../UI/Select";
|
||||
import closeIcon from "../assets/closeIcon.svg";
|
||||
import { toast } from 'react-toastify';
|
||||
import './OasModal.sass'
|
||||
|
||||
const api = Api.getInstance();
|
||||
const noOasServiceSelectedMessage = "Please Select OasService";
|
||||
|
||||
const OasModal = ({ openModal, handleCloseModal }) => {
|
||||
const [oasServices, setOasServices] = useState([])
|
||||
const [selectedServiceName, setSelectedServiceName] = useState("");
|
||||
const [selectedServiceSpec, setSelectedServiceSpec] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const services = await api.getOasServices();
|
||||
setOasServices(services);
|
||||
} catch (e) {
|
||||
toast.error("Error occurred while fetching services list");
|
||||
console.error(e);
|
||||
}
|
||||
})();
|
||||
}, [openModal]);
|
||||
|
||||
const onSelectedOASService = async (selectedService) => {
|
||||
setSelectedServiceName(selectedService);
|
||||
if(oasServices.length === 0){
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await api.getOasByService(selectedService);
|
||||
setSelectedServiceSpec(data);
|
||||
} catch (e) {
|
||||
toast.error("Error occurred while fetching service OAS spec");
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={openModal}
|
||||
onClose={handleCloseModal}
|
||||
closeAfterTransition
|
||||
hideBackdrop={true}
|
||||
style={{ overflow: "auto", backgroundColor: "#ffffff", color:"black" }}
|
||||
>
|
||||
<Fade in={openModal}>
|
||||
<Box>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
padding: "1%",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginLeft: "40%" }}>
|
||||
<FormControl>
|
||||
<Select
|
||||
labelId="service-select-label"
|
||||
id="service-select"
|
||||
label="Show OAS"
|
||||
placeholder="Show OAS"
|
||||
value={selectedServiceName}
|
||||
onChange={onSelectedOASService}
|
||||
>
|
||||
{oasServices.map((service) => (
|
||||
<MenuItem key={service} value={service}>
|
||||
{service}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div style={{ cursor: "pointer" }}>
|
||||
<img src={closeIcon} alt="Back" onClick={handleCloseModal} />
|
||||
</div>
|
||||
</div>
|
||||
{selectedServiceSpec && <RedocStandalone spec={selectedServiceSpec} />}
|
||||
<div className="NotSelectedMessage">
|
||||
{!selectedServiceName && noOasServiceSelectedMessage}
|
||||
</div>
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default OasModal;
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Filters} from "./Filters";
|
||||
import {EntriesList} from "./EntriesList";
|
||||
import {makeStyles} from "@material-ui/core";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Filters } from "./Filters";
|
||||
import { EntriesList } from "./EntriesList";
|
||||
import { makeStyles, Button } from "@material-ui/core";
|
||||
import "./style/TrafficPage.sass";
|
||||
import styles from './style/EntriesList.module.sass';
|
||||
import {EntryDetailed} from "./EntryDetailed";
|
||||
@@ -18,36 +18,39 @@ import entriesAtom from "../recoil/entries";
|
||||
import focusedEntryIdAtom from "../recoil/focusedEntryId";
|
||||
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
||||
import queryAtom from "../recoil/query";
|
||||
import OasModal from "./OasModal/OasModal";
|
||||
import {useCommonStyles} from "../helpers/commonStyle"
|
||||
|
||||
const useLayoutStyles = makeStyles(() => ({
|
||||
details: {
|
||||
flex: "0 0 50%",
|
||||
width: "45vw",
|
||||
padding: "12px 24px",
|
||||
borderRadius: 4,
|
||||
marginTop: 15,
|
||||
background: variables.headerBackgroundColor,
|
||||
},
|
||||
details: {
|
||||
flex: "0 0 50%",
|
||||
width: "45vw",
|
||||
padding: "12px 24px",
|
||||
borderRadius: 4,
|
||||
marginTop: 15,
|
||||
background: variables.headerBackgroundColor,
|
||||
},
|
||||
|
||||
viewer: {
|
||||
display: 'flex',
|
||||
overflowY: 'auto',
|
||||
height: "calc(100% - 70px)",
|
||||
padding: 5,
|
||||
paddingBottom: 0,
|
||||
overflow: "auto",
|
||||
}
|
||||
viewer: {
|
||||
display: "flex",
|
||||
overflowY: "auto",
|
||||
height: "calc(100% - 70px)",
|
||||
padding: 5,
|
||||
paddingBottom: 0,
|
||||
overflow: "auto",
|
||||
},
|
||||
}));
|
||||
|
||||
interface TrafficPageProps {
|
||||
onTLSDetected: (destAddress: string) => void;
|
||||
setAnalyzeStatus?: (status: any) => void;
|
||||
setAnalyzeStatus?: (status: any) => void;
|
||||
onTLSDetected: (destAddress: string) => void;
|
||||
setOpenServiceMapModal?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnalyzeStatus}) => {
|
||||
|
||||
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected, setOpenServiceMapModal}) => {
|
||||
const commonClasses = useCommonStyles();
|
||||
const classes = useLayoutStyles();
|
||||
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
|
||||
const [entries, setEntries] = useRecoilState(entriesAtom);
|
||||
@@ -67,24 +70,31 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
||||
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
|
||||
|
||||
const [startTime, setStartTime] = useState(0);
|
||||
|
||||
const scrollableRef = useRef(null);
|
||||
|
||||
const handleQueryChange = useMemo(() => debounce(async (query: string) => {
|
||||
if (!query) {
|
||||
setQueryBackgroundColor("#f5f5f5")
|
||||
} else {
|
||||
const [openOasModal, setOpenOasModal] = useState(false);
|
||||
const handleOpenModal = () => setOpenOasModal(true);
|
||||
const handleCloseModal = () => setOpenOasModal(false);
|
||||
|
||||
const handleQueryChange = useMemo(
|
||||
() =>
|
||||
debounce(async (query: string) => {
|
||||
if (!query) {
|
||||
setQueryBackgroundColor("#f5f5f5");
|
||||
} else {
|
||||
const data = await api.validateQuery(query);
|
||||
if (!data) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
if (data.valid) {
|
||||
setQueryBackgroundColor("#d2fad2");
|
||||
setQueryBackgroundColor("#d2fad2");
|
||||
} else {
|
||||
setQueryBackgroundColor("#fad6dc");
|
||||
setQueryBackgroundColor("#fad6dc");
|
||||
}
|
||||
}
|
||||
}, 500), []) as (query: string) => void;
|
||||
}
|
||||
}, 500),
|
||||
[]
|
||||
) as (query: string) => void;
|
||||
|
||||
useEffect(() => {
|
||||
handleQueryChange(query);
|
||||
@@ -93,7 +103,6 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
||||
const ws = useRef(null);
|
||||
|
||||
const listEntry = useRef(null);
|
||||
|
||||
const openWebSocket = (query: string, resetEntries: boolean) => {
|
||||
if (resetEntries) {
|
||||
setFocusedEntryId(null);
|
||||
@@ -121,89 +130,90 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
||||
}
|
||||
|
||||
if (ws.current) {
|
||||
ws.current.onmessage = e => {
|
||||
if (!e?.data) return;
|
||||
const message = JSON.parse(e.data);
|
||||
switch (message.messageType) {
|
||||
case "entry":
|
||||
const entry = message.data;
|
||||
if (!focusedEntryId) setFocusedEntryId(entry.id.toString())
|
||||
const newEntries = [...entries, entry];
|
||||
if (newEntries.length === 10001) {
|
||||
setLeftOffTop(newEntries[0].entry.id);
|
||||
newEntries.shift();
|
||||
setNoMoreDataTop(false);
|
||||
}
|
||||
setEntries(newEntries);
|
||||
break
|
||||
case "status":
|
||||
setTappingStatus(message.tappingStatus);
|
||||
break
|
||||
case "analyzeStatus":
|
||||
if(setAnalyzeStatus)
|
||||
setAnalyzeStatus(message.analyzeStatus);
|
||||
break
|
||||
case "outboundLink":
|
||||
onTLSDetected(message.Data.DstIP);
|
||||
break;
|
||||
case "toast":
|
||||
toast[message.data.type](message.data.text, {
|
||||
position: "bottom-right",
|
||||
theme: "colored",
|
||||
autoClose: message.data.autoClose,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
});
|
||||
break;
|
||||
case "queryMetadata":
|
||||
setQueriedCurrent(queriedCurrent + message.data.current);
|
||||
setQueriedTotal(message.data.total);
|
||||
setLeftOffBottom(message.data.leftOff);
|
||||
setTruncatedTimestamp(message.data.truncatedTimestamp);
|
||||
if (leftOffTop === null) {
|
||||
setLeftOffTop(message.data.leftOff - 1);
|
||||
}
|
||||
break;
|
||||
case "startTime":
|
||||
setStartTime(message.data);
|
||||
break;
|
||||
default:
|
||||
console.error(`unsupported websocket message type, Got: ${message.messageType}`)
|
||||
ws.current.onmessage = (e) => {
|
||||
if (!e?.data) return;
|
||||
const message = JSON.parse(e.data);
|
||||
switch (message.messageType) {
|
||||
case "entry":
|
||||
const entry = message.data;
|
||||
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
|
||||
const newEntries = [...entries, entry];
|
||||
if (newEntries.length === 10001) {
|
||||
setLeftOffTop(newEntries[0].entry.id);
|
||||
newEntries.shift();
|
||||
setNoMoreDataTop(false);
|
||||
}
|
||||
setEntries(newEntries);
|
||||
break;
|
||||
case "status":
|
||||
setTappingStatus(message.tappingStatus);
|
||||
break;
|
||||
case "analyzeStatus":
|
||||
setAnalyzeStatus(message.analyzeStatus);
|
||||
break;
|
||||
case "outboundLink":
|
||||
onTLSDetected(message.Data.DstIP);
|
||||
break;
|
||||
case "toast":
|
||||
toast[message.data.type](message.data.text, {
|
||||
position: "bottom-right",
|
||||
theme: "colored",
|
||||
autoClose: message.data.autoClose,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
});
|
||||
break;
|
||||
case "queryMetadata":
|
||||
setQueriedCurrent(queriedCurrent + message.data.current);
|
||||
setQueriedTotal(message.data.total);
|
||||
setLeftOffBottom(message.data.leftOff);
|
||||
setTruncatedTimestamp(message.data.truncatedTimestamp);
|
||||
if (leftOffTop === null) {
|
||||
setLeftOffTop(message.data.leftOff - 1);
|
||||
}
|
||||
break;
|
||||
case "startTime":
|
||||
setStartTime(message.data);
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`unsupported websocket message type, Got: ${message.messageType}`
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
openWebSocket("leftOff(-1)", true);
|
||||
try{
|
||||
const tapStatusResponse = await api.tapStatus();
|
||||
setTappingStatus(tapStatusResponse);
|
||||
if(setAnalyzeStatus) {
|
||||
const analyzeStatusResponse = await api.analyzeStatus();
|
||||
setAnalyzeStatus(analyzeStatusResponse);
|
||||
(async () => {
|
||||
openWebSocket("leftOff(-1)", true);
|
||||
try{
|
||||
const tapStatusResponse = await api.tapStatus();
|
||||
setTappingStatus(tapStatusResponse);
|
||||
if(setAnalyzeStatus) {
|
||||
const analyzeStatusResponse = await api.analyzeStatus();
|
||||
setAnalyzeStatus(analyzeStatusResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})()
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
})()
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const toggleConnection = () => {
|
||||
ws.current.close();
|
||||
if (wsConnection !== WsConnectionStatus.Connected) {
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(-1)`, true);
|
||||
} else {
|
||||
openWebSocket(`leftOff(-1)`, true);
|
||||
}
|
||||
scrollableRef.current.jumpToBottom();
|
||||
setIsSnappedToBottom(true);
|
||||
}
|
||||
ws.current.close();
|
||||
if (wsConnection !== WsConnectionStatus.Connected) {
|
||||
if (query) {
|
||||
openWebSocket(`(${query}) and leftOff(-1)`, true);
|
||||
} else {
|
||||
openWebSocket(`leftOff(-1)`, true);
|
||||
}
|
||||
scrollableRef.current.jumpToBottom();
|
||||
setIsSnappedToBottom(true);
|
||||
}
|
||||
}
|
||||
|
||||
const getConnectionStatusClass = (isContainer) => {
|
||||
@@ -215,7 +225,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
||||
return "redIndicator" + container;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getConnectionTitle = () => {
|
||||
switch (wsConnection) {
|
||||
case WsConnectionStatus.Connected:
|
||||
@@ -232,56 +242,84 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="TrafficPage">
|
||||
<div className="TrafficPageHeader">
|
||||
<img className="playPauseIcon" style={{visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
|
||||
src={pauseIcon} onClick={toggleConnection}/>
|
||||
<img className="playPauseIcon" style={{position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
|
||||
src={playIcon} onClick={toggleConnection}/>
|
||||
<div className="connectionText">
|
||||
{getConnectionTitle()}
|
||||
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||
<div className={"indicator " + getConnectionStatusClass(false)}/>
|
||||
</div>
|
||||
</div>
|
||||
const openServiceMapModalDebounce = debounce(() => {
|
||||
setOpenServiceMapModal(true)
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<div className="TrafficPage">
|
||||
<div className="TrafficPageHeader">
|
||||
<div className="TrafficPageStreamStatus">
|
||||
<img className="playPauseIcon" style={{ visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden" }} alt="pause"
|
||||
src={pauseIcon} onClick={toggleConnection} />
|
||||
<img className="playPauseIcon" style={{ position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible" }} alt="play"
|
||||
src={playIcon} onClick={toggleConnection} />
|
||||
<div className="connectionText">
|
||||
{getConnectionTitle()}
|
||||
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||
<div className={"indicator " + getConnectionStatusClass(false)} />
|
||||
</div>
|
||||
{<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 && <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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export function getClassification(statusCode: number): string {
|
||||
let classification = StatusCodeClassification.NEUTRAL;
|
||||
|
||||
// 1 - 16 HTTP/2 (gRPC) status codes
|
||||
// 2xx - 5xx HTTP/1.1 status codes
|
||||
// 2xx - 5xx HTTP/1.x status codes
|
||||
if ((statusCode >= 200 && statusCode <= 399) || statusCode === 0) {
|
||||
classification = StatusCodeClassification.SUCCESS;
|
||||
} else if (statusCode >= 400 || (statusCode >= 1 && statusCode <= 16)) {
|
||||
|
||||
@@ -16,6 +16,8 @@ export const Summary: React.FC<SummaryProps> = ({method, summary}) => {
|
||||
className={`${miscStyles.protocol} ${miscStyles.method}`}
|
||||
displayIconOnMouseOver={true}
|
||||
style={{whiteSpace: "nowrap"}}
|
||||
flipped={true}
|
||||
iconStyle={{zIndex:"5",position:"relative",right:"22px"}}
|
||||
>
|
||||
<span>
|
||||
{method}
|
||||
@@ -24,6 +26,8 @@ export const Summary: React.FC<SummaryProps> = ({method, summary}) => {
|
||||
{summary && <Queryable
|
||||
query={`summary == "${summary}"`}
|
||||
displayIconOnMouseOver={true}
|
||||
flipped={true}
|
||||
iconStyle={{zIndex:"5",position:"relative",right:"14px"}}
|
||||
>
|
||||
<div
|
||||
className={`${styles.summary}`}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
fill: #627ef7
|
||||
|
||||
.list
|
||||
margin-top: 8px
|
||||
margin-top: 8px
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 8.7 KiB |
9
ui/src/components/assets/MizuEntLogoNoPowBy.svg
Normal file
9
ui/src/components/assets/MizuEntLogoNoPowBy.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="260" height="98" viewBox="0 0 260 98" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.5682 88.7976H36.3637L26.9319 65.8512C26.7046 65.2138 26.25 64.0585 25.5682 62.3854C24.9622 60.6325 24.2425 58.6805 23.4091 56.5293C22.6516 54.2984 21.8561 52.0276 21.0228 49.7171C20.1894 47.4065 19.4319 45.2951 18.75 43.3829C18.75 51.1491 18.6472 46.6153 18.4091 54.378C18.4091 58.801 14.8863 62.3854 10.6808 62.3854H10.2359C5.5626 62.3854 1.86898 58.2186 2.19613 53.3157L4.59971 17.2938C4.95322 11.9958 9.14291 7.8878 14.1928 7.8878H14.5752C18.6752 7.8878 22.3622 10.5131 23.8849 14.5167L39.0909 54.4976C39.6212 55.9317 40.3031 57.9634 41.1364 60.5927C42.0455 63.1423 42.9167 65.9309 43.75 68.9585C44.5833 66.0106 45.4167 63.2618 46.25 60.7122C47.1591 58.0829 47.9167 56.0114 48.5227 54.4976L63.7979 14.3351C65.2789 10.4411 68.8648 7.8878 72.8525 7.8878C78.015 7.8878 82.2832 12.1192 82.5877 17.5393L86.5909 88.7976H68.9773L68.2954 72.5439C68.2197 71.0301 68.1439 69.0382 68.0682 66.5683C68.0682 64.0984 68.0682 61.5089 68.0682 58.8C68.0682 56.0114 68.0682 53.2626 68.0682 50.5537C68.1439 47.765 68.1818 45.3748 68.1818 43.3829C67.5757 45.0561 66.8939 46.9683 66.1364 49.1195C65.4545 51.1911 64.7348 53.2228 63.9773 55.2146C63.2197 57.2065 62.5 59.0789 61.8182 60.8317C61.1364 62.5049 60.6061 63.8195 60.2273 64.7756L50.5682 88.7976Z" fill="#205CF5"/>
|
||||
<path d="M101.591 86.2878C98.636 83.1802 97.7832 80.0732 97.7832 75.7707V38.2439C97.7832 33.0295 101.802 28.8024 106.76 28.8024C111.718 28.8024 115.738 33.0295 115.738 38.2439V70.3927C115.738 72.2252 116.003 73.4602 116.533 74.0976C117.063 74.735 117.859 75.0537 118.92 75.0537C119.526 75.0537 130.189 75.0138 130.795 74.9341C131.401 74.7748 130.53 75.0138 130.682 74.9341L123.465 86.4073C123.389 86.487 123.048 86.6862 122.442 87.0049C121.836 87.2439 121.041 87.5626 120.056 87.961C115.341 87.1244 111.705 88.2 107.665 88.8162C104.091 88.7976 102.955 87.7219 101.591 86.2878ZM106.988 0C110.17 0 112.594 0.995934 114.26 2.9878C115.927 4.97967 116.76 7.48943 116.76 10.5171C116.76 13.6244 115.889 16.2138 114.147 18.2854C112.48 20.3569 109.942 21.3927 106.533 21.3927C103.351 21.3927 100.889 20.3967 99.1468 18.4049C97.4801 16.3333 96.6468 13.9033 96.6468 11.1146C96.6468 7.92764 97.518 5.29837 99.2604 3.22683C101.003 1.07561 103.579 0 106.988 0Z" fill="#205CF5"/>
|
||||
<path d="M126.778 88.7976V78.2805L148.369 42.9049C146.93 42.9846 145.566 43.0642 144.278 43.1439C142.99 43.1439 141.702 43.1439 140.415 43.1439H135.512C132.304 43.1439 129.562 40.7147 129.017 37.3899C128.322 33.1534 131.424 29.2805 135.512 29.2805H160.826C165.557 29.2805 169.392 33.3139 169.392 38.2894C169.392 39.9779 168.941 41.6324 168.09 43.0642L148.937 75.2927C149.922 75.213 150.869 75.1732 151.778 75.1732C152.763 75.0935 153.786 75.0537 154.846 75.0537H194.087V88.7976H126.778Z" fill="#205CF5"/>
|
||||
<path d="M207.269 38.2439C207.269 33.0295 211.288 28.8024 216.246 28.8024C221.204 28.8024 225.224 33.0295 225.224 38.2439V59.9951C225.224 64.7756 223.75 85.8097 237.273 86.0488C237.879 89.2358 226.098 85.5707 226.932 87.4829C227.841 89.7935 223.065 86.0488 226.133 88.7976L237.273 98C221.591 98 215.678 88.1203 214.996 87.0049C214.39 86.1285 213.784 85.013 213.178 83.6585C212.572 82.2244 212.424 82.3837 212.045 80.5512C210.076 84.2959 208.977 85.5582 205.227 87.4829C202.666 88.7976 198.102 88.7976 194.087 88.7976C191.133 88.7976 185.909 88.7976 184.091 88.7976C182.045 87.7618 181.019 85.6504 179.655 83.8976C178.368 82.065 177.421 79.874 176.815 77.3244C176.284 74.7748 176.019 71.9065 176.019 68.7195V38.3037C176.019 33.0563 180.064 28.8024 185.053 28.8024C190.043 28.8024 194.087 33.0563 194.087 38.3037V66.2098C194.087 68.9984 194.39 71.2691 194.996 73.0219C195.678 74.6951 196.89 75.5317 198.633 75.5317C200.754 75.5317 203.409 74.5358 205.227 71.5878C207.045 68.5602 207.269 63.939 207.269 58.6805V38.2439Z" fill="#205CF5"/>
|
||||
<path d="M107.5 78.2805H129.091V88.7976H107.5V78.2805Z" fill="#205CF5"/>
|
||||
<path d="M15.9219 72.2601C14.4153 70.4574 12.2239 69.5561 9.34771 69.5561C6.26605 69.5561 3.93768 70.5296 2.36261 72.4765C0.787536 74.3513 0 76.7308 0 79.6152C0 82.1389 0.753295 84.3382 2.25989 86.213C3.83496 88.0157 6.0606 88.9171 8.93682 88.9171C12.0185 88.9171 14.3126 87.9797 15.8192 86.1049C17.3943 84.23 18.1818 81.8866 18.1818 79.0743C18.1818 76.3342 17.4285 74.0628 15.9219 72.2601Z" fill="#205CF5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M218.863 64.7756C222.001 64.7756 224.545 67.451 224.545 70.7512C224.545 79.9648 230.619 86.0488 236.591 86.0488C242.562 86.0488 248.636 79.9648 248.636 70.7512C248.636 67.451 251.18 64.7756 254.318 64.7756C257.456 64.7756 260 67.451 260 70.7512C260 85.0354 250.2 98 236.591 98C222.981 98 213.182 85.0354 213.182 70.7512C213.182 67.451 215.725 64.7756 218.863 64.7756Z" fill="#205CF5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
4
ui/src/components/assets/closeIcon.svg
Normal file
4
ui/src/components/assets/closeIcon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.5 11C20.5 16.2467 16.2467 20.5 11 20.5C5.75329 20.5 1.5 16.2467 1.5 11C1.5 5.75329 5.75329 1.5 11 1.5C16.2467 1.5 20.5 5.75329 20.5 11Z" stroke="#2B3560"/>
|
||||
<path d="M14.4762 9.05338L13.1448 7.7219L11.1528 9.71382L9.16091 7.7219L7.82943 9.05338L9.82135 11.0453L7.83226 13.0344L9.16374 14.3659L11.1528 12.3768L13.1419 14.3659L14.4734 13.0344L12.4843 11.0453L14.4762 9.05338Z" fill="#627EF7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
12
ui/src/components/assets/powered-by.svg
Normal file
12
ui/src/components/assets/powered-by.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 21 KiB |
@@ -5,6 +5,9 @@
|
||||
width: 100vw
|
||||
background-size: cover
|
||||
float: left
|
||||
display: flex
|
||||
align-items: center
|
||||
flex-flow: column
|
||||
|
||||
.authHeader
|
||||
margin: 80px 0 120px 0
|
||||
@@ -13,6 +16,9 @@
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.authFooter
|
||||
margin-top: 3%
|
||||
|
||||
.centeredForm
|
||||
background-color: $main-background-color
|
||||
border-radius: 5px
|
||||
|
||||
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
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
display: flex
|
||||
align-items: center
|
||||
background-color: $header-background-color
|
||||
justify-content: space-between
|
||||
|
||||
.TrafficPageStreamStatus
|
||||
display: flex
|
||||
align-items: center
|
||||
|
||||
.TrafficPage-Header
|
||||
display: flex
|
||||
height: 2.5%
|
||||
|
||||
@@ -28,6 +28,21 @@ export default class Api {
|
||||
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 () => {
|
||||
const response = await this.client.get("/status/tap");
|
||||
return response.data;
|
||||
@@ -61,6 +76,16 @@ export default class Api {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
getOasServices = async () => {
|
||||
const response = await this.client.get("/oas");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
getOasByService = async (selectedService) => {
|
||||
const response = await this.client.get(`/oas/${selectedService}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
validateQuery = async (query) => {
|
||||
if (this.source) {
|
||||
this.source.cancel();
|
||||
|
||||
Reference in New Issue
Block a user