Compare commits

..

26 Commits

Author SHA1 Message Date
RoyUP9
a9e92b60f5 added custom config path option (#247) 2021-08-26 13:50:41 +03:00
RoyUP9
35e40cd230 added tap acceptance tests, fixed duplicate namespace problem (#244) 2021-08-26 09:56:18 +03:00
Igor Gov
2575ad722a Introducing API server provider (#243) 2021-08-22 11:41:38 +03:00
RoyUP9
afd5757315 added tapper count route and wait time for tappers in test (#226) 2021-08-22 11:38:19 +03:00
Alon Girmonsky
dba8b1f215 some changes in the read me (#241)
change prerequisite to permissions and kubeconfig. These are more FYIs as Mizu requires very little prerequisites. 
Change the description to match getmizu.io
2021-08-20 12:39:52 +03:00
Igor Gov
6dd0ef1268 Adding user friendly message in view command before sleeping (#239) 2021-08-19 12:22:18 +03:00
Alex Haiut
83cfaed1a3 updated readme for release (#237) 2021-08-19 11:47:02 +03:00
Igor Gov
41cb9ee12e run acceptance tests for the latest code (and cancel all other jobs) (#238) 2021-08-19 11:40:37 +03:00
RoyUP9
667f0dc87d fixed namespace restricted validation (#235) 2021-08-19 11:33:48 +03:00
Igor Gov
a34c2fc0dc Adding version check to all commands execution (#236) 2021-08-19 11:33:20 +03:00
Nimrod Gilboa Markevich
7a31263e4a Reduce spam - print TLS detected as DEBUG level (#234) 2021-08-19 11:17:59 +03:00
RoyUP9
7f9fd82c0e fixed panic when using invalid kube config path (#231) 2021-08-19 10:59:31 +03:00
Nimrod Gilboa Markevich
a37d1f4aeb Fixed: Stopped redacting JSON after encountering nil values (#233) 2021-08-19 10:59:13 +03:00
gadotroee
acdbdedd5d Add concurrency to mizu publish action (#232) 2021-08-19 10:31:55 +03:00
Igor Gov
a9b5eba9d4 Fix: View command fail sporadically (#228) 2021-08-19 09:44:43 +03:00
RoyUP9
80201224c6 telemetry machine id (#230) 2021-08-19 09:44:23 +03:00
Selton Fiuza
e6e7d8d58b Fix TRA-3590 TRA-3589 (#229) 2021-08-18 22:28:13 +03:00
RoyUP9
bf27e94003 fixed version check, removed duplicate kube config, fix flags warning, fixed log of invalid config (#227) 2021-08-18 18:10:47 +03:00
Igor Gov
2ae0a2400d PR validation should be triggered just by PR (#225) 2021-08-18 12:51:24 +03:00
RoyUP9
db1f4458c5 Introducing acceptance test (#222) 2021-08-18 10:22:45 +03:00
Nimrod Gilboa Markevich
5d5c11c37c Add to periodic stats print in tapper (#220) 2021-08-16 14:51:01 +03:00
RoyUP9
b4f3b2c540 fixed test coverage (#218) 2021-08-15 14:22:49 +03:00
RoyUP9
a427534605 tests refactor (#216) 2021-08-15 12:30:34 +03:00
RoyUP9
1d6ca9d392 codecov yml for tests threshold (#214) 2021-08-15 12:19:00 +03:00
lirazyehezkel
f74a52d4dc UI Infra - Support multiple entry types + refactoring (#211)
* no message

* change local api path

* generic entry list item + rename files and vars

* entry detailed generic

* fix api file

* clean warnings

* switch

* empty lines

* fix scroll to end feature

Co-authored-by: Roee Gadot <roee.gadot@up9.com>
2021-08-15 12:09:56 +03:00
Neim Elezi
6d2e9af5d7 Feature/tra 3475 scroll to end (#206)
* configuration changed

* testing scroll with button

* back to scroll button feature is done

* scroll to the end of entries feature is done

* config of docker image is reverted back

* path of docker image is changed in configStruct.go
2021-08-15 10:58:16 +03:00
115 changed files with 3245 additions and 1368 deletions

32
.github/workflows/acceptance_tests.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: acceptance tests
on:
pull_request:
branches:
- 'main'
push:
branches:
- 'develop'
concurrency:
group: mizu-acceptance-tests-${{ github.ref }}
cancel-in-progress: true
jobs:
run-acceptance-tests:
name: Run acceptance tests
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Setup acceptance test
run: source ./acceptanceTests/setup.sh
- name: Test
run: make acceptance-test

46
.github/workflows/pr_validation.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: PR validation
on:
pull_request:
branches:
- 'develop'
- 'main'
concurrency:
group: mizu-pr-validation-${{ github.ref }}
cancel-in-progress: true
jobs:
build-cli:
name: Build CLI
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build CLI
run: make cli
build-agent:
name: Build Agent
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- shell: bash
run: |
sudo apt-get install libpcap-dev
- name: Build Agent
run: make agent

View File

@@ -1,9 +1,15 @@
name: publish
on:
push:
branches:
- 'develop'
- 'main'
concurrency:
group: mizu-publish-${{ github.ref }}
cancel-in-progress: true
jobs:
docker:
runs-on: ubuntu-latest
@@ -78,4 +84,3 @@ jobs:
tag: ${{ steps.versioning.outputs.version }}
prerelease: ${{ github.ref != 'refs/heads/main' }}
bodyFile: 'cli/bin/README.md'

View File

@@ -1,4 +1,5 @@
name: Validations
name: tests validation
on:
pull_request:
branches:
@@ -8,41 +9,12 @@ on:
branches:
- 'develop'
- 'main'
concurrency:
group: mizu-tests-validation-${{ github.ref }}
cancel-in-progress: true
jobs:
build-cli:
name: Build CLI
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build CLI
run: make cli
build-agent:
name: Build Agent
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.16
uses: actions/setup-go@v2
with:
go-version: '^1.16'
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- shell: bash
run: |
sudo apt-get install libpcap-dev
- name: Build Agent
run: make agent
run-tests-cli:
name: Run CLI tests
runs-on: ubuntu-latest

View File

@@ -28,6 +28,9 @@ ui: ## Build UI.
cli: ## Build CLI.
@echo "building cli"; cd cli && $(MAKE) build
build-cli-ci: ## Build CLI for CI.
@echo "building cli for ci"; cd cli && $(MAKE) build GIT_BRANCH=ci SUFFIX=ci
agent: ## Build agent.
@(echo "building mizu agent .." )
@(cd agent; go build -o build/mizuagent main.go)
@@ -42,6 +45,10 @@ push-docker: ## Build and publish agent docker image.
@echo "publishing Docker image .. "
./build-push-featurebranch.sh
build-docker-ci: ## Build agent docker image for CI.
@echo "building docker image for ci"
./build-agent-ci.sh
push-cli: ## Build and publish CLI.
@echo "publishing CLI .. "
@cd cli; $(MAKE) build-all
@@ -50,7 +57,6 @@ push-cli: ## Build and publish CLI.
gsutil cp -r ./cli/bin/* gs://${BUCKET_PATH}/
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts.
clean-ui: ## Clean UI.
@@ -65,8 +71,11 @@ clean-cli: ## Clean CLI.
clean-docker:
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
test-cli: ## Run tests.
test-cli:
@echo "running cli tests"; cd cli && $(MAKE) test
test-agent: ## Run tests.
test-agent:
@echo "running agent tests"; cd agent && $(MAKE) test
acceptance-test:
@echo "running acceptance tests"; cd acceptanceTests && $(MAKE) test

View File

@@ -2,7 +2,9 @@
# The API Traffic Viewer for Kubernetes
A simple-yet-powerful API traffic viewer for Kubernetes to help you troubleshoot and debug your microservices. Think TCPDump and Chrome Dev Tools combined
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
Think TCPDump and Chrome Dev Tools combined.
![Simple UI](assets/mizu-ui.png)
@@ -38,8 +40,10 @@ SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/
### Development (unstable) Build
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page
## Prerequisites
1. Set `KUBECONFIG` environment variable to your Kubernetes configuration. If this is not set, Mizu assumes that configuration is at `${HOME}/.kube/config`
## Kubeconfig & Permissions
While `mizu`most often works out of the box, you can influence its behavior:
1. [OPTIONAL] Set `KUBECONFIG` environment variable to your Kubernetes configuration. If this is not set, Mizu assumes that configuration is at `${HOME}/.kube/config`
2. `mizu` assumes user running the command has permissions to create resources (such as pods, services, namespaces) on your Kubernetes cluster (no worries - `mizu` resources are cleaned up upon termination)
For detailed list of k8s permissions see [PERMISSIONS](PERMISSIONS.md) document
@@ -146,6 +150,7 @@ Web interface is now available at http://localhost:8899
^C
```
Any request that contains `User-Agent` header with one of the specified values (`kube-probe` or `prometheus`) will not be captured
### API Rules validation
@@ -155,3 +160,15 @@ Such validation may test response for specific JSON fields, headers, etc.
Please see [API RULES](docs/POLICY_RULES.md) page for more details and syntax.
## How to Run local UI
- run from mizu/agent `go run main.go --hars-read --hars-dir <folder>`
- copy Har files into the folder from last command
- change `MizuWebsocketURL` and `apiURL` in `api.js` file
- run from mizu/ui - `npm run start`
- open browser on `localhost:3000`

2
acceptanceTests/Makefile Normal file
View File

@@ -0,0 +1,2 @@
test: ## Run acceptance tests.
@go test ./... -timeout 1h

View File

@@ -0,0 +1,283 @@
package acceptanceTests
import (
"fmt"
"gopkg.in/yaml.v3"
"io/ioutil"
"os"
"os/exec"
"testing"
)
type tapConfig struct {
GuiPort uint16 `yaml:"gui-port"`
}
type configStruct struct {
Tap tapConfig `yaml:"tap"`
}
func TestConfigRegenerate(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
configPath, configPathErr := getConfigPath()
if configPathErr != nil {
t.Errorf("failed to get config path, err: %v", cliPathErr)
return
}
configCmdArgs := getDefaultConfigCommandArgs()
configCmdArgs = append(configCmdArgs, "-r")
configCmd := exec.Command(cliPath, configCmdArgs...)
t.Logf("running command: %v", configCmd.String())
t.Cleanup(func() {
if err := os.Remove(configPath); err != nil {
t.Logf("failed to delete config file, err: %v", err)
}
})
if err := configCmd.Start(); err != nil {
t.Errorf("failed to start config command, err: %v", err)
return
}
if err := configCmd.Wait(); err != nil {
t.Errorf("failed to wait config command, err: %v", err)
return
}
_, readFileErr := ioutil.ReadFile(configPath)
if readFileErr != nil {
t.Errorf("failed to read config file, err: %v", readFileErr)
return
}
}
func TestConfigGuiPort(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
tests := []uint16{8898}
for _, guiPort := range tests {
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
configPath, configPathErr := getConfigPath()
if configPathErr != nil {
t.Errorf("failed to get config path, err: %v", cliPathErr)
return
}
config := configStruct{}
config.Tap.GuiPort = guiPort
configBytes, marshalErr := yaml.Marshal(config)
if marshalErr != nil {
t.Errorf("failed to marshal config, err: %v", marshalErr)
return
}
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
t.Errorf("failed to write config to file, err: %v", writeErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
if err := os.Remove(configPath); err != nil {
t.Logf("failed to delete config file, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(guiPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
})
}
}
func TestConfigSetGuiPort(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
tests := []struct {
ConfigFileGuiPort uint16
SetGuiPort uint16
}{
{ConfigFileGuiPort: 8898, SetGuiPort: 8897},
}
for _, guiPortStruct := range tests {
t.Run(fmt.Sprintf("%d", guiPortStruct.SetGuiPort), func(t *testing.T) {
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
configPath, configPathErr := getConfigPath()
if configPathErr != nil {
t.Errorf("failed to get config path, err: %v", cliPathErr)
return
}
config := configStruct{}
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
configBytes, marshalErr := yaml.Marshal(config)
if marshalErr != nil {
t.Errorf("failed to marshal config, err: %v", marshalErr)
return
}
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
t.Errorf("failed to write config to file, err: %v", writeErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "--set", fmt.Sprintf("tap.gui-port=%v", guiPortStruct.SetGuiPort))
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
if err := os.Remove(configPath); err != nil {
t.Logf("failed to delete config file, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(guiPortStruct.SetGuiPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
})
}
}
func TestConfigFlagGuiPort(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
tests := []struct {
ConfigFileGuiPort uint16
FlagGuiPort uint16
}{
{ConfigFileGuiPort: 8898, FlagGuiPort: 8896},
}
for _, guiPortStruct := range tests {
t.Run(fmt.Sprintf("%d", guiPortStruct.FlagGuiPort), func(t *testing.T) {
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
configPath, configPathErr := getConfigPath()
if configPathErr != nil {
t.Errorf("failed to get config path, err: %v", cliPathErr)
return
}
config := configStruct{}
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
configBytes, marshalErr := yaml.Marshal(config)
if marshalErr != nil {
t.Errorf("failed to marshal config, err: %v", marshalErr)
return
}
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
t.Errorf("failed to write config to file, err: %v", writeErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%v", guiPortStruct.FlagGuiPort))
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
if err := os.Remove(configPath); err != nil {
t.Logf("failed to delete config file, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(guiPortStruct.FlagGuiPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
})
}
}

5
acceptanceTests/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/up9inc/mizu/tests
go 1.16
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b

4
acceptanceTests/go.sum Normal file
View File

@@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

55
acceptanceTests/setup.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
PREFIX=$HOME/local/bin
VERSION=v1.22.0
echo "Attempting to install minikube and assorted tools to $PREFIX"
if ! [ -x "$(command -v kubectl)" ]; then
echo "Installing kubectl version $VERSION"
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl "$PREFIX"
else
echo "kubetcl is already installed"
fi
if ! [ -x "$(command -v minikube)" ]; then
echo "Installing minikube version $VERSION"
curl -Lo minikube https://storage.googleapis.com/minikube/releases/$VERSION/minikube-linux-amd64
chmod +x minikube
mv minikube "$PREFIX"
else
echo "minikube is already installed"
fi
echo "Starting minikube..."
minikube start
echo "Creating mizu tests namespaces"
kubectl create namespace mizu-tests
kubectl create namespace mizu-tests2
echo "Creating httpbin deployments"
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests
kubectl create deployment httpbin2 --image=kennethreitz/httpbin -n mizu-tests
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests2
echo "Creating httpbin services"
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests
kubectl expose deployment httpbin2 --type=NodePort --port=80 -n mizu-tests
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests2
echo "Starting proxy"
kubectl proxy --port=8080 &
echo "Setting minikube docker env"
eval $(minikube docker-env)
echo "Build agent image"
make build-docker-ci
echo "Build cli"
make build-cli-ci

754
acceptanceTests/tap_test.go Normal file
View File

@@ -0,0 +1,754 @@
package acceptanceTests
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os/exec"
"strings"
"testing"
"time"
)
func TestTapAndFetch(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
tests := []int{50}
for _, entriesCount := range tests {
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
for i := 0; i < entriesCount; i++ {
if _, requestErr := executeHttpGetRequest(fmt.Sprintf("%v/get", proxyUrl)); requestErr != nil {
t.Errorf("failed to send proxy request, err: %v", requestErr)
return
}
}
entriesCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt&timestamp=%v", apiServerUrl, entriesCount, timestamp)
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entries, err: %v", requestErr)
}
entries := requestResult.([]interface{})
if len(entries) == 0 {
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
}
entry := entries[0].(map[string]interface{})
entryUrl := fmt.Sprintf("%v/api/entries/%v", apiServerUrl, entry["id"])
requestResult, requestErr = executeHttpGetRequest(entryUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entry, err: %v", requestErr)
}
if requestResult == nil {
return fmt.Errorf("unexpected nil entry result")
}
return nil
}
if err := retriesExecute(shortRetriesCount, entriesCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
fetchCmdArgs := getDefaultFetchCommandArgs()
fetchCmd := exec.Command(cliPath, fetchCmdArgs...)
t.Logf("running command: %v", fetchCmd.String())
if err := fetchCmd.Start(); err != nil {
t.Errorf("failed to start fetch command, err: %v", err)
return
}
harCheckFunc := func() error {
harBytes, readFileErr := ioutil.ReadFile("./unknown_source.har")
if readFileErr != nil {
return fmt.Errorf("failed to read har file, err: %v", readFileErr)
}
harEntries, err := getEntriesFromHarBytes(harBytes)
if err != nil {
return fmt.Errorf("failed to get entries from har, err: %v", err)
}
if len(harEntries) == 0 {
return fmt.Errorf("unexpected har entries result - Expected more than 0 entries")
}
return nil
}
if err := retriesExecute(shortRetriesCount, harCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
})
}
}
func TestTapGuiPort(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
tests := []uint16{8898}
for _, guiPort := range tests {
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%d", guiPort))
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(guiPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
})
}
}
func TestTapAllNamespaces(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
expectedPods := []struct{
Name string
Namespace string
}{
{Name: "httpbin", Namespace: "mizu-tests"},
{Name: "httpbin", Namespace: "mizu-tests2"},
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapCmdArgs = append(tapCmdArgs, "-A")
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
requestResult, requestErr := executeHttpGetRequest(podsUrl)
if requestErr != nil {
t.Errorf("failed to get tap status, err: %v", requestErr)
return
}
pods, err := getPods(requestResult)
if err != nil {
t.Errorf("failed to get pods, err: %v", err)
return
}
for _, expectedPod := range expectedPods {
podFound := false
for _, pod := range pods {
podNamespace := pod["namespace"].(string)
podName := pod["name"].(string)
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
podFound = true
break
}
}
if !podFound {
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
return
}
}
}
func TestTapMultipleNamespaces(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
expectedPods := []struct{
Name string
Namespace string
}{
{Name: "httpbin", Namespace: "mizu-tests"},
{Name: "httpbin2", Namespace: "mizu-tests"},
{Name: "httpbin", Namespace: "mizu-tests2"},
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
var namespacesCmd []string
for _, expectedPod := range expectedPods {
namespacesCmd = append(namespacesCmd, "-n", expectedPod.Namespace)
}
tapCmdArgs = append(tapCmdArgs, namespacesCmd...)
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
requestResult, requestErr := executeHttpGetRequest(podsUrl)
if requestErr != nil {
t.Errorf("failed to get tap status, err: %v", requestErr)
return
}
pods, err := getPods(requestResult)
if err != nil {
t.Errorf("failed to get pods, err: %v", err)
return
}
if len(expectedPods) != len(pods) {
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
return
}
for _, expectedPod := range expectedPods {
podFound := false
for _, pod := range pods {
podNamespace := pod["namespace"].(string)
podName := pod["name"].(string)
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
podFound = true
break
}
}
if !podFound {
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
return
}
}
}
func TestTapRegex(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
regexPodName := "httpbin2"
expectedPods := []struct{
Name string
Namespace string
}{
{Name: regexPodName, Namespace: "mizu-tests"},
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgsWithRegex(regexPodName)
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
podsUrl := fmt.Sprintf("%v/api/tapStatus", apiServerUrl)
requestResult, requestErr := executeHttpGetRequest(podsUrl)
if requestErr != nil {
t.Errorf("failed to get tap status, err: %v", requestErr)
return
}
pods, err := getPods(requestResult)
if err != nil {
t.Errorf("failed to get pods, err: %v", err)
return
}
if len(expectedPods) != len(pods) {
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
return
}
for _, expectedPod := range expectedPods {
podFound := false
for _, pod := range pods {
podNamespace := pod["namespace"].(string)
podName := pod["name"].(string)
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
podFound = true
break
}
}
if !podFound {
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
return
}
}
}
func TestTapDryRun(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "--dry-run")
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
resultChannel := make(chan string, 1)
go func() {
if err := tapCmd.Wait(); err != nil {
resultChannel <- "fail"
return
}
resultChannel <- "success"
}()
go func() {
time.Sleep(shortRetriesCount * time.Second)
resultChannel <- "fail"
}()
testResult := <- resultChannel
if testResult != "success" {
t.Errorf("unexpected result - dry run cmd not done")
}
}
func TestTapRedact(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
requestBody := map[string]string{"User": "Mizu"}
for i := 0; i < defaultEntriesCount; i++ {
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
t.Errorf("failed to send proxy request, err: %v", requestErr)
return
}
}
redactCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt&timestamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entries, err: %v", requestErr)
}
entries := requestResult.([]interface{})
firstEntry := entries[0].(map[string]interface{})
entryUrl := fmt.Sprintf("%v/api/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{})["entry"].(map[string]interface{})
entryRequest := entry["request"].(map[string]interface{})
headers := entryRequest["headers"].([]interface{})
for _, headerInterface := range headers {
header := headerInterface.(map[string]interface{})
if header["name"].(string) != "User-Agent" {
continue
}
userAgent := header["value"].(string)
if userAgent != "[REDACTED]" {
return fmt.Errorf("unexpected result - user agent is not redacted")
}
}
data := entryRequest["postData"].(map[string]interface{})
textDataStr := data["text"].(string)
var textData map[string]string
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
}
if textData["User"] != "[REDACTED]" {
return fmt.Errorf("unexpected result - user in body is not redacted")
}
return nil
}
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
}
func TestTapNoRedact(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "--no-redact")
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
requestBody := map[string]string{"User": "Mizu"}
for i := 0; i < defaultEntriesCount; i++ {
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
t.Errorf("failed to send proxy request, err: %v", requestErr)
return
}
}
redactCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt&timestamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entries, err: %v", requestErr)
}
entries := requestResult.([]interface{})
firstEntry := entries[0].(map[string]interface{})
entryUrl := fmt.Sprintf("%v/api/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{})["entry"].(map[string]interface{})
entryRequest := entry["request"].(map[string]interface{})
headers := entryRequest["headers"].([]interface{})
for _, headerInterface := range headers {
header := headerInterface.(map[string]interface{})
if header["name"].(string) != "User-Agent" {
continue
}
userAgent := header["value"].(string)
if userAgent == "[REDACTED]" {
return fmt.Errorf("unexpected result - user agent is redacted")
}
}
data := entryRequest["postData"].(map[string]interface{})
textDataStr := data["text"].(string)
var textData map[string]string
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
}
if textData["User"] == "[REDACTED]" {
return fmt.Errorf("unexpected result - user in body is redacted")
}
return nil
}
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
t.Errorf("%v", err)
return
}
}
func TestTapRegexMasking(t *testing.T) {
if testing.Short() {
t.Skip("ignored acceptance test")
}
cliPath, cliPathErr := getCliPath()
if cliPathErr != nil {
t.Errorf("failed to get cli path, err: %v", cliPathErr)
return
}
tapCmdArgs := getDefaultTapCommandArgs()
tapNamespace := getDefaultTapNamespace()
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
tapCmdArgs = append(tapCmdArgs, "-r", "Mizu")
tapCmd := exec.Command(cliPath, tapCmdArgs...)
t.Logf("running command: %v", tapCmd.String())
t.Cleanup(func() {
if err := cleanupCommand(tapCmd); err != nil {
t.Logf("failed to cleanup tap command, err: %v", err)
}
})
if err := tapCmd.Start(); err != nil {
t.Errorf("failed to start tap command, err: %v", err)
return
}
apiServerUrl := getApiServerUrl(defaultApiServerPort)
if err := waitTapPodsReady(apiServerUrl); err != nil {
t.Errorf("failed to start tap pods on time, err: %v", err)
return
}
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
for i := 0; i < defaultEntriesCount; i++ {
response, requestErr := http.Post(fmt.Sprintf("%v/post", proxyUrl), "text/plain", bytes.NewBufferString("Mizu"))
if _, requestErr = executeHttpRequest(response, requestErr); requestErr != nil {
t.Errorf("failed to send proxy request, err: %v", requestErr)
return
}
}
redactCheckFunc := func() error {
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
entriesUrl := fmt.Sprintf("%v/api/entries?limit=%v&operator=lt&timestamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
if requestErr != nil {
return fmt.Errorf("failed to get entries, err: %v", requestErr)
}
entries := requestResult.([]interface{})
firstEntry := entries[0].(map[string]interface{})
entryUrl := fmt.Sprintf("%v/api/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{})["entry"].(map[string]interface{})
entryRequest := entry["request"].(map[string]interface{})
data := entryRequest["postData"].(map[string]interface{})
textData := data["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
}
}

View File

@@ -0,0 +1,205 @@
package acceptanceTests
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path"
"syscall"
"time"
)
const (
longRetriesCount = 100
shortRetriesCount = 10
defaultApiServerPort = 8899
defaultNamespaceName = "mizu-tests"
defaultServiceName = "httpbin"
defaultEntriesCount = 50
)
func getCliPath() (string, error) {
dir, filePathErr := os.Getwd()
if filePathErr != nil {
return "", filePathErr
}
cliPath := path.Join(dir, "../cli/bin/mizu_ci")
return cliPath, nil
}
func getConfigPath() (string, error) {
home, homeDirErr := os.UserHomeDir()
if homeDirErr != nil {
return "", homeDirErr
}
return path.Join(home, ".mizu", "config.yaml"), nil
}
func getProxyUrl(namespace string, service string) string {
return fmt.Sprintf("http://localhost:8080/api/v1/namespaces/%v/services/%v/proxy", namespace, service)
}
func getApiServerUrl(port uint16) string {
return fmt.Sprintf("http://localhost:%v/mizu", port)
}
func getDefaultCommandArgs() []string {
setFlag := "--set"
telemetry := "telemetry=false"
agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0"
imagePullPolicy := "image-pull-policy=Never"
return []string{setFlag, telemetry, setFlag, agentImage, setFlag, imagePullPolicy}
}
func getDefaultTapCommandArgs() []string {
tapCommand := "tap"
defaultCmdArgs := getDefaultCommandArgs()
return append([]string{tapCommand}, defaultCmdArgs...)
}
func getDefaultTapCommandArgsWithRegex(regex string) []string {
tapCommand := "tap"
defaultCmdArgs := getDefaultCommandArgs()
return append([]string{tapCommand, regex}, defaultCmdArgs...)
}
func getDefaultTapNamespace() []string {
return []string{"-n", "mizu-tests"}
}
func getDefaultFetchCommandArgs() []string {
fetchCommand := "fetch"
defaultCmdArgs := getDefaultCommandArgs()
return append([]string{fetchCommand}, defaultCmdArgs...)
}
func getDefaultConfigCommandArgs() []string {
configCommand := "config"
defaultCmdArgs := getDefaultCommandArgs()
return append([]string{configCommand}, defaultCmdArgs...)
}
func retriesExecute(retriesCount int, executeFunc func() error) error {
var lastError error
for i := 0; i < retriesCount; i++ {
if err := executeFunc(); err != nil {
lastError = err
time.Sleep(1 * time.Second)
continue
}
return nil
}
return fmt.Errorf("reached max retries count, retries count: %v, last err: %v", retriesCount, lastError)
}
func waitTapPodsReady(apiServerUrl string) error {
resolvingUrl := fmt.Sprintf("%v/status/tappersCount", apiServerUrl)
tapPodsReadyFunc := func() error {
requestResult, requestErr := executeHttpGetRequest(resolvingUrl)
if requestErr != nil {
return requestErr
}
tappersCount := requestResult.(float64)
if tappersCount == 0 {
return fmt.Errorf("no tappers running")
}
return nil
}
return retriesExecute(longRetriesCount, tapPodsReadyFunc)
}
func jsonBytesToInterface(jsonBytes []byte) (interface{}, error) {
var result interface{}
if parseErr := json.Unmarshal(jsonBytes, &result); parseErr != nil {
return nil, parseErr
}
return result, nil
}
func executeHttpRequest(response *http.Response, requestErr error) (interface{}, error) {
if requestErr != nil {
return nil, requestErr
} else if response.StatusCode != 200 {
return nil, fmt.Errorf("invalid status code %v", response.StatusCode)
}
defer func() { response.Body.Close() }()
data, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
return jsonBytesToInterface(data)
}
func executeHttpGetRequest(url string) (interface{}, error) {
response, requestErr := http.Get(url)
return executeHttpRequest(response, requestErr)
}
func executeHttpPostRequest(url string, body interface{}) (interface{}, error) {
requestBody, jsonErr := json.Marshal(body)
if jsonErr != nil {
return nil, jsonErr
}
response, requestErr := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
return executeHttpRequest(response, requestErr)
}
func cleanupCommand(cmd *exec.Cmd) error {
if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func getEntriesFromHarBytes(harBytes []byte) ([]interface{}, error) {
harInterface, convertErr := jsonBytesToInterface(harBytes)
if convertErr != nil {
return nil, convertErr
}
har := harInterface.(map[string]interface{})
harLog := har["log"].(map[string]interface{})
harEntries := harLog["entries"].([]interface{})
return harEntries, nil
}
func getPods(tapStatusInterface interface{}) ([]map[string]interface{}, error) {
tapStatus := tapStatusInterface.(map[string]interface{})
podsInterface := tapStatus["pods"].([]interface{})
var pods []map[string]interface{}
for _, podInterface := range podsInterface {
pods = append(pods, podInterface.(map[string]interface{}))
}
return pods, nil
}

View File

@@ -1,2 +1,2 @@
test: ## Run agent tests.
@go test ./... -race -coverprofile=coverage.out -covermode=atomic
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic

View File

@@ -26,14 +26,17 @@ var apiServerMode = flag.Bool("api-server", false, "Run in API server mode with
var standaloneMode = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
var apiServerAddress = flag.String("api-server-address", "", "Address of mizu API server")
var namespace = flag.String("namespace", "", "Resolve IPs if they belong to resources in this namespace (default is all)")
var harsReaderMode = flag.Bool("hars-read", false, "Run in hars-read mode")
var harsDir = flag.String("hars-dir", "", "Directory to read hars from")
func main() {
flag.Parse()
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
tapOpts := &tap.TapOpts{HostMode: hostMode}
if !*tapperMode && !*apiServerMode && !*standaloneMode {
panic("One of the flags --tap, --api or --standalone must be provided")
if !*tapperMode && !*apiServerMode && !*standaloneMode && !*harsReaderMode{
panic("One of the flags --tap, --api or --standalone or --hars-read must be provided")
}
if *standaloneMode {
@@ -77,6 +80,13 @@ func main() {
go api.StartReadingEntries(filteredHarChannel, nil)
hostApi(socketHarOutChannel)
} else if *harsReaderMode {
socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000)
filteredHarChannel := make(chan *tap.OutputChannelItem)
go filterHarItems(socketHarOutChannel, filteredHarChannel, getTrafficFilteringOptions())
go api.StartReadingEntries(filteredHarChannel, harsDir)
hostApi(nil)
}
signalChan := make(chan os.Signal, 1)

View File

@@ -28,6 +28,7 @@ func init() {
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
if isTapper {
rlog.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
providers.TapperAdded()
} else {
rlog.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
socketListLock.Lock()
@@ -39,6 +40,7 @@ func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
if isTapper {
rlog.Infof("Websocket event - Tapper disconnected, socket ID: %d", socketId)
providers.TapperRemoved()
} else {
rlog.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
socketListLock.Lock()

View File

@@ -30,3 +30,7 @@ func PostTappedPods(c *gin.Context) {
api.BroadcastToBrowserClients(jsonBytes)
}
}
func GetTappersCount(c *gin.Context) {
c.JSON(http.StatusOK, providers.TappersCount)
}

View File

@@ -18,9 +18,7 @@ func TestEntryAddedCount(t *testing.T) {
tests := []int{1, 5, 10, 100, 500, 1000}
for _, entriesCount := range tests {
t.Run(fmt.Sprintf("EntriesCount%v", entriesCount), func(t *testing.T) {
t.Cleanup(providers.ResetGeneralStats)
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
for i := 0; i < entriesCount; i++ {
providers.EntryAdded()
}
@@ -30,6 +28,8 @@ func TestEntryAddedCount(t *testing.T) {
if entriesStats.EntriesCount != entriesCount {
t.Errorf("unexpected result - expected: %v, actual: %v", entriesCount, entriesStats.EntriesCount)
}
t.Cleanup(providers.ResetGeneralStats)
})
}
}

View File

@@ -4,14 +4,18 @@ import (
"github.com/patrickmn/go-cache"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/tap"
"sync"
"time"
)
const tlsLinkRetainmentTime = time.Minute * 15
var (
TapStatus shared.TapStatus
TappersCount int
TapStatus shared.TapStatus
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
tappersCountLock = sync.Mutex{}
)
func GetAllRecentTLSAddresses() []string {
@@ -26,3 +30,15 @@ func GetAllRecentTLSAddresses() []string {
return recentTLSLinks
}
func TapperAdded() {
tappersCountLock.Lock()
TappersCount++
tappersCountLock.Unlock()
}
func TapperRemoved() {
tappersCountLock.Lock()
TappersCount--
tappersCountLock.Unlock()
}

View File

@@ -9,4 +9,6 @@ func StatusRoutes(ginApp *gin.Engine) {
routeGroup := ginApp.Group("/status")
routeGroup.POST("/tappedPods", controllers.PostTappedPods)
routeGroup.GET("/tappersCount", controllers.GetTappersCount)
}

View File

@@ -158,9 +158,11 @@ func filterJsonBody(bytes []byte) ([]byte, error) {
func filterJsonMap(jsonMap map[string] interface{}) {
for key, value := range jsonMap {
// Do not replace nil values with maskedFieldPlaceholderValue
if value == nil {
return
continue
}
nestedMap, isNested := value.(map[string] interface{})
if isNested {
filterJsonMap(nestedMap)

15
build-agent-ci.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
GCP_PROJECT=up9-docker-hub
REPOSITORY=gcr.io/$GCP_PROJECT
SERVER_NAME=mizu
GIT_BRANCH=ci
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
SEM_VER=${SEM_VER=0.0.0}
DOCKER_TAGGED_BUILD="$DOCKER_REPO:$SEM_VER"
echo "building $DOCKER_TAGGED_BUILD"
docker build -t ${DOCKER_TAGGED_BUILD} --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} .

View File

@@ -1,12 +1,14 @@
#!/bin/bash
set -e
SERVER_NAME=mizu
GCP_PROJECT=up9-docker-hub
REPOSITORY=gcr.io/$GCP_PROJECT
SERVER_NAME=mizu
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
SEM_VER=${SEM_VER=0.0.0}
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
SEM_VER=${SEM_VER=0.0.0}
DOCKER_TAGGED_BUILDS=("$DOCKER_REPO:latest" "$DOCKER_REPO:$SEM_VER")
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
@@ -21,6 +23,6 @@ docker build $DOCKER_TAGS_ARGS --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_
for DOCKER_TAG in "${DOCKER_TAGGED_BUILDS[@]}"
do
echo pushing "$DOCKER_TAG"
docker push "$DOCKER_TAG"
echo pushing "$DOCKER_TAG"
docker push "$DOCKER_TAG"
done

View File

@@ -24,7 +24,7 @@ build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables
build-all: ## Build for all supported platforms.
@echo "Compiling for every OS and Platform"
@mkdir -p bin && echo "SHA256 checksums available for compiled binaries \n\nRun \`shasum -a 256 -c mizu_OS_ARCH.sha256\` to verify\n\n" > bin/README.md
@mkdir -p bin && sed s/_SEM_VER_/$(SEM_VER)/g README.md.TEMPLATE > bin/README.md
@$(MAKE) build GOOS=darwin GOARCH=amd64
@$(MAKE) build GOOS=linux GOARCH=amd64
@# $(MAKE) build GOOS=darwin GOARCH=arm64
@@ -41,4 +41,4 @@ clean: ## Clean all build artifacts.
rm -rf ./bin/*
test: ## Run cli tests.
@go test ./... -race -coverprofile=coverage.out -covermode=atomic
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic

20
cli/README.md.TEMPLATE Normal file
View File

@@ -0,0 +1,20 @@
# Mizu release _SEM_VER_
Download Mizu for your platform
**Mac** (on Intel chip)
```
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_darwin_amd64 && chmod 755 mizu
```
**Linux**
```
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_linux_amd64 && chmod 755 mizu
```
### Checksums
SHA256 checksums available for compiled binaries.
Run `shasum -a 256 -c mizu_OS_ARCH.sha256` to verify.

168
cli/apiserver/provider.go Normal file
View File

@@ -0,0 +1,168 @@
package apiserver
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared"
"io/ioutil"
core "k8s.io/api/core/v1"
"net/http"
"net/url"
"time"
)
type apiServerProvider struct {
url string
isReady bool
}
var Provider = apiServerProvider{}
func (provider *apiServerProvider) InitAndTestConnection(url string, retries int) error {
healthUrl := fmt.Sprintf("%s/", url)
retriesLeft := retries
for retriesLeft > 0 {
if response, err := http.Get(healthUrl); err != nil {
logger.Log.Debugf("[ERROR] failed connecting to api server %v", err)
} else if response.StatusCode != 200 {
logger.Log.Debugf("can't connect to api server yet, response status code %v", response.StatusCode)
} else {
logger.Log.Debugf("connection test to api server passed successfully")
break
}
retriesLeft -= 1
time.Sleep(time.Second)
}
if retriesLeft == 0 {
provider.isReady = false
return fmt.Errorf("couldn't reach the api server after %v retries", retries)
}
provider.url = url
provider.isReady = true
return nil
}
func (provider *apiServerProvider) ReportTappedPods(pods []core.Pod) error {
if !provider.isReady {
return fmt.Errorf("trying to reach api server when not initialized yet")
}
tappedPodsUrl := fmt.Sprintf("%s/status/tappedPods", provider.url)
podInfos := make([]shared.PodInfo, 0)
for _, pod := range pods {
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace})
}
tapStatus := shared.TapStatus{Pods: podInfos}
if jsonValue, err := json.Marshal(tapStatus); err != nil {
return fmt.Errorf("failed Marshal the tapped pods %w", err)
} else {
if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
return fmt.Errorf("failed sending to API server the tapped pods %w", err)
} else if response.StatusCode != 200 {
return fmt.Errorf("failed sending to API server the tapped pods, response status code %v", response.StatusCode)
} else {
logger.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos))
return nil
}
}
}
func (provider *apiServerProvider) RequestAnalysis(analysisDestination string, sleepIntervalSec int) error {
if !provider.isReady {
return fmt.Errorf("trying to reach api server when not initialized yet")
}
urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", provider.url, url.QueryEscape(analysisDestination), sleepIntervalSec)
u, parseErr := url.ParseRequestURI(urlPath)
if parseErr != nil {
logger.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr)
}
logger.Log.Debugf("Analysis url %v", u.String())
if response, requestErr := http.Get(u.String()); requestErr != nil {
return fmt.Errorf("failed to notify agent for analysis, err: %w", requestErr)
} else if response.StatusCode != 200 {
return fmt.Errorf("failed to notify agent for analysis, status code: %v", response.StatusCode)
} else {
logger.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
return nil
}
}
func (provider *apiServerProvider) GetGeneralStats() (map[string]interface{}, error) {
if !provider.isReady {
return nil, fmt.Errorf("trying to reach api server when not initialized yet")
}
generalStatsUrl := fmt.Sprintf("%s/api/generalStats", provider.url)
response, requestErr := http.Get(generalStatsUrl)
if requestErr != nil {
return nil, fmt.Errorf("failed to get general stats for telemetry, err: %w", requestErr)
} else if response.StatusCode != 200 {
return nil, fmt.Errorf("failed to get general stats for telemetry, status code: %v", response.StatusCode)
}
defer func() { _ = response.Body.Close() }()
data, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, fmt.Errorf("failed to read general stats for telemetry, err: %v", readErr)
}
var generalStats map[string]interface{}
if parseErr := json.Unmarshal(data, &generalStats); parseErr != nil {
return nil, fmt.Errorf("failed to parse general stats for telemetry, err: %v", parseErr)
}
return generalStats, nil
}
func (provider *apiServerProvider) GetHars(fromTimestamp int, toTimestamp int) (*zip.Reader, error) {
if !provider.isReady {
return nil, fmt.Errorf("trying to reach api server when not initialized yet")
}
resp, err := http.Get(fmt.Sprintf("%s/api/har?from=%v&to=%v", provider.url, fromTimestamp, toTimestamp))
if err != nil {
return nil, fmt.Errorf("failed getting har from api server %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed reading hars %w", err)
}
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
return nil, fmt.Errorf("failed craeting zip reader %w", err)
}
return zipReader, nil
}
func (provider *apiServerProvider) GetVersion() (string, error) {
if !provider.isReady {
return "", fmt.Errorf("trying to reach api server when not initialized yet")
}
versionUrl, _ := url.Parse(fmt.Sprintf("%s/metadata/version", provider.url))
req := &http.Request{
Method: http.MethodGet,
URL: versionUrl,
}
statusResp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer statusResp.Body.Close()
versionResponse := &shared.VersionResponse{}
if err := json.NewDecoder(statusResp.Body).Decode(&versionResponse); err != nil {
return "", err
}
return versionResponse.SemVer, nil
}

45
cli/cmd/common.go Normal file
View File

@@ -0,0 +1,45 @@
package cmd
import (
"context"
"fmt"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/cli/uiUtils"
"os"
"os/signal"
"syscall"
)
func GetApiServerUrl() string {
return fmt.Sprintf("http://%s", kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort))
}
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
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()
}
}
func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
logger.Log.Debugf("waiting for finish...")
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// block until ctx cancel is called or termination signal is received
select {
case <-ctx.Done():
logger.Log.Debugf("ctx done")
break
case <-sigChan:
logger.Log.Debugf("Got termination signal, canceling execution...")
cancel()
}
}

View File

@@ -2,34 +2,34 @@ package cmd
import (
"fmt"
"github.com/creasty/defaults"
"github.com/spf13/cobra"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/telemetry"
"github.com/up9inc/mizu/cli/uiUtils"
"io/ioutil"
)
var regenerateFile bool
var configCmd = &cobra.Command{
Use: "config",
Short: "Generate config with default values",
RunE: func(cmd *cobra.Command, args []string) error {
go telemetry.ReportRun("config", config.Config)
go telemetry.ReportRun("config", config.Config.Config)
template, err := config.GetConfigWithDefaults()
if err != nil {
logger.Log.Errorf("Failed generating config with defaults %v", err)
return nil
}
if regenerateFile {
if config.Config.Config.Regenerate {
data := []byte(template)
if err := ioutil.WriteFile(config.GetConfigFilePath(), data, 0644); err != nil {
if err := ioutil.WriteFile(config.Config.ConfigFilePath, data, 0644); err != nil {
logger.Log.Errorf("Failed writing config %v", err)
return nil
}
logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.GetConfigFilePath())))
logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath)))
} else {
logger.Log.Debugf("Writing template config.\n%v", template)
fmt.Printf("%v", template)
@@ -40,5 +40,9 @@ var configCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(configCmd)
configCmd.Flags().BoolVarP(&regenerateFile, "regenerate", "r", false, fmt.Sprintf("Regenerate the config file with default values %s", config.GetConfigFilePath()))
defaultConfig := config.ConfigStruct{}
defaults.Set(&defaultConfig)
configCmd.Flags().BoolP(configStructs.RegenerateConfigName, "r", defaultConfig.Config.Regenerate, fmt.Sprintf("Regenerate the config file with default values to path %s or to chosen path using --%s", defaultConfig.ConfigFilePath, config.ConfigFilePathCommandName))
}

View File

@@ -3,10 +3,13 @@ package cmd
import (
"github.com/creasty/defaults"
"github.com/spf13/cobra"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu/version"
"github.com/up9inc/mizu/cli/telemetry"
"github.com/up9inc/mizu/cli/uiUtils"
)
var fetchCmd = &cobra.Command{
@@ -15,7 +18,12 @@ var fetchCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
go telemetry.ReportRun("fetch", config.Config.Fetch)
if isCompatible, err := version.CheckVersionCompatibility(config.Config.Fetch.GuiPort); err != nil {
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl(), 1); err != nil {
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, make sure one running")
return nil
}
if isCompatible, err := version.CheckVersionCompatibility(); err != nil {
return err
} else if !isCompatible {
return nil

View File

@@ -1,96 +1,25 @@
package cmd
import (
"archive/zip"
"bytes"
"fmt"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/uiUtils"
)
func RunMizuFetch() {
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Fetch.GuiPort)
resp, err := http.Get(fmt.Sprintf("http://%s/api/har?from=%v&to=%v", mizuProxiedUrl, config.Config.Fetch.FromTimestamp, config.Config.Fetch.ToTimestamp))
if err != nil {
log.Fatal(err)
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl(), 5); err != nil {
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, check logs")
}
defer func() { _ = resp.Body.Close() }()
body, err := ioutil.ReadAll(resp.Body)
zipReader, err := apiserver.Provider.GetHars(config.Config.Fetch.FromTimestamp, config.Config.Fetch.ToTimestamp)
if err != nil {
log.Fatal(err)
logger.Log.Errorf("Failed fetch data from API server %v", err)
return
}
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
if err != nil {
log.Fatal(err)
if err := fsUtils.Unzip(zipReader, config.Config.Fetch.Directory); err != nil {
logger.Log.Debugf("[ERROR] failed unzip %v", err)
}
_ = Unzip(zipReader, config.Config.Fetch.Directory)
}
func Unzip(reader *zip.Reader, dest string) error {
dest, _ = filepath.Abs(dest)
_ = os.MkdirAll(dest, os.ModePerm)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, f.Name)
// Check for ZipSlip (Directory traversal)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
if f.FileInfo().IsDir() {
_ = os.MkdirAll(path, f.Mode())
} else {
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
logger.Log.Infof("writing HAR file [ %v ]", path)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
logger.Log.Info(" done")
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range reader.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}

View File

@@ -2,42 +2,38 @@ package cmd
import (
"context"
"github.com/creasty/defaults"
"github.com/spf13/cobra"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/telemetry"
"os"
"path"
)
var filePath string
var logsCmd = &cobra.Command{
Use: "logs",
Short: "Create a zip file with logs for Github issue or troubleshoot",
RunE: func(cmd *cobra.Command, args []string) error {
go telemetry.ReportRun("logs", config.Config)
go telemetry.ReportRun("logs", config.Config.Logs)
kubernetesProvider, err := kubernetes.NewProvider(config.Config.View.KubeConfigPath)
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
if err != nil {
logger.Log.Error(err)
return nil
}
ctx, _ := context.WithCancel(context.Background())
if filePath == "" {
pwd, err := os.Getwd()
if err != nil {
logger.Log.Errorf("Failed to get PWD, %v (try using `mizu logs -f <full path dest zip file>)`", err)
return nil
}
filePath = path.Join(pwd, "mizu_logs.zip")
if validationErr := config.Config.Logs.Validate(); validationErr != nil {
return errormessage.FormatError(validationErr)
}
logger.Log.Debugf("Using file path %s", filePath)
if err := fsUtils.DumpLogs(kubernetesProvider, ctx, filePath); err != nil {
logger.Log.Errorf("Failed dump logs %v", err)
logger.Log.Debugf("Using file path %s", config.Config.Logs.FilePath())
if dumpLogsErr := fsUtils.DumpLogs(kubernetesProvider, ctx, config.Config.Logs.FilePath()); dumpLogsErr != nil {
logger.Log.Errorf("Failed dump logs %v", dumpLogsErr)
}
return nil
@@ -46,5 +42,9 @@ var logsCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(logsCmd)
logsCmd.Flags().StringVarP(&filePath, "file", "f", "", "Path for zip file (default current <pwd>\\mizu_logs.zip)")
defaultLogsConfig := configStructs.LogsConfig{}
defaults.Set(&defaultLogsConfig)
logsCmd.Flags().StringP(configStructs.FileLogsName, "f", defaultLogsConfig.FileStr, "Path for zip file (default current <pwd>\\mizu_logs.zip)")
}

View File

@@ -2,11 +2,14 @@ package cmd
import (
"fmt"
"github.com/creasty/defaults"
"github.com/spf13/cobra"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/mizu/version"
"github.com/up9inc/mizu/cli/uiUtils"
)
var rootCmd = &cobra.Command{
@@ -15,24 +18,39 @@ var rootCmd = &cobra.Command{
Long: `A web traffic viewer for kubernetes
Further info is available at https://github.com/up9inc/mizu`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil {
logger.Log.Errorf("Failed to use mizu folder, %v", err)
}
logger.InitLogger()
if err := config.InitConfig(cmd); err != nil {
logger.Log.Fatal(err)
}
return nil
},
}
func init() {
defaultConfig := config.ConfigStruct{}
defaults.Set(&defaultConfig)
rootCmd.PersistentFlags().StringSlice(config.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", config.SetCommandName))
rootCmd.PersistentFlags().String(config.ConfigFilePathCommandName, defaultConfig.ConfigFilePath, fmt.Sprintf("Override config file path using --%s", config.ConfigFilePathCommandName))
}
func printNewVersionIfNeeded(versionChan chan string) {
versionMsg := <-versionChan
if versionMsg != "" {
logger.Log.Infof(uiUtils.Yellow, versionMsg)
}
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the tapCmd.
func Execute() {
if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil {
logger.Log.Errorf("Failed to use mizu folder, %v", err)
}
logger.InitLogger()
versionChan := make(chan string)
defer printNewVersionIfNeeded(versionChan)
go version.CheckNewerVersion(versionChan)
cobra.CheckErr(rootCmd.Execute())
}

View File

@@ -33,10 +33,6 @@ Supported protocols are HTTP and gRPC.`,
return errors.New("unexpected number of arguments")
}
if err := config.Config.Validate(); err != nil {
return errormessage.FormatError(err)
}
if err := config.Config.Tap.Validate(); err != nil {
return errormessage.FormatError(err)
}

View File

@@ -1,36 +1,28 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/mizu/goUtils"
"github.com/up9inc/mizu/cli/mizu/version"
"github.com/up9inc/mizu/cli/telemetry"
"net/http"
"net/url"
"os"
"os/signal"
"path"
"regexp"
"strings"
"syscall"
"time"
"github.com/up9inc/mizu/cli/errormessage"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/cli/mizu/fsUtils"
"github.com/up9inc/mizu/cli/mizu/goUtils"
"github.com/up9inc/mizu/cli/telemetry"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/debounce"
yaml "gopkg.in/yaml.v3"
core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"path"
"regexp"
"strings"
"time"
)
const (
@@ -61,8 +53,7 @@ func RunMizuTap() {
return
}
}
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath)
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
if err != nil {
logger.Log.Error(err)
return
@@ -73,13 +64,21 @@ func RunMizuTap() {
targetNamespaces := getNamespaces(kubernetesProvider)
if config.Config.IsNsRestrictedMode() {
if len(targetNamespaces) != 1 || !mizu.Contains(targetNamespaces, config.Config.MizuResourcesNamespace) {
logger.Log.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+
"You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, config.MizuResourcesNamespaceConfigName)
return
}
}
var namespacesStr string
if !mizu.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
} else {
namespacesStr = "all namespaces"
}
version.CheckNewerVersion()
logger.Log.Infof("Tapping pods in %s", namespacesStr)
if err, _ := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces); err != nil {
@@ -101,13 +100,13 @@ func RunMizuTap() {
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
defer cleanUpMizu(kubernetesProvider)
defer finishMizuExecution(kubernetesProvider)
if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions, mizuValidationRules); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error creating resources: %v", errormessage.FormatError(err)))
return
}
go goUtils.HandleExcWrapper(createProxyToApiServerPod, ctx, kubernetesProvider, cancel)
go goUtils.HandleExcWrapper(watchApiServerPod, ctx, kubernetesProvider, cancel)
go goUtils.HandleExcWrapper(watchPodsForTapping, ctx, kubernetesProvider, targetNamespaces, cancel)
//block until exit signal or error
@@ -181,8 +180,10 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
IsNamespaceRestricted: config.Config.IsNsRestrictedMode(),
MizuApiFilteringOptions: mizuApiFilteringOptions,
MaxEntriesDBSizeBytes: config.Config.Tap.MaxEntriesDBSizeBytes(),
Resources: config.Config.Tap.ApiServerResources,
ImagePullPolicy: config.Config.ImagePullPolicy(),
}
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts, config.Config.Tap.ApiServerResources)
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts)
if err != nil {
return err
}
@@ -238,6 +239,7 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
serviceAccountName,
config.Config.Tap.TapOutgoing(),
config.Config.Tap.TapperResources,
config.Config.ImagePullPolicy(),
); err != nil {
return err
}
@@ -251,23 +253,26 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
return nil
}
func cleanUpMizu(kubernetesProvider *kubernetes.Provider) {
telemetry.ReportAPICalls(config.Config.Tap.GuiPort)
cleanUpMizuResources(kubernetesProvider)
}
func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
func finishMizuExecution(kubernetesProvider *kubernetes.Provider) {
telemetry.ReportAPICalls()
removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
defer cancel()
dumpLogsIfNeeded(kubernetesProvider, removalCtx)
cleanUpMizuResources(kubernetesProvider, removalCtx, cancel)
}
if config.Config.DumpLogs {
mizuDir := mizu.GetMizuFolderPath()
filePath = path.Join(mizuDir, fmt.Sprintf("mizu_logs_%s.zip", time.Now().Format("2006_01_02__15_04_05")))
if err := fsUtils.DumpLogs(kubernetesProvider, removalCtx, filePath); err != nil {
logger.Log.Errorf("Failed dump logs %v", err)
}
func dumpLogsIfNeeded(kubernetesProvider *kubernetes.Provider, removalCtx context.Context) {
if !config.Config.DumpLogs {
return
}
mizuDir := mizu.GetMizuFolderPath()
filePath := path.Join(mizuDir, fmt.Sprintf("mizu_logs_%s.zip", time.Now().Format("2006_01_02__15_04_05")))
if err := fsUtils.DumpLogs(kubernetesProvider, removalCtx, filePath); err != nil {
logger.Log.Errorf("Failed dump logs %v", err)
}
}
func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider, removalCtx context.Context, cancel context.CancelFunc) {
logger.Log.Infof("\nRemoving mizu resources\n")
if !config.Config.IsNsRestrictedMode() {
@@ -332,7 +337,7 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, config.Config.MizuResourcesNamespace); err != nil {
switch {
case ctx.Err() == context.Canceled:
// Do nothing. User interrupted the wait.
logger.Log.Debugf("Do nothing. User interrupted the wait")
case err == wait.ErrWaitTimeout:
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Timeout while removing Namespace %s", config.Config.MizuResourcesNamespace))
default:
@@ -341,29 +346,6 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
}
}
func reportTappedPods() {
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort)
tappedPodsUrl := fmt.Sprintf("http://%s/status/tappedPods", mizuProxiedUrl)
podInfos := make([]shared.PodInfo, 0)
for _, pod := range state.currentlyTappedPods {
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace})
}
tapStatus := shared.TapStatus{Pods: podInfos}
if jsonValue, err := json.Marshal(tapStatus); err != nil {
logger.Log.Debugf("[ERROR] failed Marshal the tapped pods %v", err)
} else {
if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
logger.Log.Debugf("[ERROR] failed sending to API server the tapped pods %v", err)
} else if response.StatusCode != 200 {
logger.Log.Debugf("[ERROR] failed sending to API server the tapped pods, response status code %v", response.StatusCode)
} else {
logger.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos))
}
}
}
func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, targetNamespaces []string, cancel context.CancelFunc) {
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, targetNamespaces, config.Config.Tap.PodRegex())
@@ -379,7 +361,9 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
return
}
reportTappedPods()
if err := apiserver.Provider.ReportTappedPods(state.currentlyTappedPods); err != nil {
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
}
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
if err != nil {
@@ -486,7 +470,7 @@ func getMissingPods(pods1 []core.Pod, pods2 []core.Pod) []core.Pod {
return missingPods
}
func createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.ApiServerPodName))
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, []string{config.Config.MizuResourcesNamespace}, podExactRegex)
isPodReady := false
@@ -512,10 +496,17 @@ func createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernet
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
isPodReady = true
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
logger.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort))
time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready
requestForAnalysis()
reportTappedPods()
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl(), 20); err != nil {
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, check logs")
cancel()
break
}
logger.Log.Infof("Mizu is available at %s\n", GetApiServerUrl())
requestForAnalysisIfNeeded()
if err := apiserver.Provider.ReportTappedPods(state.currentlyTappedPods); err != nil {
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
}
}
case <-timeAfter:
if !isPodReady {
@@ -529,34 +520,12 @@ func createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernet
}
}
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
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()
}
}
func requestForAnalysis() {
func requestForAnalysisIfNeeded() {
if !config.Config.Tap.Analysis {
return
}
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort)
urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(config.Config.Tap.AnalysisDestination), config.Config.Tap.SleepIntervalSec)
u, parseErr := url.ParseRequestURI(urlPath)
if parseErr != nil {
logger.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr)
}
logger.Log.Debugf("Sending get request to %v", u.String())
if response, requestErr := http.Get(u.String()); requestErr != nil {
logger.Log.Errorf("Failed to notify agent for analysis, err: %v", requestErr)
} else if response.StatusCode != 200 {
logger.Log.Errorf("Failed to notify agent for analysis, status code: %v", response.StatusCode)
} else {
logger.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
if err := apiserver.Provider.RequestAnalysis(config.Config.Tap.AnalysisDestination, config.Config.Tap.SleepIntervalSec); err != nil {
logger.Log.Debugf("[Error] failed requesting for analysis %v", err)
}
}
@@ -588,24 +557,11 @@ func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) map[string][]string {
return nodeToTappedPodIPMap
}
func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// block until ctx cancel is called or termination signal is received
select {
case <-ctx.Done():
break
case <-sigChan:
cancel()
}
}
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
if config.Config.Tap.AllNamespaces {
return []string{mizu.K8sAllNamespaces}
} else if len(config.Config.Tap.Namespaces) > 0 {
return config.Config.Tap.Namespaces
return mizu.Unique(config.Config.Tap.Namespaces)
} else {
return []string{kubernetesProvider.CurrentNamespace()}
}

View File

@@ -25,5 +25,4 @@ func init() {
defaults.Set(&defaultViewConfig)
viewCmd.Flags().Uint16P(configStructs.GuiPortViewName, "p", defaultViewConfig.GuiPort, "Provide a custom port for the web interface webserver")
viewCmd.Flags().StringP(configStructs.KubeConfigPathViewName, "k", defaultViewConfig.KubeConfigPath, "Path to kube-config file")
}

View File

@@ -3,16 +3,18 @@ package cmd
import (
"context"
"fmt"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"github.com/up9inc/mizu/cli/mizu/version"
"github.com/up9inc/mizu/cli/uiUtils"
"net/http"
)
func runMizuView() {
kubernetesProvider, err := kubernetes.NewProvider(config.Config.View.KubeConfigPath)
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
if err != nil {
logger.Log.Error(err)
return
@@ -33,18 +35,21 @@ func runMizuView() {
return
}
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.View.GuiPort)
_, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl))
if err == nil {
response, err := http.Get(fmt.Sprintf("%s/", GetApiServerUrl()))
if err == nil && response.StatusCode == 200 {
logger.Log.Infof("Found a running service %s and open port %d", mizu.ApiServerPodName, config.Config.View.GuiPort)
return
}
logger.Log.Debugf("Found service %s, creating k8s proxy", mizu.ApiServerPodName)
logger.Log.Infof("Establishing connection to k8s cluster...")
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
logger.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.View.GuiPort))
if isCompatible, err := version.CheckVersionCompatibility(config.Config.View.GuiPort); err != nil {
if err := apiserver.Provider.InitAndTestConnection(GetApiServerUrl(), 10); err != nil {
logger.Log.Errorf(uiUtils.Error, "Couldn't connect to API server, check logs")
return
}
logger.Log.Infof("Mizu is available at %s\n", GetApiServerUrl())
if isCompatible, err := version.CheckVersionCompatibility(); err != nil {
logger.Log.Errorf("Failed to check versions compatibility %v", err)
cancel()
return

View File

@@ -3,12 +3,10 @@ package config
import (
"errors"
"fmt"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"io/ioutil"
"os"
"path"
"reflect"
"strconv"
"strings"
@@ -28,21 +26,10 @@ const (
)
var (
Config = ConfigStruct{}
Config = ConfigStruct{}
cmdName string
)
func (config *ConfigStruct) Validate() error {
if config.IsNsRestrictedMode() {
if config.Tap.AllNamespaces || len(config.Tap.Namespaces) != 1 || config.Tap.Namespaces[0] != config.MizuResourcesNamespace {
return fmt.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+
"You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, MizuResourcesNamespaceConfigName)
}
}
return nil
}
func InitConfig(cmd *cobra.Command) error {
cmdName = cmd.Name()
@@ -50,9 +37,11 @@ func InitConfig(cmd *cobra.Command) error {
return err
}
if err := mergeConfigFile(); err != nil {
return fmt.Errorf("invalid config %w\n"+
"you can regenerate the file using `mizu config -r` or just remove it %v", err, GetConfigFilePath())
configFilePath := cmd.Flags().Lookup(ConfigFilePathCommandName).Value.String()
if err := mergeConfigFile(configFilePath); err != nil {
return fmt.Errorf("invalid config, %w\n"+
"you can regenerate the file by removing it (%v) and using `mizu config -r`", err, configFilePath)
}
cmd.Flags().Visit(initFlag)
@@ -75,12 +64,8 @@ func GetConfigWithDefaults() (string, error) {
return uiUtils.PrettyYaml(defaultConf)
}
func GetConfigFilePath() string {
return path.Join(mizu.GetMizuFolderPath(), "config.yaml")
}
func mergeConfigFile() error {
reader, openErr := os.Open(GetConfigFilePath())
func mergeConfigFile(configFilePath string) error {
reader, openErr := os.Open(configFilePath)
if openErr != nil {
return nil
}
@@ -101,7 +86,12 @@ func mergeConfigFile() error {
func initFlag(f *pflag.Flag) {
configElemValue := reflect.ValueOf(&Config).Elem()
flagPath := []string {cmdName, f.Name}
var flagPath []string
if mizu.Contains([]string{ConfigFilePathCommandName}, f.Name) {
flagPath = []string{f.Name}
} else {
flagPath = []string{cmdName, f.Name}
}
sliceValue, isSliceValue := f.Value.(pflag.SliceValue)
if !isSliceValue {

View File

@@ -4,10 +4,16 @@ import (
"fmt"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/mizu"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/util/homedir"
"os"
"path"
"path/filepath"
)
const (
MizuResourcesNamespaceConfigName = "mizu-resources-namespace"
ConfigFilePathCommandName = "config-path"
)
type ConfigStruct struct {
@@ -15,17 +21,40 @@ type ConfigStruct struct {
Fetch configStructs.FetchConfig `yaml:"fetch"`
Version configStructs.VersionConfig `yaml:"version"`
View configStructs.ViewConfig `yaml:"view"`
Logs configStructs.LogsConfig `yaml:"logs"`
Config configStructs.ConfigConfig `yaml:"config,omitempty"`
AgentImage string `yaml:"agent-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"`
DumpLogs bool `yaml:"dump-logs" default:"false"`
KubeConfigPath string `yaml:"kube-config-path" default:""`
KubeConfigPathStr string `yaml:"kube-config-path"`
ConfigFilePath string `yaml:"config-path,omitempty" readonly:""`
}
func (config *ConfigStruct) SetDefaults() {
config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer)
config.ConfigFilePath = path.Join(mizu.GetMizuFolderPath(), "config.yaml")
}
func (config *ConfigStruct) ImagePullPolicy() v1.PullPolicy {
return v1.PullPolicy(config.ImagePullPolicyStr)
}
func (config *ConfigStruct) IsNsRestrictedMode() bool {
return config.MizuResourcesNamespace != "mizu" // Notice "mizu" string must match the default MizuResourcesNamespace
}
func (config *ConfigStruct) KubeConfigPath() string {
if config.KubeConfigPathStr != "" {
return config.KubeConfigPathStr
}
envKubeConfigPath := os.Getenv("KUBECONFIG")
if envKubeConfigPath != "" {
return envKubeConfigPath
}
home := homedir.HomeDir()
return filepath.Join(home, ".kube", "config")
}

View File

@@ -0,0 +1,9 @@
package configStructs
const (
RegenerateConfigName = "regenerate"
)
type ConfigConfig struct {
Regenerate bool `yaml:"regenerate,omitempty" default:"false" readonly:""`
}

View File

@@ -0,0 +1,35 @@
package configStructs
import (
"fmt"
"os"
"path"
)
const (
FileLogsName = "file"
)
type LogsConfig struct {
FileStr string `yaml:"file"`
}
func (config *LogsConfig) Validate() error {
if config.FileStr == "" {
_, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get PWD, %v (try using `mizu logs -f <full path dest zip file>)`", err)
}
}
return nil
}
func (config *LogsConfig) FilePath() string {
if config.FileStr == "" {
pwd, _ := os.Getwd()
return path.Join(pwd, "mizu_logs.zip")
}
return config.FileStr
}

View File

@@ -1,11 +1,9 @@
package configStructs
const (
GuiPortViewName = "gui-port"
KubeConfigPathViewName = "kube-config"
GuiPortViewName = "gui-port"
)
type ViewConfig struct {
GuiPort uint16 `yaml:"gui-port" default:"8899"`
KubeConfigPath string `yaml:"kube-config"`
GuiPort uint16 `yaml:"gui-port" default:"8899"`
}

View File

@@ -1,6 +1,7 @@
package config
import (
"fmt"
"reflect"
"testing"
)
@@ -22,186 +23,226 @@ type SectionMock struct {
Test string `yaml:"test"`
}
type FieldSetValues struct {
SetValues []string
FieldName string
FieldValue interface{}
}
func TestMergeSetFlagNoSeparator(t *testing.T) {
tests := [][]string{{""}, {"t"}, {"", "t"}, {"t", "test", "test:true"}, {"test", "test:true", "testing!", "true"}}
tests := []struct {
Name string
SetValues []string
}{
{Name: "empty value", SetValues: []string{""}},
{Name: "single char", SetValues: []string{"t"}},
{Name: "combine empty value and single char", SetValues: []string{"", "t"}},
{Name: "two values without separator", SetValues: []string{"test", "test:true"}},
{Name: "four values without separator", SetValues: []string{"test", "test:true", "testing!", "true"}},
}
for _, setValues := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
err := mergeSetFlag(configMockElemValue, setValues)
err := mergeSetFlag(configMockElemValue, test.SetValues)
if err == nil {
t.Errorf("unexpected unhandled error - setValues: %v", setValues)
continue
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected value with not default value - setValues: %v", setValues)
if err == nil {
t.Errorf("unexpected unhandled error - SetValues: %v", test.SetValues)
return
}
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected value with not default value - SetValues: %v", test.SetValues)
}
}
})
}
}
func TestMergeSetFlagInvalidFlagName(t *testing.T) {
tests := [][]string{{"invalid_flag=true"}, {"section.invalid_flag=test"}, {"section=test"}, {"=true"}, {"invalid_flag=true", "config.invalid_flag=test", "section=test", "=true"}}
tests := []struct {
Name string
SetValues []string
}{
{Name: "invalid flag name", SetValues: []string{"invalid_flag=true"}},
{Name: "invalid flag name inside section struct", SetValues: []string{"section.invalid_flag=test"}},
{Name: "flag name is a struct", SetValues: []string{"section=test"}},
{Name: "empty flag name", SetValues: []string{"=true"}},
{Name: "four tests combined", SetValues: []string{"invalid_flag=true", "config.invalid_flag=test", "section=test", "=true"}},
}
for _, setValues := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
err := mergeSetFlag(configMockElemValue, setValues)
err := mergeSetFlag(configMockElemValue, test.SetValues)
if err == nil {
t.Errorf("unexpected unhandled error - setValues: %v", setValues)
continue
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected case - setValues: %v", setValues)
if err == nil {
t.Errorf("unexpected unhandled error - SetValues: %v", test.SetValues)
return
}
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected case - SetValues: %v", test.SetValues)
}
}
})
}
}
func TestMergeSetFlagInvalidFlagValue(t *testing.T) {
tests := [][]string{{"int-field=true"}, {"bool-field:5"}, {"uint-field=-1"}, {"int-slice-field=true"}, {"bool-slice-field=5"}, {"uint-slice-field=-1"}, {"int-field=6", "int-field=66"}}
tests := []struct {
Name string
SetValues []string
}{
{Name: "bool value to int field", SetValues: []string{"int-field=true"}},
{Name: "int value to bool field", SetValues: []string{"bool-field:5"}},
{Name: "int value to uint field", SetValues: []string{"uint-field=-1"}},
{Name: "bool value to int slice field", SetValues: []string{"int-slice-field=true"}},
{Name: "int value to bool slice field", SetValues: []string{"bool-slice-field=5"}},
{Name: "int value to uint slice field", SetValues: []string{"uint-slice-field=-1"}},
{Name: "int slice value to int field", SetValues: []string{"int-field=6", "int-field=66"}},
}
for _, setValues := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
err := mergeSetFlag(configMockElemValue, setValues)
err := mergeSetFlag(configMockElemValue, test.SetValues)
if err == nil {
t.Errorf("unexpected unhandled error - setValues: %v", setValues)
continue
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected case - setValues: %v", setValues)
if err == nil {
t.Errorf("unexpected unhandled error - SetValues: %v", test.SetValues)
return
}
}
for i := 0; i < configMockElemValue.NumField(); i++ {
currentField := configMockElemValue.Type().Field(i)
currentFieldByName := configMockElemValue.FieldByName(currentField.Name)
if !currentFieldByName.IsZero() {
t.Errorf("unexpected case - SetValues: %v", test.SetValues)
}
}
})
}
}
func TestMergeSetFlagNotSliceValues(t *testing.T) {
tests := [][]struct {
SetValue string
FieldName string
FieldValue interface{}
tests := []struct {
Name string
FieldsSetValues []FieldSetValues
}{
{{SetValue: "string-field=test", FieldName: "StringField", FieldValue: "test"}},
{{SetValue: "int-field=6", FieldName: "IntField", FieldValue: 6}},
{{SetValue: "bool-field=true", FieldName: "BoolField", FieldValue: true}},
{{SetValue: "uint-field=6", FieldName: "UintField", FieldValue: uint(6)}},
{
{SetValue: "string-field=test", FieldName: "StringField", FieldValue: "test"},
{SetValue: "int-field=6", FieldName: "IntField", FieldValue: 6},
{SetValue: "bool-field=true", FieldName: "BoolField", FieldValue: true},
{SetValue: "uint-field=6", FieldName: "UintField", FieldValue: uint(6)},
},
{Name: "string field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"string-field=test"}, FieldName: "StringField", FieldValue: "test"}}},
{Name: "int field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6}}},
{Name: "bool field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true}}},
{Name: "uint field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"uint-field=6"}, FieldName: "UintField", FieldValue: uint(6)}}},
{Name: "four fields combined", FieldsSetValues: []FieldSetValues {
{SetValues: []string{"string-field=test"}, FieldName: "StringField", FieldValue: "test"},
{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6},
{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true},
{SetValues: []string{"uint-field=6"}, FieldName: "UintField", FieldValue: uint(6)},
}},
}
for _, test := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
var setValues []string
for _, setValueInfo := range test {
setValues = append(setValues, setValueInfo.SetValue)
}
err := mergeSetFlag(configMockElemValue, setValues)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
continue
}
for _, setValueInfo := range test {
fieldValue := configMockElemValue.FieldByName(setValueInfo.FieldName).Interface()
if fieldValue != setValueInfo.FieldValue {
t.Errorf("unexpected result - expected: %v, actual: %v", setValueInfo.FieldValue, fieldValue)
var setValues []string
for _, fieldSetValues := range test.FieldsSetValues {
setValues = append(setValues, fieldSetValues.SetValues...)
}
}
err := mergeSetFlag(configMockElemValue, setValues)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
return
}
for _, fieldSetValues := range test.FieldsSetValues {
fieldValue := configMockElemValue.FieldByName(fieldSetValues.FieldName).Interface()
if fieldValue != fieldSetValues.FieldValue {
t.Errorf("unexpected result - expected: %v, actual: %v", fieldSetValues.FieldValue, fieldValue)
}
}
})
}
}
func TestMergeSetFlagSliceValues(t *testing.T) {
tests := [][]struct {
SetValues []string
FieldName string
FieldValue interface{}
tests := []struct {
Name string
FieldsSetValues []FieldSetValues
}{
{{SetValues: []string{"string-slice-field=test"}, FieldName: "StringSliceField", FieldValue: []string{"test"}}},
{{SetValues: []string{"int-slice-field=6"}, FieldName: "IntSliceField", FieldValue: []int{6}}},
{{SetValues: []string{"bool-slice-field=true"}, FieldName: "BoolSliceField", FieldValue: []bool{true}}},
{{SetValues: []string{"uint-slice-field=6"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6)}}},
{
{Name: "string slice field single value", FieldsSetValues: []FieldSetValues{{SetValues: []string{"string-slice-field=test"}, FieldName: "StringSliceField", FieldValue: []string{"test"}}}},
{Name: "int slice field single value", FieldsSetValues: []FieldSetValues{{SetValues: []string{"int-slice-field=6"}, FieldName: "IntSliceField", FieldValue: []int{6}}}},
{Name: "bool slice field single value", FieldsSetValues: []FieldSetValues{{SetValues: []string{"bool-slice-field=true"}, FieldName: "BoolSliceField", FieldValue: []bool{true}}}},
{Name: "uint slice field single value", FieldsSetValues: []FieldSetValues{{SetValues: []string{"uint-slice-field=6"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6)}}}},
{Name: "four single value fields combined", FieldsSetValues: []FieldSetValues{
{SetValues: []string{"string-slice-field=test"}, FieldName: "StringSliceField", FieldValue: []string{"test"}},
{SetValues: []string{"int-slice-field=6"}, FieldName: "IntSliceField", FieldValue: []int{6}},
{SetValues: []string{"bool-slice-field=true"}, FieldName: "BoolSliceField", FieldValue: []bool{true}},
{SetValues: []string{"uint-slice-field=6"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6)}},
},
{{SetValues: []string{"string-slice-field=test", "string-slice-field=test2"}, FieldName: "StringSliceField", FieldValue: []string{"test", "test2"}}},
{{SetValues: []string{"int-slice-field=6", "int-slice-field=66"}, FieldName: "IntSliceField", FieldValue: []int{6, 66}}},
{{SetValues: []string{"bool-slice-field=true", "bool-slice-field=false"}, FieldName: "BoolSliceField", FieldValue: []bool{true, false}}},
{{SetValues: []string{"uint-slice-field=6", "uint-slice-field=66"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6), uint(66)}}},
{
}},
{Name: "string slice field two values", FieldsSetValues: []FieldSetValues{{SetValues: []string{"string-slice-field=test", "string-slice-field=test2"}, FieldName: "StringSliceField", FieldValue: []string{"test", "test2"}}}},
{Name: "int slice field two values", FieldsSetValues: []FieldSetValues{{SetValues: []string{"int-slice-field=6", "int-slice-field=66"}, FieldName: "IntSliceField", FieldValue: []int{6, 66}}}},
{Name: "bool slice field two values", FieldsSetValues: []FieldSetValues{{SetValues: []string{"bool-slice-field=true", "bool-slice-field=false"}, FieldName: "BoolSliceField", FieldValue: []bool{true, false}}}},
{Name: "uint slice field two values", FieldsSetValues: []FieldSetValues{{SetValues: []string{"uint-slice-field=6", "uint-slice-field=66"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6), uint(66)}}}},
{Name: "four two values fields combined", FieldsSetValues: []FieldSetValues{
{SetValues: []string{"string-slice-field=test", "string-slice-field=test2"}, FieldName: "StringSliceField", FieldValue: []string{"test", "test2"}},
{SetValues: []string{"int-slice-field=6", "int-slice-field=66"}, FieldName: "IntSliceField", FieldValue: []int{6, 66}},
{SetValues: []string{"bool-slice-field=true", "bool-slice-field=false"}, FieldName: "BoolSliceField", FieldValue: []bool{true, false}},
{SetValues: []string{"uint-slice-field=6", "uint-slice-field=66"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6), uint(66)}},
},
}},
}
for _, test := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
var setValues []string
for _, setValueInfo := range test {
for _, setValue := range setValueInfo.SetValues {
setValues = append(setValues, setValue)
var setValues []string
for _, fieldSetValues := range test.FieldsSetValues {
setValues = append(setValues, fieldSetValues.SetValues...)
}
}
err := mergeSetFlag(configMockElemValue, setValues)
err := mergeSetFlag(configMockElemValue, setValues)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
continue
}
for _, setValueInfo := range test {
fieldValue := configMockElemValue.FieldByName(setValueInfo.FieldName).Interface()
if !reflect.DeepEqual(fieldValue, setValueInfo.FieldValue) {
t.Errorf("unexpected result - expected: %v, actual: %v", setValueInfo.FieldValue, fieldValue)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
return
}
}
for _, fieldSetValues := range test.FieldsSetValues {
fieldValue := configMockElemValue.FieldByName(fieldSetValues.FieldName).Interface()
if !reflect.DeepEqual(fieldValue, fieldSetValues.FieldValue) {
t.Errorf("unexpected result - expected: %v, actual: %v", fieldSetValues.FieldValue, fieldValue)
}
}
})
}
}
func TestMergeSetFlagMixValues(t *testing.T) {
tests := [][]struct {
SetValues []string
FieldName string
FieldValue interface{}
tests := []struct {
Name string
FieldsSetValues []FieldSetValues
}{
{
{Name: "single value all fields", FieldsSetValues: []FieldSetValues{
{SetValues: []string{"string-slice-field=test"}, FieldName: "StringSliceField", FieldValue: []string{"test"}},
{SetValues: []string{"int-slice-field=6"}, FieldName: "IntSliceField", FieldValue: []int{6}},
{SetValues: []string{"bool-slice-field=true"}, FieldName: "BoolSliceField", FieldValue: []bool{true}},
@@ -210,8 +251,8 @@ func TestMergeSetFlagMixValues(t *testing.T) {
{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6},
{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true},
{SetValues: []string{"uint-field=6"}, FieldName: "UintField", FieldValue: uint(6)},
},
{
}},
{Name: "two values slice fields and single value fields", FieldsSetValues: []FieldSetValues{
{SetValues: []string{"string-slice-field=test", "string-slice-field=test2"}, FieldName: "StringSliceField", FieldValue: []string{"test", "test2"}},
{SetValues: []string{"int-slice-field=6", "int-slice-field=66"}, FieldName: "IntSliceField", FieldValue: []int{6, 66}},
{SetValues: []string{"bool-slice-field=true", "bool-slice-field=false"}, FieldName: "BoolSliceField", FieldValue: []bool{true, false}},
@@ -220,33 +261,33 @@ func TestMergeSetFlagMixValues(t *testing.T) {
{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6},
{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true},
{SetValues: []string{"uint-field=6"}, FieldName: "UintField", FieldValue: uint(6)},
},
}},
}
for _, test := range tests {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
t.Run(test.Name, func(t *testing.T) {
configMock := ConfigMock{}
configMockElemValue := reflect.ValueOf(&configMock).Elem()
var setValues []string
for _, setValueInfo := range test {
for _, setValue := range setValueInfo.SetValues {
setValues = append(setValues, setValue)
var setValues []string
for _, fieldSetValues := range test.FieldsSetValues {
setValues = append(setValues, fieldSetValues.SetValues...)
}
}
err := mergeSetFlag(configMockElemValue, setValues)
err := mergeSetFlag(configMockElemValue, setValues)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
continue
}
for _, setValueInfo := range test {
fieldValue := configMockElemValue.FieldByName(setValueInfo.FieldName).Interface()
if !reflect.DeepEqual(fieldValue, setValueInfo.FieldValue) {
t.Errorf("unexpected result - expected: %v, actual: %v", setValueInfo.FieldValue, fieldValue)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
return
}
}
for _, fieldSetValues := range test.FieldsSetValues {
fieldValue := configMockElemValue.FieldByName(fieldSetValues.FieldName).Interface()
if !reflect.DeepEqual(fieldValue, fieldSetValues.FieldValue) {
t.Errorf("unexpected result - expected: %v, actual: %v", fieldSetValues.FieldValue, fieldValue)
}
}
})
}
}
@@ -283,16 +324,18 @@ func TestGetParsedValueValidValue(t *testing.T) {
}
for _, test := range tests {
parsedValue, err := getParsedValue(test.Kind, test.StringValue)
t.Run(fmt.Sprintf("%v %v", test.Kind, test.StringValue), func(t *testing.T) {
parsedValue, err := getParsedValue(test.Kind, test.StringValue)
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
continue
}
if err != nil {
t.Errorf("unexpected error result - err: %v", err)
return
}
if parsedValue.Interface() != test.ActualValue {
t.Errorf("unexpected result - expected: %v, actual: %v", test.ActualValue, parsedValue)
}
if parsedValue.Interface() != test.ActualValue {
t.Errorf("unexpected result - expected: %v, actual: %v", test.ActualValue, parsedValue)
}
})
}
}
@@ -326,15 +369,17 @@ func TestGetParsedValueInvalidValue(t *testing.T) {
}
for _, test := range tests {
parsedValue, err := getParsedValue(test.Kind, test.StringValue)
t.Run(fmt.Sprintf("%v %v", test.Kind, test.StringValue), func(t *testing.T) {
parsedValue, err := getParsedValue(test.Kind, test.StringValue)
if err == nil {
t.Errorf("unexpected unhandled error - stringValue: %v, Kind: %v", test.StringValue, test.Kind)
continue
}
if err == nil {
t.Errorf("unexpected unhandled error - stringValue: %v, Kind: %v", test.StringValue, test.Kind)
return
}
if parsedValue != reflect.ValueOf(nil) {
t.Errorf("unexpected parsed value - parsedValue: %v", parsedValue)
}
if parsedValue != reflect.ValueOf(nil) {
t.Errorf("unexpected parsed value - parsedValue: %v", parsedValue)
}
})
}
}

View File

@@ -1,6 +1,7 @@
package config_test
import (
"fmt"
"github.com/up9inc/mizu/cli/config"
"reflect"
"strings"
@@ -15,9 +16,12 @@ func TestConfigWriteIgnoresReadonlyFields(t *testing.T) {
configWithDefaults, _ := config.GetConfigWithDefaults()
for _, readonlyField := range readonlyFields {
if strings.Contains(configWithDefaults, readonlyField) {
t.Errorf("unexpected result - readonly field: %v, config: %v", readonlyField, configWithDefaults)
}
t.Run(readonlyField, func(t *testing.T) {
readonlyFieldToCheck := fmt.Sprintf("\n%s:", readonlyField)
if strings.Contains(configWithDefaults, readonlyFieldToCheck) {
t.Errorf("unexpected result - readonly field: %v, config: %v", readonlyField, configWithDefaults)
}
})
}
}

View File

@@ -4,6 +4,7 @@ go 1.16
require (
github.com/creasty/defaults v1.5.1
github.com/denisbrodbeck/machineid v1.0.1
github.com/google/go-github/v37 v37.0.0
github.com/gorilla/websocket v1.4.2
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7

View File

@@ -88,6 +88,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@@ -412,7 +414,6 @@ github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"github.com/up9inc/mizu/cli/config/configStructs"
"github.com/up9inc/mizu/cli/logger"
"os"
"path/filepath"
"regexp"
"strconv"
@@ -38,7 +37,6 @@ import (
"k8s.io/client-go/tools/clientcmd"
_ "k8s.io/client-go/tools/portforward"
watchtools "k8s.io/client-go/tools/watch"
"k8s.io/client-go/util/homedir"
)
type Provider struct {
@@ -57,13 +55,23 @@ func NewProvider(kubeConfigPath string) (*Provider, error) {
restClientConfig, err := kubernetesConfig.ClientConfig()
if err != nil {
if clientcmd.IsEmptyConfig(err) {
return nil, fmt.Errorf("Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'\n")
return nil, fmt.Errorf("couldn't find the kube config file, or file is empty (%s)\n" +
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
}
if clientcmd.IsConfigurationInvalid(err) {
return nil, fmt.Errorf("Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'\n")
return nil, fmt.Errorf("invalid kube config file (%s)\n" +
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
}
return nil, fmt.Errorf("error while using kube config (%s)\n" +
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
}
clientSet, err := getClientSet(restClientConfig)
if err != nil {
return nil, fmt.Errorf("error while using kube config (%s)\n" +
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
}
clientSet := getClientSet(restClientConfig)
return &Provider{
clientSet: clientSet,
@@ -142,9 +150,11 @@ type ApiServerOptions struct {
IsNamespaceRestricted bool
MizuApiFilteringOptions *shared.TrafficFilteringOptions
MaxEntriesDBSizeBytes int64
Resources configStructs.Resources
ImagePullPolicy core.PullPolicy
}
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions, resources configStructs.Resources) (*core.Pod, error) {
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions) (*core.Pod, error) {
marshaledFilteringOptions, err := json.Marshal(opts.MizuApiFilteringOptions)
if err != nil {
return nil, err
@@ -154,19 +164,19 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiS
configMapOptional := true
configMapVolumeName.Optional = &configMapOptional
cpuLimit, err := resource.ParseQuantity(resources.CpuLimit)
cpuLimit, err := resource.ParseQuantity(opts.Resources.CpuLimit)
if err != nil {
return nil, errors.New(fmt.Sprintf("invalid cpu limit for %s container", opts.PodName))
}
memLimit, err := resource.ParseQuantity(resources.MemoryLimit)
memLimit, err := resource.ParseQuantity(opts.Resources.MemoryLimit)
if err != nil {
return nil, errors.New(fmt.Sprintf("invalid memory limit for %s container", opts.PodName))
}
cpuRequests, err := resource.ParseQuantity(resources.CpuRequests)
cpuRequests, err := resource.ParseQuantity(opts.Resources.CpuRequests)
if err != nil {
return nil, errors.New(fmt.Sprintf("invalid cpu request for %s container", opts.PodName))
}
memRequests, err := resource.ParseQuantity(resources.MemoryRequests)
memRequests, err := resource.ParseQuantity(opts.Resources.MemoryRequests)
if err != nil {
return nil, errors.New(fmt.Sprintf("invalid memory request for %s container", opts.PodName))
}
@@ -187,7 +197,7 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiS
{
Name: opts.PodName,
Image: opts.PodImage,
ImagePullPolicy: core.PullAlways,
ImagePullPolicy: opts.ImagePullPolicy,
VolumeMounts: []core.VolumeMount{
{
Name: mizu.ConfigMapName,
@@ -563,7 +573,7 @@ func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string,
return nil
}
func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeToTappedPodIPMap map[string][]string, serviceAccountName string, tapOutgoing bool, resources configStructs.Resources) error {
func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeToTappedPodIPMap map[string][]string, serviceAccountName string, tapOutgoing bool, resources configStructs.Resources, imagePullPolicy core.PullPolicy) error {
logger.Log.Debugf("Applying %d tapper deamonsets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeToTappedPodIPMap), namespace, daemonSetName, podImage, tapperPodName)
if len(nodeToTappedPodIPMap) == 0 {
@@ -588,7 +598,7 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
agentContainer := applyconfcore.Container()
agentContainer.WithName(tapperPodName)
agentContainer.WithImage(podImage)
agentContainer.WithImagePullPolicy(core.PullAlways)
agentContainer.WithImagePullPolicy(imagePullPolicy)
agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(true))
agentContainer.WithCommand(mizuCmd...)
agentContainer.WithEnv(
@@ -729,24 +739,16 @@ func (provider *Provider) GetPodLogs(namespace string, podName string, ctx conte
return str, nil
}
func getClientSet(config *restclient.Config) *kubernetes.Clientset {
func getClientSet(config *restclient.Config) (*kubernetes.Clientset, error) {
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
return nil, err
}
return clientSet
return clientSet, nil
}
func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig {
if kubeConfigPath == "" {
kubeConfigPath = os.Getenv("KUBECONFIG")
}
if kubeConfigPath == "" {
home := homedir.HomeDir()
kubeConfigPath = filepath.Join(home, ".kube", "config")
}
logger.Log.Debugf("Using kube config %s", kubeConfigPath)
configPathList := filepath.SplitList(kubeConfigPath)
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}

View File

@@ -45,10 +45,10 @@ func DumpLogs(provider *kubernetes.Provider, ctx context.Context, filePath strin
logger.Log.Debugf("Successfully added log length %d from pod: %s.%s", len(logs), pod.Namespace, pod.Name)
}
}
if err := AddFileToZip(zipWriter, config.GetConfigFilePath()); err != nil {
if err := AddFileToZip(zipWriter, config.Config.ConfigFilePath); err != nil {
logger.Log.Debugf("Failed write file, %v", err)
} else {
logger.Log.Debugf("Successfully added file %s", config.GetConfigFilePath())
logger.Log.Debugf("Successfully added file %s", config.Config.ConfigFilePath)
}
if err := AddFileToZip(zipWriter, logger.GetLogFilePath()); err != nil {
logger.Log.Debugf("Failed write file, %v", err)

View File

@@ -3,9 +3,11 @@ package fsUtils
import (
"archive/zip"
"fmt"
"github.com/up9inc/mizu/cli/logger"
"io"
"os"
"path/filepath"
"strings"
)
func AddFileToZip(zipWriter *zip.Writer, filename string) error {
@@ -53,3 +55,60 @@ func AddStrToZip(writer *zip.Writer, logs string, fileName string) error {
}
return nil
}
func Unzip(reader *zip.Reader, dest string) error {
dest, _ = filepath.Abs(dest)
_ = os.MkdirAll(dest, os.ModePerm)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, f.Name)
// Check for ZipSlip (Directory traversal)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
if f.FileInfo().IsDir() {
_ = os.MkdirAll(path, f.Mode())
} else {
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
logger.Log.Infof("writing HAR file [ %v ]", path)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
logger.Log.Info(" done")
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range reader.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}

View File

@@ -9,3 +9,17 @@ func Contains(slice []string, containsValue string) bool {
return false
}
func Unique(slice []string) []string {
keys := make(map[string]bool)
var list []string
for _, entry := range slice {
if _, value := keys[entry]; !value {
keys[entry] = true
list = append(list, entry)
}
}
return list
}

View File

@@ -1,82 +1,130 @@
package mizu_test
import (
"fmt"
"github.com/up9inc/mizu/cli/mizu"
"reflect"
"testing"
)
func TestContainsExists(t *testing.T) {
tests := []struct {
slice []string
containsValue string
expected bool
Slice []string
ContainsValue string
Expected bool
}{
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "apple", expected: true},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "orange", expected: true},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "banana", expected: true},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "grapes", expected: true},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "apple", Expected: true},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "orange", Expected: true},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "banana", Expected: true},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "grapes", Expected: true},
}
for _, test := range tests {
actual := mizu.Contains(test.slice, test.containsValue)
if actual != test.expected {
t.Errorf("unexpected result - expected: %v, actual: %v", test.expected, actual)
}
t.Run(test.ContainsValue, func(t *testing.T) {
actual := mizu.Contains(test.Slice, test.ContainsValue)
if actual != test.Expected {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}
func TestContainsNotExists(t *testing.T) {
tests := []struct {
slice []string
containsValue string
expected bool
Slice []string
ContainsValue string
Expected bool
}{
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "cat", expected: false},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "dog", expected: false},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "apples", expected: false},
{slice: []string{"apple", "orange", "banana", "grapes"}, containsValue: "rapes", expected: false},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "cat", Expected: false},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "dog", Expected: false},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "apples", Expected: false},
{Slice: []string{"apple", "orange", "banana", "grapes"}, ContainsValue: "rapes", Expected: false},
}
for _, test := range tests {
actual := mizu.Contains(test.slice, test.containsValue)
if actual != test.expected {
t.Errorf("unexpected result - expected: %v, actual: %v", test.expected, actual)
}
t.Run(test.ContainsValue, func(t *testing.T) {
actual := mizu.Contains(test.Slice, test.ContainsValue)
if actual != test.Expected {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}
func TestContainsEmptySlice(t *testing.T) {
tests := []struct {
slice []string
containsValue string
expected bool
Slice []string
ContainsValue string
Expected bool
}{
{slice: []string{}, containsValue: "cat", expected: false},
{slice: []string{}, containsValue: "dog", expected: false},
{Slice: []string{}, ContainsValue: "cat", Expected: false},
{Slice: []string{}, ContainsValue: "dog", Expected: false},
}
for _, test := range tests {
actual := mizu.Contains(test.slice, test.containsValue)
if actual != test.expected {
t.Errorf("unexpected result - expected: %v, actual: %v", test.expected, actual)
}
t.Run(test.ContainsValue, func(t *testing.T) {
actual := mizu.Contains(test.Slice, test.ContainsValue)
if actual != test.Expected {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}
func TestContainsNilSlice(t *testing.T) {
tests := []struct {
slice []string
containsValue string
expected bool
Slice []string
ContainsValue string
Expected bool
}{
{slice: nil, containsValue: "cat", expected: false},
{slice: nil, containsValue: "dog", expected: false},
{Slice: nil, ContainsValue: "cat", Expected: false},
{Slice: nil, ContainsValue: "dog", Expected: false},
}
for _, test := range tests {
actual := mizu.Contains(test.slice, test.containsValue)
if actual != test.expected {
t.Errorf("unexpected result - expected: %v, actual: %v", test.expected, actual)
}
t.Run(test.ContainsValue, func(t *testing.T) {
actual := mizu.Contains(test.Slice, test.ContainsValue)
if actual != test.Expected {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}
func TestUniqueNoDuplicateValues(t *testing.T) {
tests := []struct {
Slice []string
Expected []string
}{
{Slice: []string{"apple", "orange", "banana", "grapes"}, Expected: []string{"apple", "orange", "banana", "grapes"}},
{Slice: []string{"dog", "cat", "mouse"}, Expected: []string{"dog", "cat", "mouse"}},
}
for index, test := range tests {
t.Run(fmt.Sprintf("%v", index), func(t *testing.T) {
actual := mizu.Unique(test.Slice)
if !reflect.DeepEqual(test.Expected, actual) {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}
func TestUniqueDuplicateValues(t *testing.T) {
tests := []struct {
Slice []string
Expected []string
}{
{Slice: []string{"apple", "apple", "orange", "orange", "banana", "banana", "grapes", "grapes"}, Expected: []string{"apple", "orange", "banana", "grapes"}},
{Slice: []string{"dog", "cat", "cat", "mouse"}, Expected: []string{"dog", "cat", "mouse"}},
}
for index, test := range tests {
t.Run(fmt.Sprintf("%v", index), func(t *testing.T) {
actual := mizu.Unique(test.Slice)
if !reflect.DeepEqual(test.Expected, actual) {
t.Errorf("unexpected result - Expected: %v, actual: %v", test.Expected, actual)
}
})
}
}

View File

@@ -2,43 +2,21 @@ package version
import (
"context"
"encoding/json"
"fmt"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/google/go-github/v37/github"
"github.com/up9inc/mizu/cli/uiUtils"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/semver"
)
func getApiVersion(port uint16) (string, error) {
versionUrl, _ := url.Parse(fmt.Sprintf("http://localhost:%d/mizu/metadata/version", port))
req := &http.Request{
Method: http.MethodGet,
URL: versionUrl,
}
statusResp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer statusResp.Body.Close()
versionResponse := &shared.VersionResponse{}
if err := json.NewDecoder(statusResp.Body).Decode(&versionResponse); err != nil {
return "", err
}
return versionResponse.SemVer, nil
}
func CheckVersionCompatibility(port uint16) (bool, error) {
apiSemVer, err := getApiVersion(port)
func CheckVersionCompatibility() (bool, error) {
apiSemVer, err := apiserver.Provider.GetVersion()
if err != nil {
return false, err
}
@@ -52,7 +30,7 @@ func CheckVersionCompatibility(port uint16) (bool, error) {
return false, nil
}
func CheckNewerVersion() {
func CheckNewerVersion(versionChan chan string) {
logger.Log.Debugf("Checking for newer version...")
start := time.Now()
client := github.NewClient(nil)
@@ -88,8 +66,13 @@ func CheckNewerVersion() {
}
gitHubVersion := string(data)
gitHubVersion = gitHubVersion[:len(gitHubVersion)-1]
logger.Log.Debugf("Finished version validation, took %v", time.Since(start))
if mizu.SemVer < gitHubVersion {
logger.Log.Infof(uiUtils.Yellow, fmt.Sprintf("Update available! %v -> %v (%v)", mizu.SemVer, gitHubVersion, *latestRelease.HTMLURL))
gitHubVersionSemVer := semver.SemVersion(gitHubVersion)
currentSemVer := semver.SemVersion(mizu.SemVer)
logger.Log.Debugf("Finished version validation, github version %v, current version %v, took %v", gitHubVersion, currentSemVer, time.Since(start))
if gitHubVersionSemVer.GreaterThan(currentSemVer) {
versionChan <- fmt.Sprintf("Update available! %v -> %v (%v)", mizu.SemVer, gitHubVersion, *latestRelease.HTMLURL)
}
versionChan <- ""
}

View File

@@ -4,11 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/denisbrodbeck/machineid"
"github.com/up9inc/mizu/cli/apiserver"
"github.com/up9inc/mizu/cli/config"
"github.com/up9inc/mizu/cli/kubernetes"
"github.com/up9inc/mizu/cli/logger"
"github.com/up9inc/mizu/cli/mizu"
"io/ioutil"
"net/http"
)
@@ -34,35 +34,15 @@ func ReportRun(cmd string, args interface{}) {
logger.Log.Debugf("successfully reported telemetry for cmd %v", cmd)
}
func ReportAPICalls(mizuPort uint16) {
func ReportAPICalls() {
if !shouldRunTelemetry() {
logger.Log.Debugf("not reporting telemetry")
return
}
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizuPort)
generalStatsUrl := fmt.Sprintf("http://%s/api/generalStats", mizuProxiedUrl)
response, requestErr := http.Get(generalStatsUrl)
if requestErr != nil {
logger.Log.Debugf("ERROR: failed to get general stats for telemetry, err: %v", requestErr)
return
} else if response.StatusCode != 200 {
logger.Log.Debugf("ERROR: failed to get general stats for telemetry, status code: %v", response.StatusCode)
return
}
defer func() { _ = response.Body.Close() }()
data, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
logger.Log.Debugf("ERROR: failed to read general stats for telemetry, err: %v", readErr)
return
}
var generalStats map[string]interface{}
if parseErr := json.Unmarshal(data, &generalStats); parseErr != nil {
logger.Log.Debugf("ERROR: failed to parse general stats for telemetry, err: %v", parseErr)
generalStats, err := apiserver.Provider.GetGeneralStats()
if err != nil {
logger.Log.Debugf("[ERROR] failed get general stats from api server %v", err)
return
}
@@ -99,6 +79,10 @@ func sendTelemetry(telemetryType string, argsMap map[string]interface{}) error {
argsMap["branch"] = mizu.Branch
argsMap["version"] = mizu.SemVer
if machineId, err := machineid.ProtectedID("mizu"); err == nil {
argsMap["machineId"] = machineId
}
jsonValue, _ := json.Marshal(argsMap)
if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {

8
codecov.yml Normal file
View File

@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
threshold: 1%
patch:
default:
enabled: no

View File

@@ -26,3 +26,23 @@ func (v SemVersion) Patch() string {
_, _, patch := v.Breakdown()
return patch
}
func (v SemVersion) GreaterThan(v2 SemVersion) bool {
if v.Major() > v2.Major() {
return true
} else if v.Major() < v2.Major() {
return false
}
if v.Minor() > v2.Minor() {
return true
} else if v.Minor() < v2.Minor() {
return false
}
if v.Patch() > v2.Patch() {
return true
}
return false
}

View File

@@ -79,7 +79,8 @@ func (h *httpReader) Read(p []byte) (int, error) {
clientHello := tlsx.ClientHello{}
err := clientHello.Unmarshall(msg.bytes)
if err == nil {
fmt.Printf("Detected TLS client hello with SNI %s\n", clientHello.SNI)
statsTracker.incTlsConnectionsCount()
Debug("Detected TLS client hello with SNI %s\n", clientHello.SNI)
numericPort, _ := strconv.Atoi(h.tcpID.dstPort)
h.outboundLinkWriter.WriteOutboundLink(h.tcpID.srcIP, h.tcpID.dstIP, numericPort, clientHello.SNI, TLSProtocol)
}
@@ -176,7 +177,7 @@ func (h *httpReader) handleHTTP2Stream() error {
}
if reqResPair != nil {
statsTracker.incMatchedMessages()
statsTracker.incMatchedPairs()
if h.harWriter != nil {
h.harWriter.WritePair(
@@ -215,7 +216,7 @@ func (h *httpReader) handleHTTP1ClientStream(b *bufio.Reader) error {
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.srcIP, h.tcpID.dstIP, h.tcpID.srcPort, h.tcpID.dstPort, h.messageCount)
reqResPair := reqResMatcher.registerRequest(ident, req, h.captureTime)
if reqResPair != nil {
statsTracker.incMatchedMessages()
statsTracker.incMatchedPairs()
if h.harWriter != nil {
h.harWriter.WritePair(
@@ -281,7 +282,7 @@ func (h *httpReader) handleHTTP1ServerStream(b *bufio.Reader) error {
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.dstIP, h.tcpID.srcIP, h.tcpID.dstPort, h.tcpID.srcPort, h.messageCount)
reqResPair := reqResMatcher.registerResponse(ident, res, h.captureTime)
if reqResPair != nil {
statsTracker.incMatchedMessages()
statsTracker.incMatchedPairs()
if h.harWriter != nil {
h.harWriter.WritePair(

View File

@@ -10,9 +10,9 @@ package tap
import (
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"github.com/romana/rlog"
"log"
"os"
"os/signal"
@@ -23,6 +23,8 @@ import (
"sync"
"time"
"github.com/romana/rlog"
"github.com/google/gopacket"
"github.com/google/gopacket/examples/util"
"github.com/google/gopacket/ip4defrag"
@@ -374,9 +376,7 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
errorMapLen := len(errorsMap)
errorsSummery := fmt.Sprintf("%v", errorsMap)
errorsMapMutex.Unlock()
log.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v) - Errors Summary: %s",
statsTracker.appStats.TotalPacketsCount,
statsTracker.appStats.TotalProcessedBytes,
log.Printf("%v (errors: %v, errTypes:%v) - Errors Summary: %s",
time.Since(statsTracker.appStats.StartTime),
nErrors,
errorMapLen,
@@ -395,14 +395,15 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
// Since the last print
cleanStats := cleaner.dumpStats()
matchedMessages := statsTracker.dumpStats()
log.Printf(
"flushed connections %d, closed connections: %d, deleted messages: %d, matched messages: %d",
"cleaner - flushed connections: %d, closed connections: %d, deleted messages: %d",
cleanStats.flushed,
cleanStats.closed,
cleanStats.deleted,
matchedMessages,
)
currentAppStats := statsTracker.dumpStats()
appStatsJSON, _ := json.Marshal(currentAppStats)
log.Printf("app stats - %v", string(appStatsJSON))
}
}()
@@ -414,7 +415,7 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
packetsCount := statsTracker.incPacketsCount()
rlog.Debugf("PACKET #%d", packetsCount)
data := packet.Data()
statsTracker.updateProcessedSize(int64(len(data)))
statsTracker.updateProcessedBytes(int64(len(data)))
if *hexdumppkt {
rlog.Debugf("Packet content (%d/0x%x) - %s", len(data), len(data), hex.Dump(data))
}
@@ -448,6 +449,7 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
tcp := packet.Layer(layers.LayerTypeTCP)
if tcp != nil {
statsTracker.incTcpPacketsCount()
tcp := tcp.(*layers.TCP)
if *checksum {
err := tcp.SetNetworkLayerForChecksum(packet.NetworkLayer())
@@ -465,14 +467,14 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
assemblerMutex.Unlock()
}
done := *maxcount > 0 && statsTracker.appStats.TotalPacketsCount >= *maxcount
done := *maxcount > 0 && statsTracker.appStats.PacketsCount >= *maxcount
if done {
errorsMapMutex.Lock()
errorMapLen := len(errorsMap)
errorsMapMutex.Unlock()
log.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)",
statsTracker.appStats.TotalPacketsCount,
statsTracker.appStats.TotalProcessedBytes,
statsTracker.appStats.PacketsCount,
statsTracker.appStats.ProcessedBytes,
time.Since(statsTracker.appStats.StartTime),
nErrors,
errorMapLen)

View File

@@ -6,50 +6,99 @@ import (
)
type AppStats struct {
StartTime time.Time `json:"startTime"`
MatchedMessages int `json:"matchedMessages"`
TotalPacketsCount int64 `json:"totalPacketsCount"`
TotalProcessedBytes int64 `json:"totalProcessedBytes"`
TotalMatchedMessages int64 `json:"totalMatchedMessages"`
StartTime time.Time `json:"-"`
ProcessedBytes int64 `json:"processedBytes"`
PacketsCount int64 `json:"packetsCount"`
TcpPacketsCount int64 `json:"tcpPacketsCount"`
ReassembledTcpPayloadsCount int64 `json:"reassembledTcpPayloadsCount"`
TlsConnectionsCount int64 `json:"tlsConnectionsCount"`
MatchedPairs int64 `json:"matchedPairs"`
}
type StatsTracker struct {
appStats AppStats
matchedMessagesMutex sync.Mutex
totalPacketsCountMutex sync.Mutex
totalProcessedSizeMutex sync.Mutex
appStats AppStats
processedBytesMutex sync.Mutex
packetsCountMutex sync.Mutex
tcpPacketsCountMutex sync.Mutex
reassembledTcpPayloadsCountMutex sync.Mutex
tlsConnectionsCountMutex sync.Mutex
matchedPairsMutex sync.Mutex
}
func (st *StatsTracker) incMatchedMessages() {
st.matchedMessagesMutex.Lock()
st.appStats.MatchedMessages++
st.appStats.TotalMatchedMessages++
st.matchedMessagesMutex.Unlock()
func (st *StatsTracker) incMatchedPairs() {
st.matchedPairsMutex.Lock()
st.appStats.MatchedPairs++
st.matchedPairsMutex.Unlock()
}
func (st *StatsTracker) incPacketsCount() int64 {
st.totalPacketsCountMutex.Lock()
st.appStats.TotalPacketsCount++
currentPacketsCount := st.appStats.TotalPacketsCount
st.totalPacketsCountMutex.Unlock()
st.packetsCountMutex.Lock()
st.appStats.PacketsCount++
currentPacketsCount := st.appStats.PacketsCount
st.packetsCountMutex.Unlock()
return currentPacketsCount
}
func (st *StatsTracker) updateProcessedSize(size int64) {
st.totalProcessedSizeMutex.Lock()
st.appStats.TotalProcessedBytes += size
st.totalProcessedSizeMutex.Unlock()
func (st *StatsTracker) incTcpPacketsCount() {
st.tcpPacketsCountMutex.Lock()
st.appStats.TcpPacketsCount++
st.tcpPacketsCountMutex.Unlock()
}
func (st *StatsTracker) incReassembledTcpPayloadsCount() {
st.reassembledTcpPayloadsCountMutex.Lock()
st.appStats.ReassembledTcpPayloadsCount++
st.reassembledTcpPayloadsCountMutex.Unlock()
}
func (st *StatsTracker) incTlsConnectionsCount() {
st.tlsConnectionsCountMutex.Lock()
st.appStats.TlsConnectionsCount++
st.tlsConnectionsCountMutex.Unlock()
}
func (st *StatsTracker) updateProcessedBytes(size int64) {
st.processedBytesMutex.Lock()
st.appStats.ProcessedBytes += size
st.processedBytesMutex.Unlock()
}
func (st *StatsTracker) setStartTime(startTime time.Time) {
st.appStats.StartTime = startTime
}
func (st *StatsTracker) dumpStats() int {
st.matchedMessagesMutex.Lock()
matchedMessages := st.appStats.MatchedMessages
st.appStats.MatchedMessages = 0
st.matchedMessagesMutex.Unlock()
func (st *StatsTracker) dumpStats() *AppStats {
currentAppStats := &AppStats{StartTime: st.appStats.StartTime}
return matchedMessages
st.processedBytesMutex.Lock()
currentAppStats.ProcessedBytes = st.appStats.ProcessedBytes
st.appStats.ProcessedBytes = 0
st.processedBytesMutex.Unlock()
st.packetsCountMutex.Lock()
currentAppStats.PacketsCount = st.appStats.PacketsCount
st.appStats.PacketsCount = 0
st.packetsCountMutex.Unlock()
st.tcpPacketsCountMutex.Lock()
currentAppStats.TcpPacketsCount = st.appStats.TcpPacketsCount
st.appStats.TcpPacketsCount = 0
st.tcpPacketsCountMutex.Unlock()
st.reassembledTcpPayloadsCountMutex.Lock()
currentAppStats.ReassembledTcpPayloadsCount = st.appStats.ReassembledTcpPayloadsCount
st.appStats.ReassembledTcpPayloadsCount = 0
st.reassembledTcpPayloadsCountMutex.Unlock()
st.tlsConnectionsCountMutex.Lock()
currentAppStats.TlsConnectionsCount = st.appStats.TlsConnectionsCount
st.appStats.TlsConnectionsCount = 0
st.tlsConnectionsCountMutex.Unlock()
st.matchedPairsMutex.Lock()
currentAppStats.MatchedPairs = st.appStats.MatchedPairs
st.appStats.MatchedPairs = 0
st.matchedPairsMutex.Unlock()
return currentAppStats
}

View File

@@ -148,6 +148,7 @@ func (t *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
}
// This is where we pass the reassembled information onwards
// This channel is read by an httpReader object
statsTracker.incReassembledTcpPayloadsCount()
if dir == reassembly.TCPDirClientToServer && !t.reversed {
t.client.msgQueue <- httpReaderDataMsg{data, ac.GetCaptureInfo().Timestamp}
} else {

View File

@@ -1,4 +1,4 @@
@import 'components/style/variables.module'
@import 'src/variables.module'
.mizuApp
background-color: $main-background-color

View File

@@ -2,8 +2,8 @@ import React, {useEffect, useState} from 'react';
import './App.sass';
import logo from './components/assets/Mizu-logo.svg';
import {Button, Snackbar} from "@material-ui/core";
import {HarPage} from "./components/HarPage";
import Tooltip from "./components/Tooltip";
import {TrafficPage} from "./components/TrafficPage";
import Tooltip from "./components/UI/Tooltip";
import {makeStyles} from "@material-ui/core/styles";
import MuiAlert from '@material-ui/lab/Alert';
import Api from "./helpers/api";
@@ -38,6 +38,7 @@ const App = () => {
}
})();
// eslint-disable-next-line
}, []);
const onTLSDetected = (destAddress: string) => {
@@ -116,7 +117,7 @@ const App = () => {
</Tooltip>
}
</div>
<HarPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
<Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
<MuiAlert elevation={6} variant="filled" onClose={() => setUserDismissedTLSWarning(true)} severity="warning">
Mizu is detecting TLS traffic{addressesWithTLS.size ? ` (directed to ${Array.from(addressesWithTLS).join(", ")})` : ''}, this type of traffic will not be displayed.

View File

@@ -1,16 +1,17 @@
import {HarEntry} from "./HarEntry";
import React, {useCallback, useEffect, useMemo, useState} from "react";
import styles from './style/HarEntriesList.module.sass';
import {EntryItem} from "./EntryListItem/EntryListItem";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import styles from './style/EntriesList.module.sass';
import spinner from './assets/spinner.svg';
import ScrollableFeed from "react-scrollable-feed";
import {StatusType} from "./HarFilters";
import {StatusType} from "./Filters";
import Api from "../helpers/api";
import down from "./assets/downImg.svg";
interface HarEntriesListProps {
entries: any[];
setEntries: (entries: any[]) => void;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
focusedEntry: any;
setFocusedEntry: (entry: any) => void;
connectionOpen: boolean;
noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void;
@@ -19,6 +20,9 @@ interface HarEntriesListProps {
methodsFilter: Array<string>;
statusFilter: Array<string>;
pathFilter: string
listEntryREF: any;
onScrollEvent: (isAtBottom:boolean) => void;
scrollableList: boolean;
}
enum FetchOperator {
@@ -28,11 +32,12 @@ enum FetchOperator {
const api = new Api();
export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter}) => {
export const EntriesList: React.FC<HarEntriesListProps> = ({entries, setEntries, focusedEntry, setFocusedEntry, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter, listEntryREF, onScrollEvent, scrollableList}) => {
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
const scrollableRef = useRef(null);
useEffect(() => {
const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => {
@@ -106,20 +111,25 @@ export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntri
return <>
<div className={styles.list}>
<div id="list" className={styles.list}>
<div id="list" ref={listEntryREF} className={styles.list} >
{isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/>
</div>}
<ScrollableFeed>
<ScrollableFeed ref={scrollableRef} onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}>
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
{filteredEntries.map(entry => <HarEntry key={entry.id}
{filteredEntries.map(entry => <EntryItem key={entry.id}
entry={entry}
setFocusedEntryId={setFocusedEntryId}
isSelected={focusedEntryId === entry.id}/>)}
setFocusedEntry = {setFocusedEntry}
isSelected={focusedEntry.id === entry.id}/>)}
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
</div>}
</ScrollableFeed>
<button type="button"
className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`}
onClick={(_) => scrollableRef.current.scrollToBottom()}>
<img alt="down" src={down} />
</button>
</div>
{entries?.length > 0 && <div className={styles.footer}>

View File

@@ -0,0 +1,23 @@
@import "src/variables.module"
.content
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: calc(100% - 56px)
overflow-y: auto
width: 100%
.body
background: $main-background-color
color: $blue-gray
border-radius: 4px
padding: 10px
.bodyHeader
padding: 0 1rem
.endpointURL
font-size: .75rem
display: block
color: $blue-color
text-decoration: none
margin-bottom: .5rem
overflow-wrap: anywhere
padding: 5px 0

View File

@@ -0,0 +1,56 @@
import React from "react";
import styles from './EntryDetailed.module.sass';
import {makeStyles} from "@material-ui/core";
import {EntryType} from "../EntryListItem/EntryListItem";
import {RestEntryDetailsTitle} from "./Rest/RestEntryDetailsTitle";
import {KafkaEntryDetailsTitle} from "./Kafka/KafkaEntryDetailsTitle";
import {RestEntryDetailsContent} from "./Rest/RestEntryDetailsContent";
import {KafkaEntryDetailsContent} from "./Kafka/KafkaEntryDetailsContent";
const useStyles = makeStyles(() => ({
entryTitle: {
display: 'flex',
minHeight: 46,
maxHeight: 46,
alignItems: 'center',
marginBottom: 8,
padding: 5,
paddingBottom: 0
}
}));
interface EntryDetailedProps {
entryData: any;
classes?: any;
entryType: string;
}
export const EntryDetailed: React.FC<EntryDetailedProps> = ({classes, entryData, entryType}) => {
const classesTitle = useStyles();
let title, content;
switch (entryType) {
case EntryType.Rest:
title = <RestEntryDetailsTitle entryData={entryData}/>;
content = <RestEntryDetailsContent entryData={entryData}/>;
break;
case EntryType.Kafka:
title = <KafkaEntryDetailsTitle entryData={entryData}/>;
content = <KafkaEntryDetailsContent entryData={entryData}/>;
break;
default:
title = <RestEntryDetailsTitle entryData={entryData}/>;
content = <RestEntryDetailsContent entryData={entryData}/>;
break;
}
return <>
<div className={classesTitle.entryTitle}>{title}</div>
<div className={styles.content}>
<div className={styles.body}>
{content}
</div>
</div>
</>
};

View File

@@ -1,4 +1,4 @@
@import '../style/variables.module'
@import 'src/variables.module'
.title
display: flex

View File

@@ -0,0 +1,213 @@
import styles from "./EntrySections.module.sass";
import React, {useState} from "react";
import {SyntaxHighlighter} from "../UI/SyntaxHighlighter";
import CollapsibleContainer from "../UI/CollapsibleContainer";
import FancyTextDisplay from "../UI/FancyTextDisplay";
import Checkbox from "../UI/Checkbox";
import ProtobufDecoder from "protobuf-decoder";
interface ViewLineProps {
label: string;
value: number | string;
}
const ViewLine: React.FC<ViewLineProps> = ({label, value}) => {
return (label && value && <tr className={styles.dataLine}>
<td className={styles.dataKey}>{label}</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
text={value}
applyTextEllipsis={false}
flipped={true}
displayIconOnMouseOver={true}
/>
</td>
</tr>) || null;
}
interface SectionCollapsibleTitleProps {
title: string;
isExpanded: boolean;
}
const SectionCollapsibleTitle: React.FC<SectionCollapsibleTitleProps> = ({title, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>{title}</span>
</div>
}
interface SectionContainerProps {
title: string;
}
export const SectionContainer: React.FC<SectionContainerProps> = ({title, children}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<SectionCollapsibleTitle title={title} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
interface BodySectionProps {
content: any;
encoding?: string;
contentType?: string;
}
export const BodySection: React.FC<BodySectionProps> = ({content, encoding, contentType}) => {
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 protobufFormats = ['application/grpc'];
const [isWrapped, setIsWrapped] = useState(false);
const formatTextBody = (body): string => {
const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT);
const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk;
try {
if (jsonLikeFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
} else if (protobufFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
// Replace all non printable characters (ASCII)
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2);
}
} catch (error) {
console.error(error);
}
return bodyBuf;
}
const getLanguage = (mimetype) => {
const chunk = content.text?.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.text?.length > 0 && <SectionContainer title='Body'>
<table>
<tbody>
<ViewLine label={'Mime type'} value={content?.mimeType}/>
<ViewLine label={'Encoding'} value={encoding}/>
</tbody>
</table>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
<div style={{paddingTop: 3}}>
<Checkbox checked={isWrapped} onToggle={() => {}}/>
</div>
<span style={{marginLeft: '.5rem'}}>Wrap text</span>
</div>
<SyntaxHighlighter
isWrapped={isWrapped}
code={formatTextBody(content.text)}
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
/>
</SectionContainer>}
</React.Fragment>
}
interface TableSectionProps {
title: string,
arrayToIterate: any[],
}
export const TableSection: React.FC<TableSectionProps> = ({title, arrayToIterate}) => {
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<SectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({name, value}, index) => <ViewLine key={index} label={name}
value={value}/>)}
</tbody>
</table>
</SectionContainer> : <span/>
}
</React.Fragment>
}
interface HAREntryPolicySectionProps {
service: string,
title: string,
response: any,
latency?: number,
arrayToIterate: any[],
}
interface HAREntryPolicySectionCollapsibleTitleProps {
label: string;
matched: string;
isExpanded: boolean;
}
const HAREntryPolicySectionCollapsibleTitle: React.FC<HAREntryPolicySectionCollapsibleTitleProps> = ({label, matched, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>
<tr className={styles.dataLine}>
<td className={`${styles.dataKey} ${styles.rulesTitleSuccess}`}>{label}</td>
<td className={`${styles.dataKey} ${matched === 'Success' ? styles.rulesMatchedSuccess : styles.rulesMatchedFailure}`}>{matched}</td>
</tr>
</span>
</div>
}
interface HAREntryPolicySectionContainerProps {
label: string;
matched: string;
children?: any;
}
export const HAREntryPolicySectionContainer: React.FC<HAREntryPolicySectionContainerProps> = ({label, matched, children}) => {
const [expanded, setExpanded] = useState(false);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntryPolicySectionCollapsibleTitle label={label} matched={matched} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, response, latency, arrayToIterate}) => {
return <React.Fragment>
{arrayToIterate && arrayToIterate.length > 0 ? <>
<SectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({rule, matched}, index) => {
return (<HAREntryPolicySectionContainer key={index} label={rule.Name} matched={matched && (rule.Type === 'latency' ? rule.Latency >= latency : true)? "Success" : "Failure"}>
<>
{rule.Key && <tr className={styles.dataValue}><td><b>Key</b>:</td><td>{rule.Key}</td></tr>}
{rule.Latency > 0 ? <tr className={styles.dataValue}><td><b>Latency</b>:</td><td>{rule.Latency}</td></tr> : ''}
{rule.Method && <tr className={styles.dataValue}><td><b>Method:</b></td> <td>{rule.Method}</td></tr>}
{rule.Path && <tr className={styles.dataValue}><td><b>Path:</b></td> <td>{rule.Path}</td></tr>}
{rule.Service && <tr className={styles.dataValue}><td><b>Service:</b></td> <td>{service}</td></tr>}
{rule.Type && <tr className={styles.dataValue}><td><b>Type:</b></td> <td>{rule.Type}</td></tr>}
{rule.Value && <tr className={styles.dataValue}><td><b>Value:</b></td> <td>{rule.Value}</td></tr>}
</>
</HAREntryPolicySectionContainer>)})}
</tbody>
</table>
</SectionContainer>
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>}
</React.Fragment>
}

View File

@@ -0,0 +1,6 @@
import React from "react";
export const KafkaEntryDetailsContent: React.FC<any> = ({entryData}) => {
return <></>;
}

View File

@@ -0,0 +1,6 @@
import React from "react";
export const KafkaEntryDetailsTitle: React.FC<any> = ({entryData}) => {
return <></>
}

View File

@@ -0,0 +1,43 @@
import React, {useState} from "react";
import styles from "../EntryDetailed.module.sass";
import Tabs from "../../UI/Tabs";
import {BodySection, HAREntryTablePolicySection, TableSection} from "../EntrySections";
import {singleEntryToHAR} from "../../../helpers/utils";
const MIME_TYPE_KEY = 'mimeType';
export const RestEntryDetailsContent: React.FC<any> = ({entryData}) => {
const har = singleEntryToHAR(entryData);
const {request, response, timings: {receive}} = har.log.entries[0].entry;
const rulesMatched = har.log.entries[0].rulesMatched
const TABS = [
{tab: 'request'},
{tab: 'response'},
{tab: 'Rules'},
];
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
return <>
<div className={styles.bodyHeader}>
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned/>
{request?.url && <a className={styles.endpointURL} href={request.url} target='_blank' rel="noreferrer">{request.url}</a>}
</div>
{currentTab === TABS[0].tab && <>
<TableSection title={'Headers'} arrayToIterate={request.headers}/>
<TableSection title={'Cookies'} arrayToIterate={request.cookies}/>
{request?.postData && <BodySection content={request.postData} encoding={request.postData.comment} contentType={request.postData[MIME_TYPE_KEY]}/>}
<TableSection title={'Query'} arrayToIterate={request.queryString}/>
</>
}
{currentTab === TABS[1].tab && <>
<TableSection title={'Headers'} arrayToIterate={response.headers}/>
<BodySection content={response.content} encoding={response.content?.encoding} contentType={response.content?.mimeType}/>
<TableSection title={'Cookies'} arrayToIterate={response.cookies}/>
</>}
{currentTab === TABS[2].tab && <>
<HAREntryTablePolicySection service={har.log.entries[0].service} title={'Rule'} latency={receive} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>
</>}
</>;
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import {singleEntryToHAR} from "../../../helpers/utils";
import StatusCode from "../../UI/StatusCode";
import {EndpointPath} from "../../UI/EndpointPath";
const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
export const RestEntryDetailsTitle: React.FC<any> = ({entryData}) => {
const har = singleEntryToHAR(entryData);
const {log: {entries}} = har;
const {response, request, timings: {receive}} = entries[0].entry;
const {status, statusText, bodySize} = response;
return har && <>
{status && <div style={{marginRight: 8}}>
<StatusCode statusCode={status}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<EndpointPath method={request?.method} path={request?.url}/>
</div>
<div style={{margin: "0 18px", opacity: 0.5}}>{formatSize(bodySize)}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{status} {statusText}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{Math.round(receive)}ms</div>
</>
}

View File

@@ -1,4 +1,4 @@
@import 'variables.module'
@import 'src/variables.module'
.row
display: flex
@@ -43,20 +43,20 @@
.ruleNumberTextFailure
color: #DB2156
font-family: Source Sans Pro;
font-style: normal;
font-weight: 600;
font-size: 12px;
line-height: 15px;
font-family: Source Sans Pro
font-style: normal
font-weight: 600
font-size: 12px
line-height: 15px
padding-right: 12px
.ruleNumberTextSuccess
color: #219653
font-family: Source Sans Pro;
font-style: normal;
font-weight: 600;
font-size: 12px;
line-height: 15px;
font-family: Source Sans Pro
font-style: normal
font-weight: 600
font-size: 12px
line-height: 15px
padding-right: 12px
.service
@@ -73,10 +73,11 @@
.timestamp
font-size: 12px
color: $secondary-font-color
padding-left: 12px
flex-shrink: 0
width: 145px
text-align: left
border-left: 1px solid $data-background-color
padding: 6px 0 6px 12px
.endpointServiceContainer
display: flex
@@ -88,6 +89,12 @@
.directionContainer
display: flex
border-right: 1px solid $data-background-color
padding: 4px
padding-right: 12px
padding: 4px 12px 4px 4px
.icon
height: 14px
width: 50px
padding: 5px
background-color: white
border-radius: 15px
box-shadow: 1px 1px 9px -4px black

View File

@@ -0,0 +1,85 @@
import React from "react";
import styles from './EntryListItem.module.sass';
import restIcon from '../assets/restIcon.svg';
import kafkaIcon from '../assets/kafkaIcon.svg';
import {RestEntry, RestEntryContent} from "./RestEntryContent";
import {KafkaEntry, KafkaEntryContent} from "./KafkaEntryContent";
export interface BaseEntry {
type: string;
timestamp: Date;
id: string;
rules: Rules;
latency: number;
}
interface Rules {
status: boolean;
latency: number;
numberOfRules: number;
}
interface EntryProps {
entry: RestEntry | KafkaEntry | any;
setFocusedEntry: (entry: RestEntry | KafkaEntry) => void;
isSelected?: boolean;
}
export enum EntryType {
Rest = "rest",
Kafka = "kafka"
}
export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntry, isSelected}) => {
let additionalRulesProperties = "";
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
if (entry.rules.latency >= entry.latency) {
additionalRulesProperties = styles.ruleSuccessRow
} else {
additionalRulesProperties = styles.ruleFailureRow
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.latency >= entry.latency ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
} else {
if (entry.rules.status) {
additionalRulesProperties = styles.ruleSuccessRow
} else {
additionalRulesProperties = styles.ruleFailureRow
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.status ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
}
}
let icon, content;
switch (entry.type) {
case EntryType.Rest:
content = <RestEntryContent entry={entry}/>;
icon = restIcon;
break;
case EntryType.Kafka:
content = <KafkaEntryContent entry={entry}/>;
icon = kafkaIcon;
break;
default:
content = <RestEntryContent entry={entry}/>;
icon = restIcon;
break;
}
return <>
<div id={entry.id} className={`${styles.row} ${isSelected && !rule ? styles.rowSelected : additionalRulesProperties}`}
onClick={() => setFocusedEntry(entry)}>
{icon && <div style={{width: 80}}>{<img className={styles.icon} alt="icon" src={icon}/>}</div>}
{content}
<div className={styles.timestamp}>{new Date(+entry.timestamp)?.toLocaleString()}</div>
</div>
</>
};

View File

@@ -0,0 +1,15 @@
import {BaseEntry} from "./EntryListItem";
import React from "react";
export interface KafkaEntry extends BaseEntry{
}
interface KafkaEntryContentProps {
entry: KafkaEntry;
}
export const KafkaEntryContent: React.FC<KafkaEntryContentProps> = ({entry}) => {
return <>
</>
}

View File

@@ -0,0 +1,82 @@
import React from "react";
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg";
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg";
import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg";
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg";
import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg";
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg";
import styles from "./EntryListItem.module.sass";
import {EndpointPath} from "../UI/EndpointPath";
import {BaseEntry} from "./EntryListItem";
export interface RestEntry extends BaseEntry{
method?: string,
path: string,
service: string,
statusCode?: number;
url?: string;
isCurrentRevision?: boolean;
isOutgoing?: boolean;
}
interface RestEntryContentProps {
entry: RestEntry;
}
export const RestEntryContent: React.FC<RestEntryContentProps> = ({entry}) => {
const classification = getClassification(entry.statusCode)
const numberOfRules = entry.rules.numberOfRules
let ingoingIcon;
let outgoingIcon;
switch (classification) {
case StatusCodeClassification.SUCCESS: {
ingoingIcon = ingoingIconSuccess;
outgoingIcon = outgoingIconSuccess;
break;
}
case StatusCodeClassification.FAILURE: {
ingoingIcon = ingoingIconFailure;
outgoingIcon = outgoingIconFailure;
break;
}
case StatusCodeClassification.NEUTRAL: {
ingoingIcon = ingoingIconNeutral;
outgoingIcon = outgoingIconNeutral;
break;
}
}
let ruleSuccess: boolean;
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
ruleSuccess = entry.rules.latency >= entry.latency;
} else {
ruleSuccess = entry.rules.status;
}
}
return <>
{entry.statusCode && <div>
<StatusCode statusCode={entry.statusCode}/>
</div>}
<div className={styles.endpointServiceContainer}>
<EndpointPath method={entry.method} path={entry.path}/>
<div className={styles.service}>
{entry.service}
</div>
</div>
{rule && <div className={`${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure}`}>
{`Rules (${numberOfRules})`}
</div>}
<div className={styles.directionContainer}>
{entry.isOutgoing ?
<img src={outgoingIcon} alt="outgoing traffic" title="outgoing"/>
:
<img src={ingoingIcon} alt="ingoing traffic" title="ingoing"/>
}
</div>
</>
}

View File

@@ -1,8 +1,8 @@
import React from "react";
import styles from './style/HarFilters.module.sass';
import {HARFilterSelect} from "./HARFilterSelect";
import styles from './style/Filters.module.sass';
import {FilterSelect} from "./UI/FilterSelect";
import {TextField} from "@material-ui/core";
import {ALL_KEY} from "./Select";
import {ALL_KEY} from "./UI/Select";
interface HarFiltersProps {
methodsFilter: Array<string>;
@@ -13,7 +13,7 @@ interface HarFiltersProps {
setPathFilter: (val: string) => void;
}
export const HarFilters: React.FC<HarFiltersProps> = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => {
export const Filters: React.FC<HarFiltersProps> = ({methodsFilter, setMethodsFilter, statusFilter, setStatusFilter, pathFilter, setPathFilter}) => {
return <div className={styles.container}>
<MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
@@ -59,7 +59,7 @@ const MethodFilter: React.FC<MethodFilterProps> = ({methodsFilter, setMethodsFil
}
return <FilterContainer>
<HARFilterSelect
<FilterSelect
items={Object.values(HTTPMethod)}
allowMultiple={true}
value={methodsFilter}
@@ -91,7 +91,7 @@ const StatusTypesFilter: React.FC<StatusTypesFilterProps> = ({statusFilter, setS
}
return <FilterContainer>
<HARFilterSelect
<FilterSelect
items={Object.values(StatusType)}
allowMultiple={true}
value={statusFilter}

View File

@@ -1,116 +0,0 @@
import React from "react";
import styles from './style/HarEntry.module.sass';
import StatusCode, {getClassification, StatusCodeClassification} from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
import ingoingIconSuccess from "./assets/ingoing-traffic-success.svg"
import ingoingIconFailure from "./assets/ingoing-traffic-failure.svg"
import ingoingIconNeutral from "./assets/ingoing-traffic-neutral.svg"
import outgoingIconSuccess from "./assets/outgoing-traffic-success.svg"
import outgoingIconFailure from "./assets/outgoing-traffic-failure.svg"
import outgoingIconNeutral from "./assets/outgoing-traffic-neutral.svg"
interface HAREntry {
method?: string,
path: string,
service: string,
id: string,
statusCode?: number;
url?: string;
isCurrentRevision?: boolean;
timestamp: Date;
isOutgoing?: boolean;
latency: number;
rules: Rules;
}
interface Rules {
status: boolean;
latency: number;
numberOfRules: number;
}
interface HAREntryProps {
entry: HAREntry;
setFocusedEntryId: (id: string) => void;
isSelected?: boolean;
}
export const HarEntry: React.FC<HAREntryProps> = ({entry, setFocusedEntryId, isSelected}) => {
const classification = getClassification(entry.statusCode)
const numberOfRules = entry.rules.numberOfRules
let ingoingIcon;
let outgoingIcon;
switch(classification) {
case StatusCodeClassification.SUCCESS: {
ingoingIcon = ingoingIconSuccess;
outgoingIcon = outgoingIconSuccess;
break;
}
case StatusCodeClassification.FAILURE: {
ingoingIcon = ingoingIconFailure;
outgoingIcon = outgoingIconFailure;
break;
}
case StatusCodeClassification.NEUTRAL: {
ingoingIcon = ingoingIconNeutral;
outgoingIcon = outgoingIconNeutral;
break;
}
}
let additionalRulesProperties = "";
let ruleSuccess: boolean;
let rule = 'latency' in entry.rules
if (rule) {
if (entry.rules.latency !== -1) {
if (entry.rules.latency >= entry.latency) {
additionalRulesProperties = styles.ruleSuccessRow
ruleSuccess = true
} else {
additionalRulesProperties = styles.ruleFailureRow
ruleSuccess = false
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.latency >= entry.latency ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
} else {
if (entry.rules.status) {
additionalRulesProperties = styles.ruleSuccessRow
ruleSuccess = true
} else {
additionalRulesProperties = styles.ruleFailureRow
ruleSuccess = false
}
if (isSelected) {
additionalRulesProperties += ` ${entry.rules.status ? styles.ruleSuccessRowSelected : styles.ruleFailureRowSelected}`
}
}
}
return <>
<div id={entry.id} className={`${styles.row} ${isSelected && !rule ? styles.rowSelected : additionalRulesProperties}`} onClick={() => setFocusedEntryId(entry.id)}>
{entry.statusCode && <div>
<StatusCode statusCode={entry.statusCode}/>
</div>}
<div className={styles.endpointServiceContainer}>
<EndpointPath method={entry.method} path={entry.path}/>
<div className={styles.service}>
{entry.service}
</div>
</div>
{
rule ?
<div className={`${ruleSuccess ? styles.ruleNumberTextSuccess : styles.ruleNumberTextFailure}`}>
{`Rules (${numberOfRules})`}
</div>
: ""
}
<div className={styles.directionContainer}>
{entry.isOutgoing ?
<img src={outgoingIcon} alt="outgoing traffic" title="outgoing"/>
:
<img src={ingoingIcon} alt="ingoing traffic" title="ingoing"/>
}
</div>
<div className={styles.timestamp}>{new Date(+entry.timestamp)?.toLocaleString()}</div>
</div>
</>
};

View File

@@ -1,61 +0,0 @@
import React from "react";
import {singleEntryToHAR} from "./utils";
import styles from './style/HarEntryDetailed.module.sass';
import HAREntryViewer from "./HarEntryViewer/HAREntryViewer";
import {makeStyles} from "@material-ui/core";
import StatusCode from "./StatusCode";
import {EndpointPath} from "./EndpointPath";
const useStyles = makeStyles(() => ({
entryTitle: {
display: 'flex',
minHeight: 46,
maxHeight: 46,
alignItems: 'center',
marginBottom: 8,
padding: 5,
paddingBottom: 0
}
}));
interface HarEntryDetailedProps {
harEntry: any;
classes?: any;
}
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
const HarEntryTitle: React.FC<any> = ({har}) => {
const classes = useStyles();
const {log: {entries}} = har;
const {response, request, timings: {receive}} = entries[0].entry;
const {status, statusText, bodySize} = response;
return <div className={classes.entryTitle}>
{status && <div style={{marginRight: 8}}>
<StatusCode statusCode={status}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<EndpointPath method={request?.method} path={request?.url}/>
</div>
<div style={{margin: "0 18px", opacity: 0.5}}>{formatSize(bodySize)}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{status} {statusText}</div>
<div style={{marginRight: 18, opacity: 0.5}}>{Math.round(receive)}ms</div>
</div>;
};
export const HAREntryDetailed: React.FC<HarEntryDetailedProps> = ({classes, harEntry}) => {
const har = singleEntryToHAR(harEntry);
return <>
{har && <HarEntryTitle har={har}/>}
<>
{har && <HAREntryViewer
harObject={har}
className={classes?.root ?? styles.har}
/>}
</>
</>
};

View File

@@ -1,266 +0,0 @@
import styles from "./HAREntrySections.module.sass";
import React, {useState} from "react";
import {SyntaxHighlighter} from "../SyntaxHighlighter/index";
import CollapsibleContainer from "../CollapsibleContainer";
import FancyTextDisplay from "../FancyTextDisplay";
import Checkbox from "../Checkbox";
import ProtobufDecoder from "protobuf-decoder";
var jp = require('jsonpath');
interface HAREntryViewLineProps {
label: string;
value: number | string;
}
const HAREntryViewLine: React.FC<HAREntryViewLineProps> = ({label, value}) => {
return (label && value && <tr className={styles.dataLine}>
<td className={styles.dataKey}>{label}</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
text={value}
applyTextEllipsis={false}
flipped={true}
displayIconOnMouseOver={true}
/>
</td>
</tr>) || null;
}
interface HAREntrySectionCollapsibleTitleProps {
title: string;
isExpanded: boolean;
}
const HAREntrySectionCollapsibleTitle: React.FC<HAREntrySectionCollapsibleTitleProps> = ({title, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>{title}</span>
</div>
}
interface HAREntrySectionContainerProps {
title: string;
}
export const HAREntrySectionContainer: React.FC<HAREntrySectionContainerProps> = ({title, children}) => {
const [expanded, setExpanded] = useState(true);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntrySectionCollapsibleTitle title={title} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
interface HAREntryBodySectionProps {
content: any;
encoding?: string;
contentType?: string;
}
export const HAREntryBodySection: React.FC<HAREntryBodySectionProps> = ({
content,
encoding,
contentType,
}) => {
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 protobufFormats = ['application/grpc'];
const [isWrapped, setIsWrapped] = useState(false);
const formatTextBody = (body): string => {
const chunk = body.slice(0, MAXIMUM_BYTES_TO_HIGHLIGHT);
const bodyBuf = encoding === 'base64' ? atob(chunk) : chunk;
try {
if (jsonLikeFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
return JSON.stringify(JSON.parse(bodyBuf), null, 2);
} else if (protobufFormats.some(format => content?.mimeType?.indexOf(format) > -1)) {
// Replace all non printable characters (ASCII)
const protobufDecoder = new ProtobufDecoder(bodyBuf, true);
return JSON.stringify(protobufDecoder.decode().toSimple(), null, 2);
}
} catch (error) {
console.error(error);
}
return bodyBuf;
}
const getLanguage = (mimetype) => {
const chunk = content.text?.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.text?.length > 0 && <HAREntrySectionContainer title='Body'>
<table>
<tbody>
<HAREntryViewLine label={'Mime type'} value={content?.mimeType}/>
<HAREntryViewLine label={'Encoding'} value={encoding}/>
</tbody>
</table>
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}} onClick={() => setIsWrapped(!isWrapped)}>
<div style={{paddingTop: 3}}>
<Checkbox checked={isWrapped} onToggle={() => {}}/>
</div>
<span style={{marginLeft: '.5rem'}}>Wrap text</span>
</div>
<SyntaxHighlighter
isWrapped={isWrapped}
code={formatTextBody(content.text)}
language={content?.mimeType ? getLanguage(content.mimeType) : 'default'}
/>
</HAREntrySectionContainer>}
</React.Fragment>
}
interface HAREntrySectionProps {
title: string,
arrayToIterate: any[],
}
export const HAREntryTableSection: React.FC<HAREntrySectionProps> = ({title, arrayToIterate}) => {
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<HAREntrySectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({name, value}, index) => <HAREntryViewLine key={index} label={name}
value={value}/>)}
</tbody>
</table>
</HAREntrySectionContainer> : <span/>
}
</React.Fragment>
}
interface HAREntryPolicySectionProps {
service: string,
title: string,
response: any,
latency?: number,
arrayToIterate: any[],
}
interface HAREntryPolicySectionCollapsibleTitleProps {
label: string;
matched: string;
isExpanded: boolean;
}
const HAREntryPolicySectionCollapsibleTitle: React.FC<HAREntryPolicySectionCollapsibleTitleProps> = ({label, matched, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`}>
{isExpanded ? '-' : '+'}
</span>
<span>
<tr className={styles.dataLine}>
<td className={`${styles.dataKey} ${styles.rulesTitleSuccess}`}>{label}</td>
<td className={`${styles.dataKey} ${matched === 'Success' ? styles.rulesMatchedSuccess : styles.rulesMatchedFailure}`}>{matched}</td>
</tr>
</span>
</div>
}
interface HAREntryPolicySectionContainerProps {
label: string;
matched: string;
children?: any;
}
export const HAREntryPolicySectionContainer: React.FC<HAREntryPolicySectionContainerProps> = ({label, matched, children}) => {
const [expanded, setExpanded] = useState(false);
return <CollapsibleContainer
className={styles.collapsibleContainer}
isExpanded={expanded}
onClick={() => setExpanded(!expanded)}
title={<HAREntryPolicySectionCollapsibleTitle label={label} matched={matched} isExpanded={expanded}/>}
>
{children}
</CollapsibleContainer>
}
export const HAREntryTablePolicySection: React.FC<HAREntryPolicySectionProps> = ({service, title, response, latency, arrayToIterate}) => {
const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {};
return <React.Fragment>
{
arrayToIterate && arrayToIterate.length > 0 ?
<>
<HAREntrySectionContainer title={title}>
<table>
<tbody>
{arrayToIterate.map(({rule, matched}, index) => {
return (
<HAREntryPolicySectionContainer key={index} label={rule.Name} matched={matched && (rule.Type === 'latency' ? rule.Latency >= latency : true)? "Success" : "Failure"}>
{
<>
{
rule.Key != "" ?
<tr className={styles.dataValue}><td><b>Key</b>:</td><td>{rule.Key}</td></tr>
: null
}
{
rule.Latency != "" ?
<tr className={styles.dataValue}><td><b>Latency:</b></td> <td>{rule.Latency}</td></tr>
: null
}
{
rule.Method != "" ?
<tr className={styles.dataValue}><td><b>Method:</b></td> <td>{rule.Method}</td></tr>
: null
}
{
rule.Path != "" ?
<tr className={styles.dataValue}><td><b>Path:</b></td> <td>{rule.Path}</td></tr>
: null
}
{
rule.Service != "" ?
<tr className={styles.dataValue}><td><b>Service:</b></td> <td>{service}</td></tr>
: null
}
{
rule.Type != "" ?
<tr className={styles.dataValue}><td><b>Type:</b></td> <td>{rule.Type}</td></tr>
: null
}
{
rule.Value != "" ?
<tr className={styles.dataValue}><td><b>Value:</b></td> <td>{rule.Value}</td></tr>
: null
}
</>
}
</HAREntryPolicySectionContainer>
)
}
)
}
</tbody>
</table>
</HAREntrySectionContainer>
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>
}
</React.Fragment>
}

View File

@@ -1,60 +0,0 @@
@import "../style/variables.module"
.harEntry
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: 100%
width: 100%
h3,
h4
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
.header
background-color: rgb(55, 65, 111)
padding: 0.5rem .75rem .65rem .75rem
border-top-left-radius: 0.25rem
border-top-right-radius: 0.25rem
display: flex
font-size: .75rem
align-items: center
.description
min-width: 25rem
display: flex
align-items: center
justify-content: space-between
.method
padding: 0 .25rem
font-size: 0.75rem
font-weight: bold
border-radius: 0.25rem
border: 0.0625rem solid rgba(255, 255, 255, 0.16)
margin-right: .5rem
> span
margin-left: .5rem
.timing
border-left: 1px solid #627ef7
margin-left: .3rem
padding-left: .3rem
.headerClickable
cursor: pointer
&:hover
background: lighten(rgb(55, 65, 111), 10%)
border-top-left-radius: 0
border-top-right-radius: 0
.body
background: $main-background-color
color: $blue-gray
border-radius: 4px
padding: 10px
.bodyHeader
padding: 0 1rem
.endpointURL
font-size: .75rem
display: block
color: $blue-color
text-decoration: none
margin-bottom: .5rem
overflow-wrap: anywhere
padding: 5px 0

View File

@@ -1,71 +0,0 @@
import React, {useState} from 'react';
import styles from './HAREntryViewer.module.sass';
import Tabs from "../Tabs";
import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections";
const MIME_TYPE_KEY = 'mimeType';
const HAREntryDisplay: React.FC<any> = ({har, entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => {
const {request, response, timings: {receive}} = entry;
const rulesMatched = har.log.entries[0].rulesMatched
const TABS = [
{tab: 'request'},
{
tab: 'response',
badge: <>{isResponseMocked && <span className="smallBadge virtual mock">MOCK</span>}</>
},
{
tab: 'Rules',
},
];
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
return <div className={styles.harEntry}>
{!initialIsCollapsed && <div className={styles.body}>
<div className={styles.bodyHeader}>
<Tabs tabs={TABS} currentTab={currentTab} onChange={setCurrentTab} leftAligned/>
{request?.url && <a className={styles.endpointURL} href={request.url} target='_blank' rel="noreferrer">{request.url}</a>}
</div>
{
currentTab === TABS[0].tab && <React.Fragment>
<HAREntryTableSection title={'Headers'} arrayToIterate={request.headers}/>
<HAREntryTableSection title={'Cookies'} arrayToIterate={request.cookies}/>
{request?.postData && <HAREntryBodySection content={request.postData} encoding={request.postData.comment} contentType={request.postData[MIME_TYPE_KEY]}/>}
<HAREntryTableSection title={'Query'} arrayToIterate={request.queryString}/>
</React.Fragment>
}
{currentTab === TABS[1].tab && <React.Fragment>
<HAREntryTableSection title={'Headers'} arrayToIterate={response.headers}/>
<HAREntryBodySection content={response.content} encoding={response.content?.encoding} contentType={response.content?.mimeType}/>
<HAREntryTableSection title={'Cookies'} arrayToIterate={response.cookies}/>
</React.Fragment>}
{currentTab === TABS[2].tab && <React.Fragment>
<HAREntryTablePolicySection service={har.log.entries[0].service} title={'Rule'} latency={receive} response={response} arrayToIterate={rulesMatched ? rulesMatched : []}/>
</React.Fragment>}
</div>}
</div>;
}
interface Props {
harObject: any;
className?: string;
isResponseMocked?: boolean;
showTitle?: boolean;
}
const HAREntryViewer: React.FC<Props> = ({harObject, className, isResponseMocked, showTitle=true}) => {
const {log: {entries}} = harObject;
const isCollapsed = entries.length > 1;
return <div className={`${className ? className : ''}`}>
{Object.keys(entries).map((entry: any, index) => <HAREntryDisplay har={harObject} isCollapsed={isCollapsed} key={index} entry={entries[entry].entry} isResponseMocked={isResponseMocked} showTitle={showTitle}/>)}
</div>
};
export default HAREntryViewer;

View File

@@ -1,27 +0,0 @@
import prevIcon from "./assets/icon-prev.svg";
import nextIcon from "./assets/icon-next.svg";
import {Box} from "@material-ui/core";
import React from "react";
import styles from './style/HarPaging.module.sass'
import numeral from 'numeral';
interface HarPagingProps {
showPageNumber?: boolean;
}
export const HarPaging: React.FC<HarPagingProps> = ({showPageNumber=false}) => {
return <Box className={styles.HarPaging} display='flex'>
<img src={prevIcon} onClick={() => {
// harStore.data.moveBack(); todo
}} alt="back"/>
{showPageNumber && <span className={styles.text}>
Page <span className={styles.pageNumber}>
{/*{numeral(harStore.data.currentPage).format(0, 0)}*/} //todo
</span>
</span>}
<img src={nextIcon} onClick={() => {
// harStore.data.moveNext(); todo
}} alt="next"/>
</Box>
};

View File

@@ -1,14 +1,14 @@
import React, {useEffect, useRef, useState} from "react";
import {HarFilters} from "./HarFilters";
import {HarEntriesList} from "./HarEntriesList";
import {Filters} from "./Filters";
import {EntriesList} from "./EntriesList";
import {makeStyles} from "@material-ui/core";
import "./style/HarPage.sass";
import styles from './style/HarEntriesList.module.sass';
import {HAREntryDetailed} from "./HarEntryDetailed";
import "./style/TrafficPage.sass";
import styles from './style/EntriesList.module.sass';
import {EntryDetailed} from "./EntryDetailed/EntryDetailed";
import playIcon from './assets/run.svg';
import pauseIcon from './assets/pause.svg';
import variables from './style/variables.module.scss';
import {StatusBar} from "./StatusBar";
import variables from '../variables.module.scss';
import {StatusBar} from "./UI/StatusBar";
import Api, {MizuWebsocketURL} from "../helpers/api";
const useLayoutStyles = makeStyles(() => ({
@@ -43,13 +43,13 @@ interface HarPageProps {
const api = new Api();
export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected}) => {
export const TrafficPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected}) => {
const classes = useLayoutStyles();
const [entries, setEntries] = useState([] as any);
const [focusedEntryId, setFocusedEntryId] = useState(null);
const [selectedHarEntry, setSelectedHarEntry] = useState(null);
const [focusedEntry, setFocusedEntry] = useState(null);
const [selectedEntryData, setSelectedEntryData] = useState(null);
const [connection, setConnection] = useState(ConnectionStatus.Closed);
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [noMoreDataBottom, setNoMoreDataBottom] = useState(false);
@@ -60,8 +60,12 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
const [tappingStatus, setTappingStatus] = useState(null);
const [disableScrollList, setDisableScrollList] = useState(false);
const ws = useRef(null);
const listEntry = useRef(null);
const openWebSocket = () => {
ws.current = new WebSocket(MizuWebsocketURL);
ws.current.onopen = () => setConnection(ConnectionStatus.Connected);
@@ -79,13 +83,18 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
setNoMoreDataBottom(false)
return;
}
if (!focusedEntryId) setFocusedEntryId(entry.id)
if (!focusedEntry) setFocusedEntry(entry)
let newEntries = [...entries];
if (entries.length === 1000) {
newEntries = newEntries.splice(1);
setNoMoreDataTop(false);
}
setEntries([...newEntries, entry])
if(listEntry.current) {
if(isScrollable(listEntry.current.firstChild)) {
setDisableScrollList(true)
}
}
break
case "status":
setTappingStatus(message.tappingStatus);
@@ -119,17 +128,17 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
useEffect(() => {
if (!focusedEntryId) return;
setSelectedHarEntry(null);
if (!focusedEntry) return;
setSelectedEntryData(null);
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId);
setSelectedHarEntry(entryData);
const entryData = await api.getEntry(focusedEntry.id);
setSelectedEntryData(entryData);
} catch (error) {
console.error(error);
}
})()
}, [focusedEntryId])
}, [focusedEntry])
const toggleConnection = () => {
setConnection(connection === ConnectionStatus.Connected ? ConnectionStatus.Paused : ConnectionStatus.Connected);
@@ -158,6 +167,14 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
}
}
const onScrollEvent = (isAtBottom) => {
isAtBottom ? setDisableScrollList(false) : setDisableScrollList(true)
}
const isScrollable = (element) => {
return element.scrollHeight > element.clientHeight;
};
return (
<div className="HarPage">
<div className="harPageHeader">
@@ -172,32 +189,34 @@ export const HarPage: React.FC<HarPageProps> = ({setAnalyzeStatus, onTLSDetected
</div>
{entries.length > 0 && <div className="HarPage-Container">
<div className="HarPage-ListContainer">
<HarFilters methodsFilter={methodsFilter}
setMethodsFilter={setMethodsFilter}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pathFilter={pathFilter}
setPathFilter={setPathFilter}
<Filters methodsFilter={methodsFilter}
setMethodsFilter={setMethodsFilter}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
pathFilter={pathFilter}
setPathFilter={setPathFilter}
/>
<div className={styles.container}>
<HarEntriesList entries={entries}
setEntries={setEntries}
focusedEntryId={focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
connectionOpen={connection === ConnectionStatus.Connected}
noMoreDataBottom={noMoreDataBottom}
setNoMoreDataBottom={setNoMoreDataBottom}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
methodsFilter={methodsFilter}
statusFilter={statusFilter}
pathFilter={pathFilter}
<EntriesList entries={entries}
setEntries={setEntries}
focusedEntry={focusedEntry}
setFocusedEntry={setFocusedEntry}
connectionOpen={connection === ConnectionStatus.Connected}
noMoreDataBottom={noMoreDataBottom}
setNoMoreDataBottom={setNoMoreDataBottom}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
methodsFilter={methodsFilter}
statusFilter={statusFilter}
pathFilter={pathFilter}
listEntryREF={listEntry}
onScrollEvent={onScrollEvent}
scrollableList={disableScrollList}
/>
</div>
</div>
<div className={classes.details}>
{selectedHarEntry &&
<HAREntryDetailed harEntry={selectedHarEntry} classes={{root: classes.harViewer}}/>}
{selectedEntryData && <EntryDetailed entryData={selectedEntryData} entryType={focusedEntry?.type} classes={{root: classes.harViewer}}/>}
</div>
</div>}
{tappingStatus?.pods != null && <StatusBar tappingStatus={tappingStatus}/>}

View File

@@ -1,6 +1,6 @@
import React, {useState} from "react";
import collapsedImg from "./assets/collapsed.svg";
import expandedImg from "./assets/expanded.svg";
import collapsedImg from "../assets/collapsed.svg";
import expandedImg from "../assets/expanded.svg";
import "./style/CollapsibleContainer.sass";
interface Props {

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import duplicateImg from "./assets/duplicate.svg";
import duplicateImg from "../assets/duplicate.svg";
import './style/FancyTextDisplay.sass';
interface Props {

View File

@@ -1,6 +1,6 @@
import React from "react";
import { MenuItem } from '@material-ui/core';
import style from './style/HARFilterSelect.module.sass';
import style from './style/FilterSelect.module.sass';
import { Select, SelectProps } from "./Select";
interface HARFilterSelectProps extends SelectProps {
@@ -12,7 +12,7 @@ interface HARFilterSelectProps extends SelectProps {
transformDisplay?: (string) => string;
}
export const HARFilterSelect: React.FC<HARFilterSelectProps> = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => {
export const FilterSelect: React.FC<HARFilterSelectProps> = ({items, value, onChange, label, allowMultiple= false, transformDisplay}) => {
return <Select
value={value}
multiple={allowMultiple}

View File

@@ -1,4 +1,4 @@
import {ReactComponent as DefaultIconDown} from './assets/default_icon_down.svg';
import {ReactComponent as DefaultIconDown} from '../assets/default_icon_down.svg';
import {MenuItem, Select as MUISelect} from '@material-ui/core';
import React from 'react';
import {SelectProps as MUISelectProps} from '@material-ui/core/Select/Select';

View File

@@ -1,7 +1,7 @@
import Tooltip from "./Tooltip";
import React from "react";
import {makeStyles} from '@material-ui/core/styles';
import variables from './style/variables.module.scss';
import variables from '../../variables.module.scss';
interface Tab {
tab: string,

View File

@@ -1,4 +1,4 @@
@import 'variables.module.scss'
@import 'src/variables.module'
.statusBar
position: absolute

Some files were not shown because too many files have changed in this diff Show More