mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-02-20 04:50:20 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52ce6044ea | ||
|
|
3a83531590 | ||
|
|
e358aa4c8f | ||
|
|
03b1313a9f | ||
|
|
32dfe40e18 | ||
|
|
12aaa762f6 | ||
|
|
a75bac181d | ||
|
|
2d78785558 | ||
|
|
cba0c682e5 | ||
|
|
791f762803 | ||
|
|
d316589bda | ||
|
|
36828bcc1d | ||
|
|
23332639d0 | ||
|
|
3b69508581 | ||
|
|
397d3931ad | ||
|
|
4de795e463 |
12
.github/workflows/acceptance_tests.yml
vendored
12
.github/workflows/acceptance_tests.yml
vendored
@@ -30,3 +30,15 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: make acceptance-test
|
||||
|
||||
- name: Slack notification on failure
|
||||
uses: ravsamhq/notify-slack-action@v1
|
||||
if: always()
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
notification_title: 'Mizu {workflow} has {status_message}'
|
||||
message_format: '{emoji} *{workflow}* {status_message} during <{run_url}|run>, after commit: <{commit_url}|{commit_sha}>'
|
||||
footer: 'Linked Repo <{repo_url}|{repo}>'
|
||||
notify_when: 'failure'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
25
.github/workflows/security_validation.yml
vendored
25
.github/workflows/security_validation.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Security validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
security:
|
||||
name: Check for vulnerabilities
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: snyk/actions/setup@master
|
||||
- name: Set up Go 1.16
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.16'
|
||||
|
||||
- name: Run snyl on all projects
|
||||
run: snyk test --all-projects
|
||||
@@ -304,11 +304,10 @@ func cleanupCommand(cmd *exec.Cmd) error {
|
||||
}
|
||||
|
||||
func getPods(tapStatusInterface interface{}) ([]map[string]interface{}, error) {
|
||||
tapStatus := tapStatusInterface.(map[string]interface{})
|
||||
podsInterface := tapStatus["pods"].([]interface{})
|
||||
tapPodsInterface := tapStatusInterface.([]interface{})
|
||||
|
||||
var pods []map[string]interface{}
|
||||
for _, podInterface := range podsInterface {
|
||||
for _, podInterface := range tapPodsInterface {
|
||||
pods = append(pods, podInterface.(map[string]interface{}))
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ require (
|
||||
github.com/djherbis/atime v1.0.0
|
||||
github.com/getkin/kin-openapi v0.76.0
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.7.2
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/go-playground/locales v0.13.0
|
||||
github.com/go-playground/universal-translator v0.17.0
|
||||
github.com/go-playground/validator/v10 v10.5.0
|
||||
|
||||
@@ -125,6 +125,8 @@ github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTI
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
||||
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
|
||||
@@ -465,14 +465,15 @@ func startMizuTapperSyncer(ctx context.Context, provider *kubernetes.Provider) (
|
||||
logger.Log.Debug("mizuTapperSyncer pod changes channel closed, ending listener loop")
|
||||
return
|
||||
}
|
||||
tapStatus := shared.TapStatus{Pods: kubernetes.GetPodInfosForPods(tapperSyncer.CurrentlyTappedPods)}
|
||||
providers.TapStatus = shared.TapStatus{Pods: kubernetes.GetPodInfosForPods(tapperSyncer.CurrentlyTappedPods)}
|
||||
|
||||
serializedTapStatus, err := json.Marshal(shared.CreateWebSocketStatusMessage(tapStatus))
|
||||
tappedPodsStatus := utils.GetTappedPodsStatus()
|
||||
|
||||
serializedTapStatus, err := json.Marshal(shared.CreateWebSocketStatusMessage(tappedPodsStatus))
|
||||
if err != nil {
|
||||
logger.Log.Fatalf("error serializing tap status: %v", err)
|
||||
}
|
||||
api.BroadcastToBrowserClients(serializedTapStatus)
|
||||
providers.TapStatus.Pods = tapStatus.Pods
|
||||
providers.ExpectedTapperAmount = tapPodChangeEvent.ExpectedTapperAmount
|
||||
case tapperStatus, ok := <-tapperSyncer.TapperStatusChangedOut:
|
||||
if !ok {
|
||||
|
||||
@@ -83,7 +83,6 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
|
||||
if err != nil {
|
||||
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
|
||||
} else {
|
||||
providers.TapStatus.Pods = statusMessage.TappingStatus.Pods
|
||||
BroadcastToBrowserClients(message)
|
||||
}
|
||||
case shared.WebsocketMessageTypeOutboundLink:
|
||||
|
||||
@@ -3,17 +3,17 @@ package controllers
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
"mizuserver/pkg/api"
|
||||
"mizuserver/pkg/config"
|
||||
"mizuserver/pkg/holder"
|
||||
"mizuserver/pkg/providers"
|
||||
"mizuserver/pkg/up9"
|
||||
"mizuserver/pkg/utils"
|
||||
"mizuserver/pkg/validation"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
)
|
||||
|
||||
func HealthCheck(c *gin.Context) {
|
||||
@@ -24,7 +24,7 @@ func HealthCheck(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
tappers := make([]shared.TapperStatus, len(providers.TappersStatus))
|
||||
tappers := make([]shared.TapperStatus, 0)
|
||||
for _, value := range providers.TappersStatus {
|
||||
tappers = append(tappers, value)
|
||||
}
|
||||
@@ -49,7 +49,13 @@ func PostTappedPods(c *gin.Context) {
|
||||
}
|
||||
logger.Log.Infof("[Status] POST request: %d tapped pods", len(tapStatus.Pods))
|
||||
providers.TapStatus.Pods = tapStatus.Pods
|
||||
message := shared.CreateWebSocketStatusMessage(*tapStatus)
|
||||
broadcastTappedPodsStatus()
|
||||
}
|
||||
|
||||
func broadcastTappedPodsStatus() {
|
||||
tappedPodsStatus := utils.GetTappedPodsStatus()
|
||||
|
||||
message := shared.CreateWebSocketStatusMessage(tappedPodsStatus)
|
||||
if jsonBytes, err := json.Marshal(message); err != nil {
|
||||
logger.Log.Errorf("Could not Marshal message %v", err)
|
||||
} else {
|
||||
@@ -72,6 +78,7 @@ func PostTapperStatus(c *gin.Context) {
|
||||
providers.TappersStatus = make(map[string]shared.TapperStatus)
|
||||
}
|
||||
providers.TappersStatus[tapperStatus.NodeName] = *tapperStatus
|
||||
broadcastTappedPodsStatus()
|
||||
}
|
||||
|
||||
func GetTappersCount(c *gin.Context) {
|
||||
@@ -89,7 +96,8 @@ func GetAuthStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
func GetTappingStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, providers.TapStatus)
|
||||
tappedPodsStatus := utils.GetTappedPodsStatus()
|
||||
c.JSON(http.StatusOK, tappedPodsStatus)
|
||||
}
|
||||
|
||||
func AnalyzeInformation(c *gin.Context) {
|
||||
|
||||
@@ -3,11 +3,12 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mizuserver/pkg/providers"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -44,15 +45,13 @@ func StartServer(app *gin.Engine) {
|
||||
}
|
||||
}
|
||||
|
||||
func ReverseSlice(data interface{}) {
|
||||
value := reflect.ValueOf(data)
|
||||
valueLen := value.Len()
|
||||
for i := 0; i <= int((valueLen-1)/2); i++ {
|
||||
reverseIndex := valueLen - 1 - i
|
||||
tmp := value.Index(reverseIndex).Interface()
|
||||
value.Index(reverseIndex).Set(value.Index(i))
|
||||
value.Index(i).Set(reflect.ValueOf(tmp))
|
||||
func GetTappedPodsStatus() []shared.TappedPodStatus {
|
||||
tappedPodsStatus := make([]shared.TappedPodStatus, 0)
|
||||
for _, pod := range providers.TapStatus.Pods {
|
||||
isTapped := strings.ToLower(providers.TappersStatus[pod.NodeName].Status) == "started"
|
||||
tappedPodsStatus = append(tappedPodsStatus, shared.TappedPodStatus{Name: pod.Name, Namespace: pod.Namespace, IsTapped: isTapped})
|
||||
}
|
||||
return tappedPodsStatus
|
||||
}
|
||||
|
||||
func CheckErr(e error) {
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"gopkg.in/yaml.v3"
|
||||
core "k8s.io/api/core/v1"
|
||||
@@ -151,6 +152,7 @@ func RunMizuTap() {
|
||||
} else {
|
||||
defer finishMizuExecution(kubernetesProvider, apiProvider)
|
||||
|
||||
go goUtils.HandleExcWrapper(watchApiServerEvents, ctx, kubernetesProvider, cancel)
|
||||
go goUtils.HandleExcWrapper(watchApiServerPod, ctx, kubernetesProvider, cancel)
|
||||
|
||||
// block until exit signal or error
|
||||
@@ -159,10 +161,6 @@ func RunMizuTap() {
|
||||
}
|
||||
|
||||
func handleDaemonModePostCreation(ctx context.Context, cancel context.CancelFunc, kubernetesProvider *kubernetes.Provider, namespaces []string) error {
|
||||
if err := printTappedPodsPreview(ctx, kubernetesProvider, namespaces); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiProvider := apiserver.NewProvider(GetApiServerUrl(), 90, 1*time.Second)
|
||||
|
||||
if err := waitForDaemonModeToBeReady(cancel, kubernetesProvider, apiProvider); err != nil {
|
||||
@@ -192,7 +190,6 @@ func printTappedPodsPreview(ctx context.Context, kubernetesProvider *kubernetes.
|
||||
}
|
||||
|
||||
func waitForDaemonModeToBeReady(cancel context.CancelFunc, kubernetesProvider *kubernetes.Provider, apiProvider *apiserver.Provider) error {
|
||||
logger.Log.Info("Waiting for mizu to be ready... (may take a few minutes)")
|
||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
|
||||
// TODO: TRA-3903 add a smarter test to see that tapping/pod watching is functioning properly
|
||||
@@ -564,6 +561,67 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
|
||||
}
|
||||
|
||||
func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", kubernetes.ApiServerPodName))
|
||||
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
||||
isPodReady := false
|
||||
timeAfter := time.After(25 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case wEvent, ok := <-eventChan:
|
||||
if !ok {
|
||||
eventChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
switch wEvent.Type {
|
||||
case kubernetes.EventAdded:
|
||||
logger.Log.Debugf("Watching API Server pod loop, added")
|
||||
case kubernetes.EventDeleted:
|
||||
logger.Log.Infof("%s removed", kubernetes.ApiServerPodName)
|
||||
cancel()
|
||||
return
|
||||
case kubernetes.EventModified:
|
||||
modifiedPod, err := wEvent.ToPod()
|
||||
if err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, err)
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase)
|
||||
|
||||
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
|
||||
isPodReady = true
|
||||
postApiServerStarted(ctx, kubernetesProvider, cancel, err)
|
||||
}
|
||||
case kubernetes.EventBookmark:
|
||||
break
|
||||
case kubernetes.EventError:
|
||||
break
|
||||
}
|
||||
case err, ok := <-errorChan:
|
||||
if !ok {
|
||||
errorChan = nil
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Log.Errorf("[ERROR] Agent creation, watching %v namespace, error: %v", config.Config.MizuResourcesNamespace, err)
|
||||
cancel()
|
||||
|
||||
case <-timeAfter:
|
||||
if !isPodReady {
|
||||
logger.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time")
|
||||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
logger.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func watchApiServerEvents(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s", kubernetes.ApiServerPodName))
|
||||
eventWatchHelper := kubernetes.NewEventWatchHelper(kubernetesProvider, podExactRegex, "pod")
|
||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, eventWatchHelper, []string{config.Config.MizuResourcesNamespace}, eventWatchHelper)
|
||||
@@ -594,26 +652,7 @@ func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
event.Note))
|
||||
|
||||
switch event.Reason {
|
||||
case "Started":
|
||||
go 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()
|
||||
break
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
logger.Log.Infof("Mizu is available at %s", url)
|
||||
if !config.Config.HeadlessMode {
|
||||
uiUtils.OpenBrowser(url)
|
||||
}
|
||||
case "FailedScheduling", "Failed", "Killing":
|
||||
case "FailedScheduling", "Failed":
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Mizu API Server status: %s - %s", event.Reason, event.Note))
|
||||
cancel()
|
||||
break
|
||||
@@ -632,6 +671,27 @@ func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
}
|
||||
}
|
||||
|
||||
func postApiServerStarted(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, err error) {
|
||||
go 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()
|
||||
}
|
||||
|
||||
logger.Log.Infof("Mizu is available at %s", url)
|
||||
if !config.Config.HeadlessMode {
|
||||
uiUtils.OpenBrowser(url)
|
||||
}
|
||||
}
|
||||
|
||||
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
|
||||
if config.Config.Tap.AllNamespaces {
|
||||
return []string{kubernetes.K8sAllNamespaces}
|
||||
|
||||
@@ -64,7 +64,7 @@ type AnalyzeStatus struct {
|
||||
|
||||
type WebSocketStatusMessage struct {
|
||||
*WebSocketMessageMetadata
|
||||
TappingStatus TapStatus `json:"tappingStatus"`
|
||||
TappingStatus []TappedPodStatus `json:"tappingStatus"`
|
||||
}
|
||||
|
||||
type TapperStatus struct {
|
||||
@@ -73,9 +73,14 @@ type TapperStatus struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type TappedPodStatus struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
IsTapped bool `json:"isTapped"`
|
||||
}
|
||||
|
||||
type TapStatus struct {
|
||||
Pods []PodInfo `json:"pods"`
|
||||
TLSLinks []TLSLinkInfo `json:"tlsLinks"`
|
||||
Pods []PodInfo `json:"pods"`
|
||||
}
|
||||
|
||||
type PodInfo struct {
|
||||
@@ -98,12 +103,12 @@ type SyncEntriesConfig struct {
|
||||
UploadIntervalSec int `json:"interval"`
|
||||
}
|
||||
|
||||
func CreateWebSocketStatusMessage(tappingStatus TapStatus) WebSocketStatusMessage {
|
||||
func CreateWebSocketStatusMessage(tappedPodsStatus []TappedPodStatus) WebSocketStatusMessage {
|
||||
return WebSocketStatusMessage{
|
||||
WebSocketMessageMetadata: &WebSocketMessageMetadata{
|
||||
MessageType: WebSocketMessageTypeUpdateStatus,
|
||||
},
|
||||
TappingStatus: tappingStatus,
|
||||
TappingStatus: tappedPodsStatus,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -579,12 +579,18 @@ func representConnectionStart(event map[string]interface{}) []interface{} {
|
||||
}
|
||||
|
||||
func representConnectionClose(event map[string]interface{}) []interface{} {
|
||||
replyCode := ""
|
||||
|
||||
if event["replyCode"] != nil {
|
||||
replyCode = fmt.Sprintf("%g", event["replyCode"].(float64))
|
||||
}
|
||||
|
||||
rep := make([]interface{}, 0)
|
||||
|
||||
details, _ := json.Marshal([]api.TableData{
|
||||
{
|
||||
Name: "Reply Code",
|
||||
Value: fmt.Sprintf("%g", event["replyCode"].(float64)),
|
||||
Value: replyCode,
|
||||
Selector: `request.replyCode`,
|
||||
},
|
||||
{
|
||||
|
||||
38
tap/source/discoverer_util.go
Normal file
38
tap/source/discoverer_util.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
)
|
||||
|
||||
var numberRegex = regexp.MustCompile("[0-9]+")
|
||||
|
||||
func getSingleValueFromEnvironmentVariableFile(filePath string, variableName string) (string, error) {
|
||||
bytes, err := ioutil.ReadFile(filePath)
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Warningf("Error reading environment file %v - %v", filePath, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
envs := strings.Split(string(bytes), string([]byte{0}))
|
||||
|
||||
for _, env := range envs {
|
||||
if !strings.Contains(env, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(env, "=")
|
||||
varName := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
if variableName == varName {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
@@ -13,8 +12,6 @@ import (
|
||||
|
||||
const envoyBinary = "/envoy"
|
||||
|
||||
var numberRegex = regexp.MustCompile("[0-9]+")
|
||||
|
||||
func discoverRelevantEnvoyPids(procfs string, pods []v1.Pod) ([]string, error) {
|
||||
result := make([]string, 0)
|
||||
|
||||
@@ -36,7 +33,7 @@ func discoverRelevantEnvoyPids(procfs string, pods []v1.Pod) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if checkPid(procfs, pid.Name(), pods) {
|
||||
if checkEnvoyPid(procfs, pid.Name(), pods) {
|
||||
result = append(result, pid.Name())
|
||||
}
|
||||
}
|
||||
@@ -46,7 +43,7 @@ func discoverRelevantEnvoyPids(procfs string, pods []v1.Pod) ([]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func checkPid(procfs string, pid string, pods []v1.Pod) bool {
|
||||
func checkEnvoyPid(procfs string, pid string, pods []v1.Pod) bool {
|
||||
execLink := fmt.Sprintf("%v/%v/exe", procfs, pid)
|
||||
exec, err := os.Readlink(execLink)
|
||||
|
||||
@@ -63,7 +60,7 @@ func checkPid(procfs string, pid string, pods []v1.Pod) bool {
|
||||
}
|
||||
|
||||
environmentFile := fmt.Sprintf("%v/%v/environ", procfs, pid)
|
||||
podIp, err := readEnvironmentVariable(environmentFile, "INSTANCE_IP")
|
||||
podIp, err := getSingleValueFromEnvironmentVariableFile(environmentFile, "INSTANCE_IP")
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -84,30 +81,3 @@ func checkPid(procfs string, pid string, pods []v1.Pod) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func readEnvironmentVariable(file string, name string) (string, error) {
|
||||
bytes, err := ioutil.ReadFile(file)
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Warningf("Error reading environment file %v - %v", file, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
envs := strings.Split(string(bytes), string([]byte{0}))
|
||||
|
||||
for _, env := range envs {
|
||||
if !strings.Contains(env, "=") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(env, "=")
|
||||
varName := parts[0]
|
||||
value := parts[1]
|
||||
|
||||
if name == varName {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
83
tap/source/linkerd_discoverer.go
Normal file
83
tap/source/linkerd_discoverer.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const linkerdBinary = "/linkerd2-proxy"
|
||||
|
||||
func discoverRelevantLinkerdPids(procfs string, pods []v1.Pod) ([]string, error) {
|
||||
result := make([]string, 0)
|
||||
|
||||
pids, err := ioutil.ReadDir(procfs)
|
||||
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
logger.Log.Infof("Starting linkerd auto discoverer %v %v - scanning %v potential pids",
|
||||
procfs, pods, len(pids))
|
||||
|
||||
for _, pid := range pids {
|
||||
if !pid.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !numberRegex.MatchString(pid.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if checkLinkerdPid(procfs, pid.Name(), pods) {
|
||||
result = append(result, pid.Name())
|
||||
}
|
||||
}
|
||||
|
||||
logger.Log.Infof("Found %v relevant linkerd processes - %v", len(result), result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func checkLinkerdPid(procfs string, pid string, pods []v1.Pod) bool {
|
||||
execLink := fmt.Sprintf("%v/%v/exe", procfs, pid)
|
||||
exec, err := os.Readlink(execLink)
|
||||
|
||||
if err != nil {
|
||||
// Debug on purpose - it may happen due to many reasons and we only care
|
||||
// for it during troubleshooting
|
||||
//
|
||||
logger.Log.Debugf("Unable to read link %v - %v\n", execLink, err)
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(exec, linkerdBinary) {
|
||||
return false
|
||||
}
|
||||
|
||||
environmentFile := fmt.Sprintf("%v/%v/environ", procfs, pid)
|
||||
podName, err := getSingleValueFromEnvironmentVariableFile(environmentFile, "_pod_name")
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if podName == "" {
|
||||
logger.Log.Debugf("Found a linkerd process without _pod_name variable %v\n", pid)
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Log.Infof("Found linkerd pid %v with pod name %v", pid, podName)
|
||||
|
||||
for _, pod := range pods {
|
||||
if pod.Name == podName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type PacketSourceManager struct {
|
||||
}
|
||||
|
||||
func NewPacketSourceManager(procfs string, pids string, filename string, interfaceName string,
|
||||
istio bool, pods []v1.Pod, behaviour TcpPacketSourceBehaviour) (*PacketSourceManager, error) {
|
||||
mtls bool, pods []v1.Pod, behaviour TcpPacketSourceBehaviour) (*PacketSourceManager, error) {
|
||||
sources := make([]*tcpPacketSource, 0)
|
||||
sources, err := createHostSource(sources, filename, interfaceName, behaviour)
|
||||
|
||||
@@ -25,7 +25,8 @@ func NewPacketSourceManager(procfs string, pids string, filename string, interfa
|
||||
}
|
||||
|
||||
sources = createSourcesFromPids(sources, procfs, pids, interfaceName, behaviour)
|
||||
sources = createSourcesFromEnvoy(sources, istio, procfs, pods, interfaceName, behaviour)
|
||||
sources = createSourcesFromEnvoy(sources, mtls, procfs, pods, interfaceName, behaviour)
|
||||
sources = createSourcesFromLinkerd(sources, mtls, procfs, pods, interfaceName, behaviour)
|
||||
|
||||
return &PacketSourceManager{
|
||||
sources: sources,
|
||||
@@ -54,13 +55,13 @@ func createSourcesFromPids(sources []*tcpPacketSource, procfs string, pids strin
|
||||
return sources
|
||||
}
|
||||
|
||||
func createSourcesFromEnvoy(sources []*tcpPacketSource, istio bool, procfs string, clusterIps []v1.Pod,
|
||||
func createSourcesFromEnvoy(sources []*tcpPacketSource, mtls bool, procfs string, pods []v1.Pod,
|
||||
interfaceName string, behaviour TcpPacketSourceBehaviour) []*tcpPacketSource {
|
||||
if !istio {
|
||||
if !mtls {
|
||||
return sources
|
||||
}
|
||||
|
||||
envoyPids, err := discoverRelevantEnvoyPids(procfs, clusterIps)
|
||||
envoyPids, err := discoverRelevantEnvoyPids(procfs, pods)
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Warningf("Unable to discover envoy pids - %v", err)
|
||||
@@ -73,6 +74,25 @@ func createSourcesFromEnvoy(sources []*tcpPacketSource, istio bool, procfs strin
|
||||
return sources
|
||||
}
|
||||
|
||||
func createSourcesFromLinkerd(sources []*tcpPacketSource, mtls bool, procfs string, pods []v1.Pod,
|
||||
interfaceName string, behaviour TcpPacketSourceBehaviour) []*tcpPacketSource {
|
||||
if !mtls {
|
||||
return sources
|
||||
}
|
||||
|
||||
linkerdPids, err := discoverRelevantLinkerdPids(procfs, pods)
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Warningf("Unable to discover linkerd pids - %v", err)
|
||||
return sources
|
||||
}
|
||||
|
||||
netnsSources := newNetnsPacketSources(procfs, linkerdPids, interfaceName, behaviour)
|
||||
sources = append(sources, netnsSources...)
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
func newHostPacketSource(filename string, interfaceName string,
|
||||
behaviour TcpPacketSourceBehaviour) (*tcpPacketSource, error) {
|
||||
var name string
|
||||
|
||||
@@ -65,11 +65,11 @@ func (a *tcpAssembler) processPackets(dumpPacket bool, packets <-chan source.Tcp
|
||||
|
||||
for packetInfo := range packets {
|
||||
packetsCount := diagnose.AppStats.IncPacketsCount()
|
||||
|
||||
if packetsCount % PACKETS_SEEN_LOG_THRESHOLD == 0 {
|
||||
|
||||
if packetsCount%PACKETS_SEEN_LOG_THRESHOLD == 0 {
|
||||
logger.Log.Debugf("Packets seen: #%d", packetsCount)
|
||||
}
|
||||
|
||||
|
||||
packet := packetInfo.Packet
|
||||
data := packet.Data()
|
||||
diagnose.AppStats.UpdateProcessedBytes(uint64(len(data)))
|
||||
@@ -91,7 +91,6 @@ func (a *tcpAssembler) processPackets(dumpPacket bool, packets <-chan source.Tcp
|
||||
CaptureInfo: packet.Metadata().CaptureInfo,
|
||||
}
|
||||
diagnose.InternalStats.Totalsz += len(tcp.Payload)
|
||||
logger.Log.Debugf("%s:%v -> %s:%v", packet.NetworkLayer().NetworkFlow().Src(), tcp.SrcPort, packet.NetworkLayer().NetworkFlow().Dst(), tcp.DstPort)
|
||||
a.assemblerMutex.Lock()
|
||||
a.AssembleWithContext(packet.NetworkLayer().NetworkFlow(), tcp, &c)
|
||||
a.assemblerMutex.Unlock()
|
||||
|
||||
@@ -2,7 +2,6 @@ package tap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
@@ -75,7 +74,7 @@ func (t *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassem
|
||||
}
|
||||
|
||||
func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) {
|
||||
dir, start, end, skip := sg.Info()
|
||||
dir, _, _, skip := sg.Info()
|
||||
length, saved := sg.Lengths()
|
||||
// update stats
|
||||
sgStats := sg.Stats()
|
||||
@@ -103,13 +102,6 @@ func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
|
||||
diagnose.InternalStats.OverlapBytes += sgStats.OverlapBytes
|
||||
diagnose.InternalStats.OverlapPackets += sgStats.OverlapPackets
|
||||
|
||||
var ident string
|
||||
if dir == reassembly.TCPDirClientToServer {
|
||||
ident = fmt.Sprintf("%v %v(%s): ", t.net, t.transport, dir)
|
||||
} else {
|
||||
ident = fmt.Sprintf("%v %v(%s): ", t.net.Reverse(), t.transport.Reverse(), dir)
|
||||
}
|
||||
diagnose.TapErrors.Debug("%s: SG reassembled packet with %d bytes (start:%v,end:%v,skip:%d,saved:%d,nb:%d,%d,overlap:%d,%d)", ident, length, start, end, skip, saved, sgStats.Packets, sgStats.Chunks, sgStats.OverlapBytes, sgStats.OverlapPackets)
|
||||
if skip == -1 && *allowmissinginit {
|
||||
// this is allowed
|
||||
} else if skip != 0 {
|
||||
@@ -174,7 +166,6 @@ func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
|
||||
}
|
||||
|
||||
func (t *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool {
|
||||
diagnose.TapErrors.Debug("%s: Connection closed", t.ident)
|
||||
if t.isTapTarget && !t.isClosed {
|
||||
t.Close()
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ func NewTcpStreamFactory(emitter api.Emitter, streamsMap *tcpStreamMap, opts *Ta
|
||||
}
|
||||
|
||||
func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
|
||||
logger.Log.Debugf("* NEW: %s %s", net, transport)
|
||||
fsmOptions := reassembly.TCPSimpleFSMOptions{
|
||||
SupportMissingEstablishment: *allowmissinginit,
|
||||
}
|
||||
@@ -153,21 +152,16 @@ func inArrayPod(pods []v1.Pod, address string) bool {
|
||||
func (factory *tcpStreamFactory) getStreamProps(srcIP string, srcPort string, dstIP string, dstPort string) *streamProps {
|
||||
if factory.opts.HostMode {
|
||||
if inArrayPod(factory.opts.FilterAuthorities, fmt.Sprintf("%s:%s", dstIP, dstPort)) {
|
||||
logger.Log.Debugf("getStreamProps %s", fmt.Sprintf("+ host1 %s:%s", dstIP, dstPort))
|
||||
return &streamProps{isTapTarget: true, isOutgoing: false}
|
||||
} else if inArrayPod(factory.opts.FilterAuthorities, dstIP) {
|
||||
logger.Log.Debugf("getStreamProps %s", fmt.Sprintf("+ host2 %s", dstIP))
|
||||
return &streamProps{isTapTarget: true, isOutgoing: false}
|
||||
} else if inArrayPod(factory.opts.FilterAuthorities, fmt.Sprintf("%s:%s", srcIP, srcPort)) {
|
||||
logger.Log.Debugf("getStreamProps %s", fmt.Sprintf("+ host3 %s:%s", srcIP, srcPort))
|
||||
return &streamProps{isTapTarget: true, isOutgoing: true}
|
||||
} else if inArrayPod(factory.opts.FilterAuthorities, srcIP) {
|
||||
logger.Log.Debugf("getStreamProps %s", fmt.Sprintf("+ host4 %s", srcIP))
|
||||
return &streamProps{isTapTarget: true, isOutgoing: true}
|
||||
}
|
||||
return &streamProps{isTapTarget: false, isOutgoing: false}
|
||||
} else {
|
||||
logger.Log.Debugf("getStreamProps %s", fmt.Sprintf("+ notHost3 %s:%s -> %s:%s", srcIP, srcPort, dstIP, dstPort))
|
||||
return &streamProps{isTapTarget: true}
|
||||
}
|
||||
}
|
||||
|
||||
66
ui/package-lock.json
generated
66
ui/package-lock.json
generated
@@ -7747,9 +7747,9 @@
|
||||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "10.7.2",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz",
|
||||
"integrity": "sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg=="
|
||||
"version": "11.3.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.3.1.tgz",
|
||||
"integrity": "sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw=="
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
@@ -10234,6 +10234,11 @@
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
||||
},
|
||||
"json-beautify": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-beautify/-/json-beautify-1.1.1.tgz",
|
||||
"integrity": "sha512-17j+Hk2lado0xqKtUcyAjK0AtoHnPSIgktWRsEXgdFQFG9UnaGw6CHa0J7xsvulxRpFl6CrkDFHght1p5ZJc4A=="
|
||||
},
|
||||
"json-parse-better-errors": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
|
||||
@@ -10612,6 +10617,13 @@
|
||||
"requires": {
|
||||
"fault": "^1.0.0",
|
||||
"highlight.js": "~10.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"lru-cache": {
|
||||
@@ -13577,6 +13589,34 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-lowlight": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-lowlight/-/react-lowlight-3.0.0.tgz",
|
||||
"integrity": "sha512-s0+T81PsCbUZYd/0XrplGc6kQEUdiwLKI0G6umJP1ViqRoZRCvSuHvXOy20Usd2ywDKWLuVETQgBDPeNQhPNZg==",
|
||||
"requires": {
|
||||
"lowlight": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"fault": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz",
|
||||
"integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==",
|
||||
"requires": {
|
||||
"format": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"lowlight": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-2.4.1.tgz",
|
||||
"integrity": "sha512-mQkAG0zGQ9lcYecEft+hl9uV1fD6HpURA83/TYrsxKvb8xX2mfyB+aaV/A/aWmhhEcWVzr9Cc+l/fvUYfEUumw==",
|
||||
"requires": {
|
||||
"@types/hast": "^2.0.0",
|
||||
"fault": "^2.0.0",
|
||||
"highlight.js": "~11.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-refresh": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
||||
@@ -13663,6 +13703,13 @@
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.22.0",
|
||||
"refractor": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
@@ -18149,11 +18196,24 @@
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
|
||||
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
|
||||
},
|
||||
"xml-formatter": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-2.6.0.tgz",
|
||||
"integrity": "sha512-+bQeoiE5W3CJdDCHTlveYSWFfQWnYB3uHGeRJ6LlEsL5kT++mWy9iN1cMeEDfBbgOnXO2DNUbmQ6elkR/mCcjg==",
|
||||
"requires": {
|
||||
"xml-parser-xo": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"xml-name-validator": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
|
||||
},
|
||||
"xml-parser-xo": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz",
|
||||
"integrity": "sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg=="
|
||||
},
|
||||
"xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@uiw/react-textarea-code-editor": "^1.4.12",
|
||||
"axios": "^0.21.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"json-beautify": "^1.1.1",
|
||||
"jsonpath": "^1.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"node-sass": "^5.0.0",
|
||||
@@ -23,12 +25,14 @@
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"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",
|
||||
"typescript": "^4.2.4",
|
||||
"web-vitals": "^1.1.1"
|
||||
"web-vitals": "^1.1.1",
|
||||
"xml-formatter": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -66,7 +66,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
|
||||
}
|
||||
setIsLoadingTop(true);
|
||||
const data = await api.fetchEntries(leftOffTop, -1, query, 100, 3000);
|
||||
if (!data || !data.meta) {
|
||||
if (!data || data.data === null || data.meta === null) {
|
||||
setNoMoreDataTop(true);
|
||||
setIsLoadingTop(false);
|
||||
return;
|
||||
|
||||
@@ -6,16 +6,20 @@ import FancyTextDisplay from "../UI/FancyTextDisplay";
|
||||
import Queryable from "../UI/Queryable";
|
||||
import Checkbox from "../UI/Checkbox";
|
||||
import ProtobufDecoder from "protobuf-decoder";
|
||||
import {default as jsonBeautify} from "json-beautify";
|
||||
import {default as xmlBeautify} from "xml-formatter";
|
||||
|
||||
interface EntryViewLineProps {
|
||||
label: string;
|
||||
value: number | string;
|
||||
updateQuery: any;
|
||||
selector: string;
|
||||
updateQuery?: any;
|
||||
selector?: string;
|
||||
overrideQueryValue?: string;
|
||||
displayIconOnMouseOver?: boolean;
|
||||
useTooltip?: boolean;
|
||||
}
|
||||
|
||||
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery, selector, overrideQueryValue}) => {
|
||||
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery = null, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
|
||||
let query: string;
|
||||
if (!selector) {
|
||||
query = "";
|
||||
@@ -34,7 +38,8 @@ const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery,
|
||||
style={{float: "right", height: "18px"}}
|
||||
iconStyle={{marginRight: "20px"}}
|
||||
flipped={true}
|
||||
displayIconOnMouseOver={true}
|
||||
useTooltip={useTooltip}
|
||||
displayIconOnMouseOver={displayIconOnMouseOver}
|
||||
>
|
||||
{label}
|
||||
</Queryable>
|
||||
@@ -55,30 +60,47 @@ const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery,
|
||||
interface EntrySectionCollapsibleTitleProps {
|
||||
title: string,
|
||||
color: string,
|
||||
isExpanded: boolean,
|
||||
expanded: boolean,
|
||||
setExpanded: any,
|
||||
query?: string,
|
||||
updateQuery?: any,
|
||||
}
|
||||
|
||||
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, isExpanded}) => {
|
||||
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = "", updateQuery = null}) => {
|
||||
return <div className={styles.title}>
|
||||
<div className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
|
||||
{isExpanded ? '-' : '+'}
|
||||
<div
|
||||
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
|
||||
style={{backgroundColor: color}}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded)
|
||||
}}
|
||||
>
|
||||
{expanded ? '-' : '+'}
|
||||
</div>
|
||||
<span>{title}</span>
|
||||
<Queryable
|
||||
query={query}
|
||||
updateQuery={updateQuery}
|
||||
useTooltip={updateQuery ? true : false}
|
||||
displayIconOnMouseOver={updateQuery ? true : false}
|
||||
>
|
||||
<span>{title}</span>
|
||||
</Queryable>
|
||||
</div>
|
||||
}
|
||||
|
||||
interface EntrySectionContainerProps {
|
||||
title: string,
|
||||
color: string,
|
||||
query?: string,
|
||||
updateQuery?: any,
|
||||
}
|
||||
|
||||
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children}) => {
|
||||
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = "", updateQuery = null}) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
return <CollapsibleContainer
|
||||
className={styles.collapsibleContainer}
|
||||
isExpanded={expanded}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title={<EntrySectionCollapsibleTitle title={title} color={color} isExpanded={expanded}/>}
|
||||
expanded={expanded}
|
||||
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query} updateQuery={updateQuery}/>}
|
||||
>
|
||||
{children}
|
||||
</CollapsibleContainer>
|
||||
@@ -101,23 +123,41 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||
contentType,
|
||||
selector,
|
||||
}) => {
|
||||
const MAXIMUM_BYTES_TO_HIGHLIGHT = 10000; // The maximum of chars to highlight in body, in case the response can be megabytes
|
||||
const supportedLanguages = [['html', 'html'], ['json', 'json'], ['application/grpc', 'json']]; // [[indicator, languageToUse],...]
|
||||
const jsonLikeFormats = ['json'];
|
||||
const MAXIMUM_BYTES_TO_FORMAT = 1000000; // The maximum of chars to highlight in body, in case the response can be megabytes
|
||||
const jsonLikeFormats = ['json', 'yaml', 'yml'];
|
||||
const xmlLikeFormats = ['xml', 'html'];
|
||||
const protobufFormats = ['application/grpc'];
|
||||
const [isWrapped, setIsWrapped] = useState(false);
|
||||
const supportedFormats = jsonLikeFormats.concat(xmlLikeFormats, protobufFormats);
|
||||
|
||||
const formatTextBody = (body): string => {
|
||||
const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT);
|
||||
const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk;
|
||||
const [isPretty, setIsPretty] = useState(true);
|
||||
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
||||
const [decodeBase64, setDecodeBase64] = useState(true);
|
||||
|
||||
const isBase64Encoding = encoding === 'base64';
|
||||
const supportsPrettying = supportedFormats.some(format => contentType?.indexOf(format) > -1);
|
||||
|
||||
const formatTextBody = (body: any): string => {
|
||||
if (!decodeBase64) return body;
|
||||
|
||||
const chunk = body.slice(0, MAXIMUM_BYTES_TO_FORMAT);
|
||||
const bodyBuf = isBase64Encoding ? atob(chunk) : chunk;
|
||||
|
||||
if (!isPretty) return bodyBuf;
|
||||
|
||||
try {
|
||||
if (jsonLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
|
||||
return jsonBeautify(JSON.parse(bodyBuf), null, 2, 80);
|
||||
} else if (xmlLikeFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||
return xmlBeautify(bodyBuf, {
|
||||
indentation: ' ',
|
||||
filter: (node) => node.type !== 'Comment',
|
||||
collapseContent: true,
|
||||
lineSeparator: '\n'
|
||||
});
|
||||
} else if (protobufFormats.some(format => contentType?.indexOf(format) > -1)) {
|
||||
// Replace all non printable characters (ASCII)
|
||||
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
|
||||
return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2);
|
||||
return jsonBeautify(protobufDecoder.decode().toSimple(), null, 2, 80);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -125,33 +165,33 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||
return bodyBuf;
|
||||
}
|
||||
|
||||
const getLanguage = (mimetype) => {
|
||||
const chunk = content?.slice(0, 100);
|
||||
if (chunk.indexOf('html') > 0 || chunk.indexOf('HTML') > 0) return supportedLanguages[0][1];
|
||||
const language = supportedLanguages.find(el => (mimetype + contentType).indexOf(el[0]) > -1);
|
||||
return language ? language[1] : 'default';
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
{content && content?.length > 0 && <EntrySectionContainer title='Body' color={color}>
|
||||
<table>
|
||||
<tbody>
|
||||
<EntryViewLine label={'Mime type'} value={contentType} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>
|
||||
{encoding && <EntryViewLine label={'Encoding'} value={encoding} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>}
|
||||
</tbody>
|
||||
</table>
|
||||
{content && content?.length > 0 && <EntrySectionContainer
|
||||
title='Body'
|
||||
color={color}
|
||||
query={`${selector} == r".*"`}
|
||||
updateQuery={updateQuery}
|
||||
>
|
||||
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
|
||||
{supportsPrettying && <div style={{paddingTop: 3}}>
|
||||
<Checkbox checked={isPretty} onToggle={() => {setIsPretty(!isPretty)}}/>
|
||||
</div>}
|
||||
{supportsPrettying && <span style={{marginLeft: '.2rem'}}>Pretty</span>}
|
||||
|
||||
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
|
||||
<div style={{paddingTop: 3}}>
|
||||
<Checkbox checked={isWrapped} onToggle={() => {}}/>
|
||||
<div style={{paddingTop: 3, paddingLeft: supportsPrettying ? 20 : 0}}>
|
||||
<Checkbox checked={showLineNumbers} onToggle={() => {setShowLineNumbers(!showLineNumbers)}}/>
|
||||
</div>
|
||||
<span style={{marginLeft: '.5rem'}}>Wrap text</span>
|
||||
<span style={{marginLeft: '.2rem'}}>Line numbers</span>
|
||||
|
||||
{isBase64Encoding && <div style={{paddingTop: 3, paddingLeft: 20}}>
|
||||
<Checkbox checked={decodeBase64} onToggle={() => {setDecodeBase64(!decodeBase64)}}/>
|
||||
</div>}
|
||||
{isBase64Encoding && <span style={{marginLeft: '.2rem'}}>Decode Base64</span>}
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
isWrapped={isWrapped}
|
||||
code={formatTextBody(content)}
|
||||
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
</EntrySectionContainer>}
|
||||
</React.Fragment>
|
||||
@@ -195,13 +235,20 @@ interface EntryPolicySectionProps {
|
||||
interface EntryPolicySectionCollapsibleTitleProps {
|
||||
label: string;
|
||||
matched: string;
|
||||
isExpanded: boolean;
|
||||
expanded: boolean;
|
||||
setExpanded: any;
|
||||
}
|
||||
|
||||
const EntryPolicySectionCollapsibleTitle: React.FC<EntryPolicySectionCollapsibleTitleProps> = ({label, matched, isExpanded}) => {
|
||||
const EntryPolicySectionCollapsibleTitle: React.FC<EntryPolicySectionCollapsibleTitleProps> = ({label, matched, expanded, setExpanded}) => {
|
||||
return <div className={styles.title}>
|
||||
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
|
||||
{isExpanded ? '-' : '+'}
|
||||
<span
|
||||
className={`${styles.button}
|
||||
${expanded ? styles.expanded : ''}`}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded)
|
||||
}}
|
||||
>
|
||||
{expanded ? '-' : '+'}
|
||||
</span>
|
||||
<span>
|
||||
<tr className={styles.dataLine}>
|
||||
@@ -222,9 +269,8 @@ export const EntryPolicySectionContainer: React.FC<EntryPolicySectionContainerPr
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return <CollapsibleContainer
|
||||
className={styles.collapsibleContainer}
|
||||
isExpanded={expanded}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title={<EntryPolicySectionCollapsibleTitle label={label} matched={matched} isExpanded={expanded}/>}
|
||||
expanded={expanded}
|
||||
title={<EntryPolicySectionCollapsibleTitle label={label} matched={matched} expanded={expanded} setExpanded={setExpanded}/>}
|
||||
>
|
||||
{children}
|
||||
</CollapsibleContainer>
|
||||
@@ -303,7 +349,6 @@ export const EntryContractSection: React.FC<EntryContractSectionProps> = ({color
|
||||
</EntrySectionContainer>}
|
||||
{contractContent && <EntrySectionContainer title="Contract" color={color}>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
code={contractContent}
|
||||
language={"yaml"}
|
||||
/>
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
flex-direction: column
|
||||
overflow: hidden
|
||||
padding-right: 10px
|
||||
padding-top: 4px
|
||||
flex-grow: 1
|
||||
|
||||
.separatorRight
|
||||
@@ -92,3 +93,13 @@
|
||||
|
||||
.ip
|
||||
margin-left: 5px
|
||||
|
||||
@media (max-width: 1760px)
|
||||
.timestamp
|
||||
display: none
|
||||
.separatorRight
|
||||
border-right: 0px
|
||||
|
||||
@media (max-width: 1340px)
|
||||
.separatorRight
|
||||
display: none
|
||||
|
||||
@@ -167,7 +167,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
This is a simple query that matches to HTTP packets with request path "/catalogue":
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and request.path == "/catalogue"`}
|
||||
language="python"
|
||||
@@ -176,7 +175,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
The same query can be negated for HTTP path and written like this:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and request.path != "/catalogue"`}
|
||||
language="python"
|
||||
@@ -185,7 +183,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
The syntax supports regular expressions. Here is a query that matches the HTTP requests that send JSON to a server:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and request.headers["Accept"] == r"application/json.*"`}
|
||||
language="python"
|
||||
@@ -194,7 +191,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
Here is another query that matches HTTP responses with status code 4xx:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and response.status == r"4.*"`}
|
||||
language="python"
|
||||
@@ -203,7 +199,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
The same exact query can be as integer comparison:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and response.status >= 400`}
|
||||
language="python"
|
||||
@@ -212,7 +207,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
The results can be queried based on their timestamps:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`timestamp < datetime("10/28/2021, 9:13:02.905 PM")`}
|
||||
language="python"
|
||||
@@ -224,7 +218,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
Since Mizu supports various protocols like gRPC, AMQP, Kafka and Redis. It's possible to write complex queries that match multiple protocols like this:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`(http and request.method == "PUT") or (amqp and request.queue.startsWith("test"))\n or (kafka and response.payload.errorCode == 2) or (redis and request.key == "example")\n or (grpc and request.headers[":path"] == r".*foo.*")`}
|
||||
language="python"
|
||||
@@ -242,7 +235,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
Such that; clicking this icon in left-pane, would append the query below:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`and dst.name == "carts.sock-shop"`}
|
||||
language="python"
|
||||
@@ -260,7 +252,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
A query that compares one selector to another is also a valid query:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`http and (request.query["x"] == response.headers["y"]\n or response.content.text.contains(request.query["x"]))`}
|
||||
language="python"
|
||||
@@ -276,7 +267,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
true if the given selector's value starts with the string:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`request.path.startsWith("something")`}
|
||||
language="python"
|
||||
@@ -285,7 +275,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
true if the given selector's value ends with the string:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`request.path.endsWith("something")`}
|
||||
language="python"
|
||||
@@ -294,7 +283,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
true if the given selector's value contains the string:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`request.path.contains("something")`}
|
||||
language="python"
|
||||
@@ -303,7 +291,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
returns the UNIX timestamp which is the equivalent of the time that's provided by the string. Invalid input evaluates to false:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`timestamp >= datetime("10/19/2021, 6:29:02.593 PM")`}
|
||||
language="python"
|
||||
@@ -312,7 +299,6 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
|
||||
limits the number of records that are streamed back as a result of a query. Always evaluates to true:
|
||||
</Typography>
|
||||
<SyntaxHighlighter
|
||||
isWrapped={false}
|
||||
showLineNumbers={false}
|
||||
code={`and limit(100)`}
|
||||
language="python"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import React, {useEffect, useMemo, useRef, useState} from "react";
|
||||
import {Filters} from "./Filters";
|
||||
import {EntriesList} from "./EntriesList";
|
||||
import {makeStyles} from "@material-ui/core";
|
||||
@@ -12,6 +12,7 @@ import {StatusBar} from "./UI/StatusBar";
|
||||
import Api, {MizuWebsocketURL} from "../helpers/api";
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const useLayoutStyles = makeStyles(() => ({
|
||||
details: {
|
||||
@@ -72,23 +73,25 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
|
||||
const [startTime, setStartTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async function() {
|
||||
if (!query) {
|
||||
setQueryBackgroundColor("#f5f5f5")
|
||||
} else {
|
||||
const data = await api.validateQuery(query);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.valid) {
|
||||
setQueryBackgroundColor("#d2fad2");
|
||||
} else {
|
||||
setQueryBackgroundColor("#fad6dc");
|
||||
}
|
||||
const handleQueryChange = useMemo(() => debounce(async (query: string) => {
|
||||
if (!query) {
|
||||
setQueryBackgroundColor("#f5f5f5")
|
||||
} else {
|
||||
const data = await api.validateQuery(query);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
}, [query]);
|
||||
if (data.valid) {
|
||||
setQueryBackgroundColor("#d2fad2");
|
||||
} else {
|
||||
setQueryBackgroundColor("#fad6dc");
|
||||
}
|
||||
}
|
||||
}, 500), []) as (query: string) => void;
|
||||
|
||||
useEffect(() => {
|
||||
handleQueryChange(query);
|
||||
}, [query, handleQueryChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
@@ -320,7 +323,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
|
||||
{selectedEntryData && <EntryDetailed entryData={selectedEntryData} updateQuery={updateQuery}/>}
|
||||
</div>
|
||||
</div>}
|
||||
{tappingStatus?.pods != null && <StatusBar tappingStatus={tappingStatus}/>}
|
||||
{tappingStatus && <StatusBar tappingStatus={tappingStatus}/>}
|
||||
<ToastContainer
|
||||
position="bottom-right"
|
||||
autoClose={5000}
|
||||
|
||||
@@ -1,38 +1,25 @@
|
||||
import React, {useState} from "react";
|
||||
import React from "react";
|
||||
import collapsedImg from "../assets/collapsed.svg";
|
||||
import expandedImg from "../assets/expanded.svg";
|
||||
import "./style/CollapsibleContainer.sass";
|
||||
|
||||
interface Props {
|
||||
title: string | React.ReactNode,
|
||||
onClick?: (e: React.SyntheticEvent) => void,
|
||||
isExpanded?: boolean,
|
||||
expanded: boolean,
|
||||
titleClassName?: string,
|
||||
stickyHeader?: boolean,
|
||||
className?: string,
|
||||
initialExpanded?: boolean;
|
||||
passiveOnClick?: boolean; //whether specifying onClick overrides internal _isExpanded state handling
|
||||
stickyHeader?: boolean,
|
||||
}
|
||||
|
||||
const CollapsibleContainer: React.FC<Props> = ({title, children, isExpanded, onClick, titleClassName, stickyHeader = false, className, initialExpanded = true, passiveOnClick}) => {
|
||||
const [_isExpanded, _setExpanded] = useState(initialExpanded);
|
||||
let expanded = isExpanded !== undefined ? isExpanded : _isExpanded;
|
||||
const CollapsibleContainer: React.FC<Props> = ({title, children, expanded, titleClassName, className, stickyHeader = false}) => {
|
||||
const classNames = `CollapsibleContainer ${expanded ? "CollapsibleContainer-Expanded" : "CollapsibleContainer-Collapsed"} ${className ? className : ''}`;
|
||||
|
||||
// This is needed to achieve the sticky header feature.
|
||||
// This is needed to achieve the sticky header feature.
|
||||
// It is needed an un-contained component for the css to work properly.
|
||||
const content = <React.Fragment>
|
||||
<div
|
||||
className={`CollapsibleContainer-Header ${stickyHeader ? "CollapsibleContainer-Header-Sticky" : ""}
|
||||
className={`CollapsibleContainer-Header ${stickyHeader ? "CollapsibleContainer-Header-Sticky" : ""}
|
||||
${expanded ? "CollapsibleContainer-Header-Expanded" : ""}`}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
onClick(e)
|
||||
if (passiveOnClick !== true)
|
||||
return;
|
||||
}
|
||||
_setExpanded(!_isExpanded)
|
||||
}}
|
||||
>
|
||||
{
|
||||
React.isValidElement(title)?
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import './style/StatusBar.sass';
|
||||
import React, {useState} from "react";
|
||||
import warningIcon from '../assets/warning_icon.svg';
|
||||
import failIcon from '../assets/failed.svg';
|
||||
import successIcon from '../assets/success.svg';
|
||||
|
||||
export interface TappingStatusPod {
|
||||
name: string;
|
||||
namespace: string;
|
||||
isTapped: boolean;
|
||||
}
|
||||
|
||||
export interface TappingStatus {
|
||||
@@ -11,7 +15,7 @@ export interface TappingStatus {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
tappingStatus: TappingStatus
|
||||
tappingStatus: TappingStatusPod[]
|
||||
}
|
||||
|
||||
const pluralize = (noun: string, amount: number) => {
|
||||
@@ -22,23 +26,29 @@ export const StatusBar: React.FC<Props> = ({tappingStatus}) => {
|
||||
|
||||
const [expandedBar, setExpandedBar] = useState(false);
|
||||
|
||||
const uniqueNamespaces = Array.from(new Set(tappingStatus.pods.map(pod => pod.namespace)));
|
||||
const amountOfPods = tappingStatus.pods.length;
|
||||
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
|
||||
const amountOfPods = tappingStatus.length;
|
||||
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
|
||||
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
|
||||
|
||||
return <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
|
||||
<div className="podsCount">{`Tapping ${amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}</div>
|
||||
<div className="podsCount">
|
||||
{tappingStatus.some(pod => !pod.isTapped) && <img src={warningIcon} alt="warning"/>}
|
||||
{`Tapping ${amountOfUntappedPods > 0 ? amountOfTappedPods + " / " + amountOfPods : amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}</div>
|
||||
{expandedBar && <div style={{marginTop: 20}}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pod name</th>
|
||||
<th>Namespace</th>
|
||||
<th style={{marginLeft: 10}}>Tapping</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tappingStatus.pods.map(pod => <tr key={pod.name}>
|
||||
{tappingStatus.map(pod => <tr key={pod.name}>
|
||||
<td>{pod.name}</td>
|
||||
<td>{pod.namespace}</td>
|
||||
<td style={{textAlign: "center"}}><img style={{height: 20}} alt="status" src={pod.isTapped ? successIcon : failIcon}/></td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
export const highlighterStyle = {
|
||||
"code[class*=\"language-\"]": {
|
||||
"color": "#494677",
|
||||
"fontFamily": "Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace",
|
||||
"direction": "ltr",
|
||||
"textAlign": "left",
|
||||
"whiteSpace": "pre",
|
||||
"wordSpacing": "normal",
|
||||
"wordBreak": "normal",
|
||||
"lineHeight": "1.5",
|
||||
"MozTabSize": "4",
|
||||
"OTabSize": "4",
|
||||
"tabSize": "4",
|
||||
"padding": "1rem",
|
||||
"WebkitHyphetokenns": "none",
|
||||
"MozHyphens": "none",
|
||||
"msHyphens": "none",
|
||||
"hyphens": "none"
|
||||
},
|
||||
"pre[class*=\"language-\"]": {
|
||||
"color": "#494677",
|
||||
"fontFamily": "Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace",
|
||||
"direction": "ltr",
|
||||
"textAlign": "left",
|
||||
"whiteSpace": "pre",
|
||||
"wordSpacing": "normal",
|
||||
"wordBreak": "normal",
|
||||
"lineHeight": "1.2",
|
||||
"MozTabSize": "4",
|
||||
"OTabSize": "4",
|
||||
"tabSize": "4",
|
||||
"WebkitHyphens": "none",
|
||||
"MozHyphens": "none",
|
||||
"msHyphens": "none",
|
||||
"hyphens": "none",
|
||||
"padding": "0",
|
||||
"margin": ".5em 0",
|
||||
"overflow": "auto",
|
||||
"borderRadius": "0.3em",
|
||||
"background": "#F7F9FC"
|
||||
},
|
||||
":not(pre) > code[class*=\"language-\"]": {
|
||||
"background": "#F7F9FC",
|
||||
"padding": ".1em",
|
||||
"borderRadius": ".3em"
|
||||
},
|
||||
"comment": {
|
||||
"color": "#5d6aa0"
|
||||
},
|
||||
"prolog": {
|
||||
"color": "#494677"
|
||||
},
|
||||
"doctype": {
|
||||
"color": "#494677"
|
||||
},
|
||||
"cdata": {
|
||||
"color": "#494677"
|
||||
},
|
||||
"punctuation": {
|
||||
"color": "#494677"
|
||||
},
|
||||
".namespace": {
|
||||
"Opacity": ".7"
|
||||
},
|
||||
"property": {
|
||||
"color": "#627ef7"
|
||||
},
|
||||
"keyword": {
|
||||
"color": "#627ef7"
|
||||
},
|
||||
"tag": {
|
||||
"color": "#627ef7"
|
||||
},
|
||||
"class-name": {
|
||||
"color": "#3eb545",
|
||||
"textDecoration": "underline"
|
||||
},
|
||||
"boolean": {
|
||||
"color": "#3eb545"
|
||||
},
|
||||
"constant": {
|
||||
"color": "#3eb545"
|
||||
},
|
||||
"symbol": {
|
||||
"color": "#ff3a30"
|
||||
},
|
||||
"deleted": {
|
||||
"color": "#ff3a30"
|
||||
},
|
||||
"number": {
|
||||
"color": "#ff16f7"
|
||||
},
|
||||
"selector": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"attr-name": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"string": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"char": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"builtin": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"inserted": {
|
||||
"color": "rgb(9,224,19)"
|
||||
},
|
||||
"variable": {
|
||||
"color": "#C6C5FE"
|
||||
},
|
||||
"operator": {
|
||||
"color": "#A1A1A1"
|
||||
},
|
||||
"entity": {
|
||||
"color": "#fdab2b",
|
||||
"cursor": "help"
|
||||
},
|
||||
"url": {
|
||||
"color": "#96CBFE"
|
||||
},
|
||||
".language-css .token.string": {
|
||||
"color": "#87C38A"
|
||||
},
|
||||
".style .token.string": {
|
||||
"color": "#87C38A"
|
||||
},
|
||||
"atrule": {
|
||||
"color": "#fdab2b"
|
||||
},
|
||||
"attr-value": {
|
||||
"color": "#f8c575"
|
||||
},
|
||||
"function": {
|
||||
"color": "#fdab2b"
|
||||
},
|
||||
"regex": {
|
||||
"color": "#fab248"
|
||||
},
|
||||
"important": {
|
||||
"color": "#fd971f",
|
||||
"fontWeight": "bold"
|
||||
},
|
||||
"bold": {
|
||||
"fontWeight": "bold"
|
||||
},
|
||||
"italic": {
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
};
|
||||
@@ -26,12 +26,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.wrapped{
|
||||
pre {
|
||||
code {
|
||||
&:last-child {
|
||||
white-space: pre-wrap!important
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
code.hljs {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
code.hljs:before {
|
||||
counter-reset: listing;
|
||||
}
|
||||
|
||||
code.hljs .hljs-marker-line {
|
||||
counter-increment: listing;
|
||||
}
|
||||
|
||||
code.hljs .hljs-marker-line:before {
|
||||
content: counter(listing) " ";
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
padding-left: auto;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,47 @@
|
||||
import React from 'react';
|
||||
import {Prism as SyntaxHighlighterContainer} from 'react-syntax-highlighter';
|
||||
import {highlighterStyle} from './highlighterStyle'
|
||||
import Lowlight from 'react-lowlight'
|
||||
import 'highlight.js/styles/atom-one-light.css'
|
||||
import './index.scss';
|
||||
|
||||
import xml from 'highlight.js/lib/languages/xml'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
import protobuf from 'highlight.js/lib/languages/protobuf'
|
||||
import javascript from 'highlight.js/lib/languages/javascript'
|
||||
import actionscript from 'highlight.js/lib/languages/actionscript'
|
||||
import wasm from 'highlight.js/lib/languages/wasm'
|
||||
import handlebars from 'highlight.js/lib/languages/handlebars'
|
||||
import yaml from 'highlight.js/lib/languages/yaml'
|
||||
import python from 'highlight.js/lib/languages/python'
|
||||
|
||||
Lowlight.registerLanguage('python', python);
|
||||
Lowlight.registerLanguage('xml', xml);
|
||||
Lowlight.registerLanguage('json', json);
|
||||
Lowlight.registerLanguage('yaml', yaml);
|
||||
Lowlight.registerLanguage('protobuf', protobuf);
|
||||
Lowlight.registerLanguage('javascript', javascript);
|
||||
Lowlight.registerLanguage('actionscript', actionscript);
|
||||
Lowlight.registerLanguage('wasm', wasm);
|
||||
Lowlight.registerLanguage('handlebars', handlebars);
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
style?: any;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
language?: string;
|
||||
isWrapped?: boolean;
|
||||
}
|
||||
|
||||
export const SyntaxHighlighter: React.FC<Props> = ({
|
||||
code,
|
||||
style = highlighterStyle,
|
||||
showLineNumbers = true,
|
||||
className,
|
||||
language = 'python',
|
||||
isWrapped = false,
|
||||
}) => {
|
||||
return <div className={`highlighterContainer ${className ? className : ''} ${isWrapped ? 'wrapped' : ''}`}>
|
||||
<SyntaxHighlighterContainer language={language} style={style} showLineNumbers={showLineNumbers}>
|
||||
{code ?? ""}
|
||||
</SyntaxHighlighterContainer>
|
||||
</div>;
|
||||
code,
|
||||
showLineNumbers = false,
|
||||
language = null
|
||||
}) => {
|
||||
const markers = showLineNumbers ? code.split("\n").map((item, i) => {
|
||||
return {
|
||||
line: i + 1,
|
||||
className: 'hljs-marker-line'
|
||||
}
|
||||
}) : [];
|
||||
|
||||
return <div style={{fontSize: ".75rem"}}><Lowlight language={language ? language : ""} value={code} markers={markers}/></div>;
|
||||
};
|
||||
|
||||
export default SyntaxHighlighter;
|
||||
|
||||
@@ -24,8 +24,13 @@
|
||||
padding: 8px
|
||||
font-weight: 600
|
||||
|
||||
img
|
||||
margin-right: 10px
|
||||
height: 22px
|
||||
|
||||
th
|
||||
text-align: left
|
||||
padding-right: 15px
|
||||
td
|
||||
padding-right: 15px
|
||||
padding-top: 5px
|
||||
|
||||
1
ui/src/components/assets/failed.svg
Normal file
1
ui/src/components/assets/failed.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="wRKXFJsqHCxLE9yyOYHkza" x1="9.858" x2="38.142" y1="9.858" y2="38.142" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f44f5a"/><stop offset=".443" stop-color="#ee3d4a"/><stop offset="1" stop-color="#e52030"/></linearGradient><path fill="url(#wRKXFJsqHCxLE9yyOYHkza)" d="M44,24c0,11.045-8.955,20-20,20S4,35.045,4,24S12.955,4,24,4S44,12.955,44,24z"/><path d="M33.192,28.95L28.243,24l4.95-4.95c0.781-0.781,0.781-2.047,0-2.828l-1.414-1.414 c-0.781-0.781-2.047-0.781-2.828,0L24,19.757l-4.95-4.95c-0.781-0.781-2.047-0.781-2.828,0l-1.414,1.414 c-0.781,0.781-0.781,2.047,0,2.828l4.95,4.95l-4.95,4.95c-0.781,0.781-0.781,2.047,0,2.828l1.414,1.414 c0.781,0.781,2.047,0.781,2.828,0l4.95-4.95l4.95,4.95c0.781,0.781,2.047,0.781,2.828,0l1.414-1.414 C33.973,30.997,33.973,29.731,33.192,28.95z" opacity=".05"/><path d="M32.839,29.303L27.536,24l5.303-5.303c0.586-0.586,0.586-1.536,0-2.121l-1.414-1.414 c-0.586-0.586-1.536-0.586-2.121,0L24,20.464l-5.303-5.303c-0.586-0.586-1.536-0.586-2.121,0l-1.414,1.414 c-0.586,0.586-0.586,1.536,0,2.121L20.464,24l-5.303,5.303c-0.586,0.586-0.586,1.536,0,2.121l1.414,1.414 c0.586,0.586,1.536,0.586,2.121,0L24,27.536l5.303,5.303c0.586,0.586,1.536,0.586,2.121,0l1.414-1.414 C33.425,30.839,33.425,29.889,32.839,29.303z" opacity=".07"/><path fill="#fff" d="M31.071,15.515l1.414,1.414c0.391,0.391,0.391,1.024,0,1.414L18.343,32.485 c-0.391,0.391-1.024,0.391-1.414,0l-1.414-1.414c-0.391-0.391-0.391-1.024,0-1.414l14.142-14.142 C30.047,15.124,30.681,15.124,31.071,15.515z"/><path fill="#fff" d="M32.485,31.071l-1.414,1.414c-0.391,0.391-1.024,0.391-1.414,0L15.515,18.343 c-0.391-0.391-0.391-1.024,0-1.414l1.414-1.414c0.391-0.391,1.024-0.391,1.414,0l14.142,14.142 C32.876,30.047,32.876,30.681,32.485,31.071z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
ui/src/components/assets/success.svg
Normal file
1
ui/src/components/assets/success.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="I9GV0SozQFknxHSR6DCx5a" x1="9.858" x2="38.142" y1="9.858" y2="38.142" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#21ad64"/><stop offset="1" stop-color="#088242"/></linearGradient><path fill="url(#I9GV0SozQFknxHSR6DCx5a)" d="M44,24c0,11.045-8.955,20-20,20S4,35.045,4,24S12.955,4,24,4S44,12.955,44,24z"/><path d="M32.172,16.172L22,26.344l-5.172-5.172c-0.781-0.781-2.047-0.781-2.828,0l-1.414,1.414 c-0.781,0.781-0.781,2.047,0,2.828l8,8c0.781,0.781,2.047,0.781,2.828,0l13-13c0.781-0.781,0.781-2.047,0-2.828L35,16.172 C34.219,15.391,32.953,15.391,32.172,16.172z" opacity=".05"/><path d="M20.939,33.061l-8-8c-0.586-0.586-0.586-1.536,0-2.121l1.414-1.414c0.586-0.586,1.536-0.586,2.121,0 L22,27.051l10.525-10.525c0.586-0.586,1.536-0.586,2.121,0l1.414,1.414c0.586,0.586,0.586,1.536,0,2.121l-13,13 C22.475,33.646,21.525,33.646,20.939,33.061z" opacity=".07"/><path fill="#fff" d="M21.293,32.707l-8-8c-0.391-0.391-0.391-1.024,0-1.414l1.414-1.414c0.391-0.391,1.024-0.391,1.414,0 L22,27.758l10.879-10.879c0.391-0.391,1.024-0.391,1.414,0l1.414,1.414c0.391,0.391,0.391,1.024,0,1.414l-13,13 C22.317,33.098,21.683,33.098,21.293,32.707z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
34
ui/src/components/assets/warning_icon.svg
Normal file
34
ui/src/components/assets/warning_icon.svg
Normal file
@@ -0,0 +1,34 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="29.999" viewBox="0 0 22 29.999">
|
||||
<defs>
|
||||
<filter id="Rectangle_2909" width="21" height="25.999" x=".43" y="0" filterUnits="userSpaceOnUse">
|
||||
<feOffset dy="3"/>
|
||||
<feGaussianBlur result="blur" stdDeviation="3"/>
|
||||
<feFlood flood-opacity=".161"/>
|
||||
<feComposite in2="blur" operator="in"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
<filter id="Rectangle_2911" width="21" height="20.999" x=".43" y="9" filterUnits="userSpaceOnUse">
|
||||
<feOffset dy="3"/>
|
||||
<feGaussianBlur result="blur-2" stdDeviation="3"/>
|
||||
<feFlood flood-opacity=".161"/>
|
||||
<feComposite in2="blur-2" operator="in"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
<style>
|
||||
.cls-2{fill:#fff}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="warning_icon" transform="translate(-883 -4234.5)">
|
||||
<circle id="Ellipse_1021" cx="11" cy="11" r="11" fill="#fdab2b" data-name="Ellipse 1021" transform="translate(883 4235)"/>
|
||||
<g id="Group_5975" data-name="Group 5975" transform="translate(892.43 4240.5)">
|
||||
<g id="Group_5974" data-name="Group 5974">
|
||||
<g filter="url(#Rectangle_2909)" transform="translate(-9.43 -6)">
|
||||
<rect id="Rectangle_2909-2" width="3" height="7.999" class="cls-2" data-name="Rectangle 2909" rx="1.5" transform="translate(9.43 6)"/>
|
||||
</g>
|
||||
<g filter="url(#Rectangle_2911)" transform="translate(-9.43 -6)">
|
||||
<rect id="Rectangle_2911-2" width="3" height="2.999" class="cls-2" data-name="Rectangle 2911" rx="1.499" transform="translate(9.43 15)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user