mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-02-20 04:50:20 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f3b2c540 | ||
|
|
a427534605 | ||
|
|
1d6ca9d392 | ||
|
|
f74a52d4dc | ||
|
|
6d2e9af5d7 | ||
|
|
e4ff4a0745 | ||
|
|
f9677dbaa1 | ||
|
|
0afab6c068 | ||
|
|
1d1b62ec4f | ||
|
|
e2db5087b8 | ||
|
|
241477fb5c | ||
|
|
c8e5886a96 | ||
|
|
8a8cf4aa77 | ||
|
|
7b73004e85 | ||
|
|
56dc6843e0 | ||
|
|
0409eb239d | ||
|
|
cbe04af801 | ||
|
|
59dec1a547 | ||
|
|
c4afeee5b3 | ||
|
|
8c9b8d3217 | ||
|
|
d705ae3eb6 | ||
|
|
c53b2148d1 | ||
|
|
ca897dd3c7 | ||
|
|
4406919565 |
4
.github/CODEOWNERS
vendored
Normal file
4
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# This is a comment.
|
||||
# Each line is a file pattern followed by one or more owners.
|
||||
|
||||
/ui/ @frontend
|
||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Run mizu <command> '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
Upload logs:
|
||||
1. Run the mizu command with `--set dump-logs=true` (e.g `mizu tap --set dump-logs=true`)
|
||||
2. Try to reproduce the issue
|
||||
3. CNTRL+C on terminal tab which runs mizu
|
||||
4. Upload the logs zip file from ~/.mizu/mizu_logs_**.zip
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
84
.github/workflows/validations.yaml
vendored
Normal file
84
.github/workflows/validations.yaml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: validations
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'main'
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'main'
|
||||
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
|
||||
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: Test
|
||||
run: make test-cli
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
run-tests-agent:
|
||||
name: Run Agent 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
|
||||
|
||||
- shell: bash
|
||||
run: |
|
||||
sudo apt-get install libpcap-dev
|
||||
|
||||
- name: Test
|
||||
run: make test-agent
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
18
CONTRIBUTE.md
Normal file
18
CONTRIBUTE.md
Normal file
@@ -0,0 +1,18 @@
|
||||

|
||||
# CONTRIBUTE
|
||||
We welcome code contributions from the community.
|
||||
Please read and follow the guidelines below.
|
||||
|
||||
## Communication
|
||||
* Before starting work on a major feature, please reach out to us via [GitHub](https://github.com/up9inc/mizu), [Slack](https://join.slack.com/share/zt-u6bbs3pg-X1zhQOXOH0yEoqILgH~csw), [email](mailto:mizu@up9.com), etc. We will make sure no one else is already working on it. A _major feature_ is defined as any change that is > 100 LOC altered (not including tests), or changes any user-facing behavior
|
||||
* Small patches and bug fixes don't need prior communication.
|
||||
|
||||
## Contribution requirements
|
||||
* Code style - most of the code is written in Go, please follow [these guidelines](https://golang.org/doc/effective_go)
|
||||
* Go-tools compatible (`go get`, `go test`, etc)
|
||||
* Unit-test coverage can’t go down ..
|
||||
* Code must be usefully commented. Not only for developers on the project, but also for external users of these packages
|
||||
* When reviewing PRs, you are encouraged to use Golang's [code review comments page](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -65,3 +65,8 @@ clean-cli: ## Clean CLI.
|
||||
clean-docker:
|
||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||
|
||||
test-cli: ## Run tests.
|
||||
@echo "running cli tests"; cd cli && $(MAKE) test
|
||||
|
||||
test-agent: ## Run tests.
|
||||
@echo "running agent tests"; cd agent && $(MAKE) test
|
||||
|
||||
69
README.md
69
README.md
@@ -1,16 +1,17 @@
|
||||

|
||||
|
||||
# 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 to help you troubleshoot and debug your microservices. Think TCPDump and Chrome Dev Tools combined
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Simple and powerful CLI
|
||||
- Real time view of all HTTP requests, REST and gRPC API calls
|
||||
- Real-time view of all HTTP requests, REST and gRPC API calls
|
||||
- No installation or code instrumentation
|
||||
- Works completely on premises (on-prem)
|
||||
- Works completely on premises
|
||||
|
||||
## Download
|
||||
|
||||
@@ -32,10 +33,10 @@ https://github.com/up9inc/mizu/releases/latest/download/mizu_linux_amd64 \
|
||||
&& chmod 755 mizu
|
||||
```
|
||||
|
||||
SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/releases) page.
|
||||
SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/releases) page
|
||||
|
||||
### Development (unstable) Build
|
||||
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page.
|
||||
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`
|
||||
@@ -48,8 +49,8 @@ For detailed list of k8s permissions see [PERMISSIONS](PERMISSIONS.md) document
|
||||
|
||||
1. Find pods you'd like to tap to in your Kubernetes cluster
|
||||
2. Run `mizu tap` or `mizu tap PODNAME`
|
||||
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI ..
|
||||
4. Watch the API traffic flowing ..
|
||||
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI
|
||||
4. Watch the API traffic flowing
|
||||
5. Type ^C to stop
|
||||
|
||||
## Examples
|
||||
@@ -106,44 +107,64 @@ To tap multiple pods using regex -
|
||||
## Configuration
|
||||
|
||||
Mizu can work with config file which should be stored in ${HOME}/.mizu/config.yaml (macOS: ~/.mizu/config.yaml) <br />
|
||||
In case no config file found, defaults will be used. <br />
|
||||
In case of partial configuration defined, all other fields will be used with defaults. <br />
|
||||
You can always override the defaults or config file with CLI flags.
|
||||
In case no config file found, defaults will be used <br />
|
||||
In case of partial configuration defined, all other fields will be used with defaults <br />
|
||||
You can always override the defaults or config file with CLI flags
|
||||
|
||||
To get the default config params run `mizu config` <br />
|
||||
To generate a new config file with default values use `mizu config -r`
|
||||
|
||||
Mizu has several undocumented flags which can be set by using --set flag (e.g., `mizu tap --set dump-logs=true`)
|
||||
* **mizu-resources-namespace**: Type - String, See [Namespace-Restricted Mode](#namespace-restricted-mode)
|
||||
* **telemetry**: Type - Boolean, Reports telemetry
|
||||
* **dump-logs**: Type - Boolean, At the end of the execution it creates a zip file with logs (in .mizu folder)
|
||||
* **kube-config-path**: Type - String, Setting the path to kube config (which isn't in standard path)
|
||||
### Telemetry
|
||||
|
||||
By default, mizu reports usage telemetry. It can be disabled by adding a line of `telemetry: false` in the `${HOME}/.mizu/config.yaml` file
|
||||
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Namespace-Restricted Mode
|
||||
|
||||
Some users have permission to only manage resources in one particular namespace assigned to them.
|
||||
Some users have permission to only manage resources in one particular namespace assigned to them
|
||||
By default `mizu tap` creates a new namespace `mizu` for all of its Kubernetes resources. In order to instead install
|
||||
Mizu in an existing namespace, set the `mizu-resources-namespace` config option.
|
||||
Mizu in an existing namespace, set the `mizu-resources-namespace` config option
|
||||
|
||||
If `mizu-resources-namespace` is set to a value other than the default `mizu`, Mizu will operate in a
|
||||
Namespace-Restricted mode. It will only tap pods in `mizu-resources-namespace`. This way Mizu only requires permissions
|
||||
to the namespace set by `mizu-resources-namespace`. The user must set the tapped namespace to the same namespace by
|
||||
using the `--namespace` flag or by setting `tap.namespaces` in the config file.
|
||||
using the `--namespace` flag or by setting `tap.namespaces` in the config file
|
||||
|
||||
Setting `mizu-resources-namespace=mizu` resets Mizu to its default behavior.
|
||||
Setting `mizu-resources-namespace=mizu` resets Mizu to its default behavior
|
||||
|
||||
### User agent filtering
|
||||
|
||||
User-agent filtering (like health checks) - can be configured:
|
||||
User-agent filtering (like health checks) - can be configured using command-line options:
|
||||
|
||||
Any request that contains one of those values in the user-agent header will not be captured
|
||||
|
||||
```bash
|
||||
```shell
|
||||
$ mizu tap "^ca.*" --set ignored-user-agents=kube-probe --set ignored-user-agents=prometheus
|
||||
+carts-66c77f5fbb-fq65r
|
||||
+catalogue-5f4cb7cf5-7zrmn
|
||||
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
|
||||
|
||||
This feature allows you to define set of simple rules, and test the API against them.
|
||||
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`
|
||||
|
||||
15
TESTING.md
Normal file
15
TESTING.md
Normal file
@@ -0,0 +1,15 @@
|
||||

|
||||
# TESTING
|
||||
Testing guidelines for Mizu project
|
||||
|
||||
## Unit-tests
|
||||
* TBD
|
||||
* TBD
|
||||
* TBD
|
||||
|
||||
|
||||
|
||||
## System tests
|
||||
* TBD
|
||||
* TBD
|
||||
* TBD
|
||||
2
agent/Makefile
Normal file
2
agent/Makefile
Normal file
@@ -0,0 +1,2 @@
|
||||
test: ## Run agent tests.
|
||||
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic
|
||||
@@ -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)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mizuserver/pkg/holder"
|
||||
"mizuserver/pkg/providers"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -108,6 +109,7 @@ func startReadingChannel(outputItems <-chan *tap.OutputChannelItem) {
|
||||
}
|
||||
|
||||
for item := range outputItems {
|
||||
providers.EntryAdded()
|
||||
saveHarToDb(item.HarEntry, item.ConnectionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,14 +241,7 @@ func DeleteAllEntries(c *gin.Context) {
|
||||
}
|
||||
|
||||
func GetGeneralStats(c *gin.Context) {
|
||||
sqlQuery := "SELECT count(*) as count, min(timestamp) as min, max(timestamp) as max from mizu_entries"
|
||||
var result struct {
|
||||
Count int
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
database.GetEntriesTable().Raw(sqlQuery).Scan(&result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
c.JSON(http.StatusOK, providers.GetGeneralStats())
|
||||
}
|
||||
|
||||
func GetTappingStatus(c *gin.Context) {
|
||||
|
||||
@@ -56,12 +56,14 @@ type BaseEntryDetails struct {
|
||||
type ApplicableRules struct {
|
||||
Latency int64 `json:"latency,omitempty"`
|
||||
Status bool `json:"status,omitempty"`
|
||||
NumberOfRules int `json:"numberOfRules,omitempty"`
|
||||
}
|
||||
|
||||
func NewApplicableRules(status bool, latency int64) ApplicableRules {
|
||||
func NewApplicableRules(status bool, latency int64, number int) ApplicableRules {
|
||||
ar := ApplicableRules{}
|
||||
ar.Status = status
|
||||
ar.Latency = latency
|
||||
ar.NumberOfRules = number
|
||||
return ar
|
||||
}
|
||||
|
||||
@@ -218,7 +220,7 @@ func (fewp *FullEntryWithPolicy) UnmarshalData(entry *MizuEntry) error {
|
||||
|
||||
func RunValidationRulesState(harEntry har.Entry, service string) ApplicableRules {
|
||||
numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service)
|
||||
statusPolicyToSend, latency := rules.PassedValidationRules(resultPolicyToSend, numberOfRules)
|
||||
ar := NewApplicableRules(statusPolicyToSend, latency)
|
||||
statusPolicyToSend, latency, numberOfRules := rules.PassedValidationRules(resultPolicyToSend, numberOfRules)
|
||||
ar := NewApplicableRules(statusPolicyToSend, latency, numberOfRules)
|
||||
return ar
|
||||
}
|
||||
|
||||
36
agent/pkg/providers/stats_provider.go
Normal file
36
agent/pkg/providers/stats_provider.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GeneralStats struct {
|
||||
EntriesCount int
|
||||
FirstEntryTimestamp int
|
||||
LastEntryTimestamp int
|
||||
}
|
||||
|
||||
var generalStats = GeneralStats{}
|
||||
|
||||
func ResetGeneralStats() {
|
||||
generalStats = GeneralStats{}
|
||||
}
|
||||
|
||||
func GetGeneralStats() GeneralStats {
|
||||
return generalStats
|
||||
}
|
||||
|
||||
func EntryAdded() {
|
||||
generalStats.EntriesCount++
|
||||
|
||||
currentTimestamp := int(time.Now().Unix())
|
||||
|
||||
if reflect.Value.IsZero(reflect.ValueOf(generalStats.FirstEntryTimestamp)) {
|
||||
generalStats.FirstEntryTimestamp = currentTimestamp
|
||||
}
|
||||
|
||||
generalStats.LastEntryTimestamp = currentTimestamp
|
||||
}
|
||||
|
||||
|
||||
35
agent/pkg/providers/stats_provider_test.go
Normal file
35
agent/pkg/providers/stats_provider_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package providers_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mizuserver/pkg/providers"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoEntryAddedCount(t *testing.T) {
|
||||
entriesStats := providers.GetGeneralStats()
|
||||
|
||||
if entriesStats.EntriesCount != 0 {
|
||||
t.Errorf("unexpected result - expected: %v, actual: %v", 0, entriesStats.EntriesCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntryAddedCount(t *testing.T) {
|
||||
tests := []int{1, 5, 10, 100, 500, 1000}
|
||||
|
||||
for _, entriesCount := range tests {
|
||||
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
|
||||
for i := 0; i < entriesCount; i++ {
|
||||
providers.EntryAdded()
|
||||
}
|
||||
|
||||
entriesStats := providers.GetGeneralStats()
|
||||
|
||||
if entriesStats.EntriesCount != entriesCount {
|
||||
t.Errorf("unexpected result - expected: %v, actual: %v", entriesCount, entriesStats.EntriesCount)
|
||||
}
|
||||
|
||||
t.Cleanup(providers.ResetGeneralStats)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -92,19 +92,19 @@ func MatchRequestPolicy(harEntry har.Entry, service string) (int, []RulesMatched
|
||||
return len(enforcePolicy.Rules), resultPolicyToSend
|
||||
}
|
||||
|
||||
func PassedValidationRules(rulesMatched []RulesMatched, numberOfRules int) (bool, int64) {
|
||||
func PassedValidationRules(rulesMatched []RulesMatched, numberOfRules int) (bool, int64, int) {
|
||||
if len(rulesMatched) == 0 {
|
||||
return false, 0
|
||||
return false, 0, 0
|
||||
}
|
||||
for _, rule := range rulesMatched {
|
||||
if rule.Matched == false {
|
||||
return false, -1
|
||||
return false, -1, len(rulesMatched)
|
||||
}
|
||||
}
|
||||
for _, rule := range rulesMatched {
|
||||
if strings.ToLower(rule.Rule.Type) == "latency" {
|
||||
return true, rule.Rule.Latency
|
||||
return true, rule.Rule.Latency, len(rulesMatched)
|
||||
}
|
||||
}
|
||||
return true, -1
|
||||
return true, -1, len(rulesMatched)
|
||||
}
|
||||
|
||||
BIN
assets/validation-example1.png
Normal file
BIN
assets/validation-example1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
BIN
assets/validation-example2.png
Normal file
BIN
assets/validation-example2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -39,3 +39,6 @@ build-all: ## Build for all supported platforms.
|
||||
clean: ## Clean all build artifacts.
|
||||
go clean
|
||||
rm -rf ./bin/*
|
||||
|
||||
test: ## Run cli tests.
|
||||
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic
|
||||
|
||||
@@ -3,7 +3,9 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"github.com/up9inc/mizu/cli/telemetry"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"io/ioutil"
|
||||
)
|
||||
@@ -14,20 +16,22 @@ var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Generate config with default values",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
template, err := mizu.GetConfigWithDefaults()
|
||||
go telemetry.ReportRun("config", config.Config)
|
||||
|
||||
template, err := config.GetConfigWithDefaults()
|
||||
if err != nil {
|
||||
mizu.Log.Errorf("Failed generating config with defaults %v", err)
|
||||
logger.Log.Errorf("Failed generating config with defaults %v", err)
|
||||
return nil
|
||||
}
|
||||
if regenerateFile {
|
||||
data := []byte(template)
|
||||
if err := ioutil.WriteFile(mizu.GetConfigFilePath(), data, 0644); err != nil {
|
||||
mizu.Log.Errorf("Failed writing config %v", err)
|
||||
if err := ioutil.WriteFile(config.GetConfigFilePath(), data, 0644); err != nil {
|
||||
logger.Log.Errorf("Failed writing config %v", err)
|
||||
return nil
|
||||
}
|
||||
mizu.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, mizu.GetConfigFilePath())))
|
||||
logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.GetConfigFilePath())))
|
||||
} else {
|
||||
mizu.Log.Debugf("Writing template config.\n%v", template)
|
||||
logger.Log.Debugf("Writing template config.\n%v", template)
|
||||
fmt.Printf("%v", template)
|
||||
}
|
||||
return nil
|
||||
@@ -36,5 +40,5 @@ var configCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
configCmd.Flags().BoolVarP(®enerateFile, "regenerate", "r", false, fmt.Sprintf("Regenerate the config file with default values %s", mizu.GetConfigFilePath()))
|
||||
configCmd.Flags().BoolVarP(®enerateFile, "regenerate", "r", false, fmt.Sprintf("Regenerate the config file with default values %s", config.GetConfigFilePath()))
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ package cmd
|
||||
import (
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/mizu/version"
|
||||
"github.com/up9inc/mizu/cli/telemetry"
|
||||
)
|
||||
|
||||
var fetchCmd = &cobra.Command{
|
||||
Use: "fetch",
|
||||
Short: "Download recorded traffic to files",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
go mizu.ReportRun("fetch", mizu.Config.Fetch)
|
||||
if isCompatible, err := mizu.CheckVersionCompatibility(mizu.Config.Fetch.GuiPort); err != nil {
|
||||
go telemetry.ReportRun("fetch", config.Config.Fetch)
|
||||
|
||||
if isCompatible, err := version.CheckVersionCompatibility(config.Config.Fetch.GuiPort); err != nil {
|
||||
return err
|
||||
} else if !isCompatible {
|
||||
return nil
|
||||
|
||||
@@ -4,8 +4,9 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
)
|
||||
|
||||
func RunMizuFetch() {
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Fetch.GuiPort)
|
||||
resp, err := http.Get(fmt.Sprintf("http://%s/api/har?from=%v&to=%v", mizuProxiedUrl, mizu.Config.Fetch.FromTimestamp, mizu.Config.Fetch.ToTimestamp))
|
||||
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)
|
||||
}
|
||||
@@ -34,7 +35,7 @@ func RunMizuFetch() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_ = Unzip(zipReader, mizu.Config.Fetch.Directory)
|
||||
_ = Unzip(zipReader, config.Config.Fetch.Directory)
|
||||
}
|
||||
|
||||
func Unzip(reader *zip.Reader, dest string) error {
|
||||
@@ -64,7 +65,7 @@ func Unzip(reader *zip.Reader, dest string) error {
|
||||
_ = os.MkdirAll(path, f.Mode())
|
||||
} else {
|
||||
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
|
||||
mizu.Log.Infof("writing HAR file [ %v ]", path)
|
||||
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
|
||||
@@ -73,7 +74,7 @@ func Unzip(reader *zip.Reader, dest string) error {
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mizu.Log.Info(" done")
|
||||
logger.Log.Info(" done")
|
||||
}()
|
||||
|
||||
_, err = io.Copy(f, rc)
|
||||
|
||||
@@ -3,9 +3,11 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/fsUtils"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||
"github.com/up9inc/mizu/cli/telemetry"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
@@ -16,7 +18,9 @@ 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 {
|
||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath)
|
||||
go telemetry.ReportRun("logs", config.Config)
|
||||
|
||||
kubernetesProvider, err := kubernetes.NewProvider(config.Config.View.KubeConfigPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -25,15 +29,15 @@ var logsCmd = &cobra.Command{
|
||||
if filePath == "" {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
mizu.Log.Errorf("Failed to get PWD, %v (try using `mizu logs -f <full path dest zip file>)`", err)
|
||||
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")
|
||||
}
|
||||
mizu.Log.Debugf("Using file path %s", filePath)
|
||||
logger.Log.Debugf("Using file path %s", filePath)
|
||||
|
||||
if err := fsUtils.DumpLogs(kubernetesProvider, ctx, filePath); err != nil {
|
||||
mizu.Log.Errorf("Failed dump logs %v", err)
|
||||
logger.Log.Errorf("Failed dump logs %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,8 +3,10 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/fsUtils"
|
||||
"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"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -14,11 +16,11 @@ var rootCmd = &cobra.Command{
|
||||
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 {
|
||||
mizu.Log.Errorf("Failed to use mizu folder, %v", err)
|
||||
logger.Log.Errorf("Failed to use mizu folder, %v", err)
|
||||
}
|
||||
mizu.InitLogger()
|
||||
if err := mizu.InitConfig(cmd); err != nil {
|
||||
mizu.Log.Fatal(err)
|
||||
logger.InitLogger()
|
||||
if err := config.InitConfig(cmd); err != nil {
|
||||
logger.Log.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -26,7 +28,7 @@ Further info is available at https://github.com/up9inc/mizu`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringSlice(mizu.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", mizu.SetCommandName))
|
||||
rootCmd.PersistentFlags().StringSlice(config.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", config.SetCommandName))
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
||||
@@ -2,13 +2,15 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"os"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/errormessage"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
)
|
||||
|
||||
@@ -20,31 +22,31 @@ var tapCmd = &cobra.Command{
|
||||
Long: `Record the ingoing traffic of a kubernetes pod.
|
||||
Supported protocols are HTTP and gRPC.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
go mizu.ReportRun("tap", mizu.Config.Tap)
|
||||
go telemetry.ReportRun("tap", config.Config.Tap)
|
||||
RunMizuTap()
|
||||
return nil
|
||||
},
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 1 {
|
||||
mizu.Config.Tap.PodRegexStr = args[0]
|
||||
config.Config.Tap.PodRegexStr = args[0]
|
||||
} else if len(args) > 1 {
|
||||
return errors.New("unexpected number of arguments")
|
||||
}
|
||||
|
||||
if err := mizu.Config.Validate(); err != nil {
|
||||
if err := config.Config.Validate(); err != nil {
|
||||
return errormessage.FormatError(err)
|
||||
}
|
||||
|
||||
if err := mizu.Config.Tap.Validate(); err != nil {
|
||||
if err := config.Config.Tap.Validate(); err != nil {
|
||||
return errormessage.FormatError(err)
|
||||
}
|
||||
|
||||
mizu.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.", mizu.Config.Tap.HumanMaxEntriesDBSize)
|
||||
logger.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.", config.Config.Tap.HumanMaxEntriesDBSize)
|
||||
|
||||
if mizu.Config.Tap.Analysis {
|
||||
mizu.Log.Infof(analysisMessageToConfirm)
|
||||
if config.Config.Tap.Analysis {
|
||||
logger.Log.Infof(analysisMessageToConfirm)
|
||||
if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") {
|
||||
mizu.Log.Infof("You can always run mizu without analysis, aborting")
|
||||
logger.Log.Infof("You can always run mizu without analysis, aborting")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
@@ -60,10 +62,10 @@ func init() {
|
||||
defaults.Set(&defaultTapConfig)
|
||||
|
||||
tapCmd.Flags().Uint16P(configStructs.GuiPortTapName, "p", defaultTapConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
||||
tapCmd.Flags().StringArrayP(configStructs.NamespacesTapName, "n", defaultTapConfig.Namespaces, "Namespaces selector")
|
||||
tapCmd.Flags().StringSliceP(configStructs.NamespacesTapName, "n", defaultTapConfig.Namespaces, "Namespaces selector")
|
||||
tapCmd.Flags().Bool(configStructs.AnalysisTapName, defaultTapConfig.Analysis, "Uploads traffic to UP9 for further analysis (Beta)")
|
||||
tapCmd.Flags().BoolP(configStructs.AllNamespacesTapName, "A", defaultTapConfig.AllNamespaces, "Tap all namespaces")
|
||||
tapCmd.Flags().StringArrayP(configStructs.PlainTextFilterRegexesTapName, "r", defaultTapConfig.PlainTextFilterRegexes, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
||||
tapCmd.Flags().StringSliceP(configStructs.PlainTextFilterRegexesTapName, "r", defaultTapConfig.PlainTextFilterRegexes, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
||||
tapCmd.Flags().Bool(configStructs.DisableRedactionTapName, defaultTapConfig.DisableRedaction, "Disables redaction of potentially sensitive request/response headers and body values")
|
||||
tapCmd.Flags().String(configStructs.HumanMaxEntriesDBSizeTapName, defaultTapConfig.HumanMaxEntriesDBSize, "Override the default max entries db size")
|
||||
tapCmd.Flags().String(configStructs.DirectionTapName, defaultTapConfig.Direction, "Record traffic that goes in this direction (relative to the tapped pod): in/any")
|
||||
|
||||
@@ -5,9 +5,13 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/fsUtils"
|
||||
"github.com/up9inc/mizu/cli/goUtils"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"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"
|
||||
@@ -46,21 +50,21 @@ var state tapState
|
||||
func RunMizuTap() {
|
||||
mizuApiFilteringOptions, err := getMizuApiFilteringOptions()
|
||||
if err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error parsing regex-masking: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error parsing regex-masking: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
var mizuValidationRules string
|
||||
if mizu.Config.Tap.EnforcePolicyFile != "" {
|
||||
mizuValidationRules, err = readValidationRules(mizu.Config.Tap.EnforcePolicyFile)
|
||||
if config.Config.Tap.EnforcePolicyFile != "" {
|
||||
mizuValidationRules, err = readValidationRules(config.Config.Tap.EnforcePolicyFile)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading policy file: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading policy file: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.KubeConfigPath)
|
||||
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath)
|
||||
if err != nil {
|
||||
mizu.Log.Error(err)
|
||||
logger.Log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,36 +74,36 @@ func RunMizuTap() {
|
||||
targetNamespaces := getNamespaces(kubernetesProvider)
|
||||
|
||||
var namespacesStr string
|
||||
if targetNamespaces[0] != mizu.K8sAllNamespaces {
|
||||
if !mizu.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
|
||||
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
|
||||
} else {
|
||||
namespacesStr = "all namespaces"
|
||||
}
|
||||
mizu.CheckNewerVersion()
|
||||
mizu.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||
version.CheckNewerVersion()
|
||||
logger.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||
|
||||
if err, _ := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error getting pods by regex: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error getting pods by regex: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if len(state.currentlyTappedPods) == 0 {
|
||||
var suggestionStr string
|
||||
if targetNamespaces[0] != mizu.K8sAllNamespaces {
|
||||
if !mizu.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
|
||||
suggestionStr = ". Select a different namespace with -n or tap all namespaces with -A"
|
||||
}
|
||||
mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Did not find any pods matching the regex argument%s", suggestionStr))
|
||||
logger.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Did not find any pods matching the regex argument%s", suggestionStr))
|
||||
}
|
||||
|
||||
if mizu.Config.Tap.DryRun {
|
||||
if config.Config.Tap.DryRun {
|
||||
return
|
||||
}
|
||||
|
||||
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
||||
|
||||
defer cleanUpMizuResources(kubernetesProvider)
|
||||
defer cleanUpMizu(kubernetesProvider)
|
||||
if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions, mizuValidationRules); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error creating resources: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error creating resources: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +124,7 @@ func readValidationRules(file string) (string, error) {
|
||||
}
|
||||
|
||||
func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, mizuApiFilteringOptions *shared.TrafficFilteringOptions, mizuValidationRules string) error {
|
||||
if !mizu.Config.IsNsRestrictedMode() {
|
||||
if !config.Config.IsNsRestrictedMode() {
|
||||
if err := createMizuNamespace(ctx, kubernetesProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,7 +139,7 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
|
||||
if err := createMizuConfigmap(ctx, kubernetesProvider, mizuValidationRules); err != nil {
|
||||
mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to create resources required for policy validation. Mizu will not validate policy rules. error: %v\n", errormessage.FormatError(err)))
|
||||
logger.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to create resources required for policy validation. Mizu will not validate policy rules. error: %v\n", errormessage.FormatError(err)))
|
||||
state.doNotRemoveConfigMap = true
|
||||
} else if mizuValidationRules == "" {
|
||||
state.doNotRemoveConfigMap = true
|
||||
@@ -145,12 +149,12 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
|
||||
func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, data string) error {
|
||||
err := kubernetesProvider.CreateConfigMap(ctx, mizu.Config.MizuResourcesNamespace, mizu.ConfigMapName, data)
|
||||
err := kubernetesProvider.CreateConfigMap(ctx, config.Config.MizuResourcesNamespace, mizu.ConfigMapName, data)
|
||||
return err
|
||||
}
|
||||
|
||||
func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Provider) error {
|
||||
_, err := kubernetesProvider.CreateNamespace(ctx, mizu.Config.MizuResourcesNamespace)
|
||||
_, err := kubernetesProvider.CreateNamespace(ctx, config.Config.MizuResourcesNamespace)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -159,7 +163,7 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
|
||||
state.mizuServiceAccountExists, err = createRBACIfNecessary(ctx, kubernetesProvider)
|
||||
if err != nil {
|
||||
mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to ensure the resources required for IP resolving. Mizu will not resolve target IPs to names. error: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to ensure the resources required for IP resolving. Mizu will not resolve target IPs to names. error: %v", errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
var serviceAccountName string
|
||||
@@ -170,25 +174,25 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
|
||||
opts := &kubernetes.ApiServerOptions{
|
||||
Namespace: mizu.Config.MizuResourcesNamespace,
|
||||
Namespace: config.Config.MizuResourcesNamespace,
|
||||
PodName: mizu.ApiServerPodName,
|
||||
PodImage: mizu.Config.AgentImage,
|
||||
PodImage: config.Config.AgentImage,
|
||||
ServiceAccountName: serviceAccountName,
|
||||
IsNamespaceRestricted: mizu.Config.IsNsRestrictedMode(),
|
||||
IsNamespaceRestricted: config.Config.IsNsRestrictedMode(),
|
||||
MizuApiFilteringOptions: mizuApiFilteringOptions,
|
||||
MaxEntriesDBSizeBytes: mizu.Config.Tap.MaxEntriesDBSizeBytes(),
|
||||
MaxEntriesDBSizeBytes: config.Config.Tap.MaxEntriesDBSizeBytes(),
|
||||
}
|
||||
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts)
|
||||
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts, config.Config.Tap.ApiServerResources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mizu.Log.Debugf("Successfully created API server pod: %s", mizu.ApiServerPodName)
|
||||
logger.Log.Debugf("Successfully created API server pod: %s", mizu.ApiServerPodName)
|
||||
|
||||
state.apiServerService, err = kubernetesProvider.CreateService(ctx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName, mizu.ApiServerPodName)
|
||||
state.apiServerService, err = kubernetesProvider.CreateService(ctx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mizu.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName)
|
||||
logger.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -196,9 +200,9 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) {
|
||||
var compiledRegexSlice []*shared.SerializableRegexp
|
||||
|
||||
if mizu.Config.Tap.PlainTextFilterRegexes != nil && len(mizu.Config.Tap.PlainTextFilterRegexes) > 0 {
|
||||
if config.Config.Tap.PlainTextFilterRegexes != nil && len(config.Config.Tap.PlainTextFilterRegexes) > 0 {
|
||||
compiledRegexSlice = make([]*shared.SerializableRegexp, 0)
|
||||
for _, regexStr := range mizu.Config.Tap.PlainTextFilterRegexes {
|
||||
for _, regexStr := range config.Config.Tap.PlainTextFilterRegexes {
|
||||
compiledRegex, err := shared.CompileRegexToSerializableRegexp(regexStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -209,8 +213,8 @@ func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) {
|
||||
|
||||
return &shared.TrafficFilteringOptions{
|
||||
PlainTextMaskingRegexes: compiledRegexSlice,
|
||||
HealthChecksUserAgentHeaders: mizu.Config.Tap.HealthChecksUserAgentHeaders,
|
||||
DisableRedaction: mizu.Config.Tap.DisableRedaction,
|
||||
HealthChecksUserAgentHeaders: config.Config.Tap.HealthChecksUserAgentHeaders,
|
||||
DisableRedaction: config.Config.Tap.DisableRedaction,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -225,20 +229,21 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
|
||||
if err := kubernetesProvider.ApplyMizuTapperDaemonSet(
|
||||
ctx,
|
||||
mizu.Config.MizuResourcesNamespace,
|
||||
config.Config.MizuResourcesNamespace,
|
||||
mizu.TapperDaemonSetName,
|
||||
mizu.Config.AgentImage,
|
||||
config.Config.AgentImage,
|
||||
mizu.TapperPodName,
|
||||
fmt.Sprintf("%s.%s.svc.cluster.local", state.apiServerService.Name, state.apiServerService.Namespace),
|
||||
nodeToTappedPodIPMap,
|
||||
serviceAccountName,
|
||||
mizu.Config.Tap.TapOutgoing(),
|
||||
config.Config.Tap.TapOutgoing(),
|
||||
config.Config.Tap.TapperResources,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
mizu.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap))
|
||||
logger.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap))
|
||||
} else {
|
||||
if err := kubernetesProvider.RemoveDaemonSet(ctx, mizu.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||
if err := kubernetesProvider.RemoveDaemonSet(ctx, config.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -246,70 +251,74 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
|
||||
func cleanUpMizu(kubernetesProvider *kubernetes.Provider) {
|
||||
telemetry.ReportAPICalls(config.Config.Tap.GuiPort)
|
||||
cleanUpMizuResources(kubernetesProvider)
|
||||
}
|
||||
|
||||
func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
|
||||
removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
|
||||
defer cancel()
|
||||
|
||||
if mizu.Config.DumpLogs {
|
||||
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 {
|
||||
mizu.Log.Errorf("Failed dump logs %v", err)
|
||||
logger.Log.Errorf("Failed dump logs %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
mizu.Log.Infof("\nRemoving mizu resources\n")
|
||||
logger.Log.Infof("\nRemoving mizu resources\n")
|
||||
|
||||
if !mizu.Config.IsNsRestrictedMode() {
|
||||
if err := kubernetesProvider.RemoveNamespace(removalCtx, mizu.Config.MizuResourcesNamespace); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Namespace %s: %v", mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if !config.Config.IsNsRestrictedMode() {
|
||||
if err := kubernetesProvider.RemoveNamespace(removalCtx, config.Config.MizuResourcesNamespace); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Namespace %s: %v", config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := kubernetesProvider.RemovePod(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Pod %s in namespace %s: %v", mizu.ApiServerPodName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemovePod(removalCtx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Pod %s in namespace %s: %v", mizu.ApiServerPodName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
if err := kubernetesProvider.RemoveService(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service %s in namespace %s: %v", mizu.ApiServerPodName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveService(removalCtx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service %s in namespace %s: %v", mizu.ApiServerPodName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
if err := kubernetesProvider.RemoveDaemonSet(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing DaemonSet %s in namespace %s: %v", mizu.TapperDaemonSetName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveDaemonSet(removalCtx, config.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing DaemonSet %s in namespace %s: %v", mizu.TapperDaemonSetName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
if !state.doNotRemoveConfigMap {
|
||||
if err := kubernetesProvider.RemoveConfigMap(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ConfigMapName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing ConfigMap %s in namespace %s: %v", mizu.ConfigMapName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveConfigMap(removalCtx, config.Config.MizuResourcesNamespace, mizu.ConfigMapName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing ConfigMap %s in namespace %s: %v", mizu.ConfigMapName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if state.mizuServiceAccountExists {
|
||||
if !mizu.Config.IsNsRestrictedMode() {
|
||||
if !config.Config.IsNsRestrictedMode() {
|
||||
if err := kubernetesProvider.RemoveNonNamespacedResources(removalCtx, mizu.ClusterRoleName, mizu.ClusterRoleBindingName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing non-namespaced resources: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing non-namespaced resources: %v", errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := kubernetesProvider.RemoveServicAccount(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service Account %s in namespace %s: %v", mizu.ServiceAccountName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveServicAccount(removalCtx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service Account %s in namespace %s: %v", mizu.ServiceAccountName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err := kubernetesProvider.RemoveRole(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.RoleName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Role %s in namespace %s: %v", mizu.RoleName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveRole(removalCtx, config.Config.MizuResourcesNamespace, mizu.RoleName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Role %s in namespace %s: %v", mizu.RoleName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
|
||||
if err := kubernetesProvider.RemoveRoleBinding(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.RoleBindingName); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing RoleBinding %s in namespace %s: %v", mizu.RoleBindingName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
if err := kubernetesProvider.RemoveRoleBinding(removalCtx, config.Config.MizuResourcesNamespace, mizu.RoleBindingName); err != nil {
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing RoleBinding %s in namespace %s: %v", mizu.RoleBindingName, config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mizu.Config.IsNsRestrictedMode() {
|
||||
if !config.Config.IsNsRestrictedMode() {
|
||||
waitUntilNamespaceDeleted(removalCtx, cancel, kubernetesProvider)
|
||||
}
|
||||
}
|
||||
@@ -320,20 +329,20 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
|
||||
waitForFinish(ctx, cancel)
|
||||
}()
|
||||
|
||||
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, mizu.Config.MizuResourcesNamespace); err != nil {
|
||||
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, config.Config.MizuResourcesNamespace); err != nil {
|
||||
switch {
|
||||
case ctx.Err() == context.Canceled:
|
||||
// Do nothing. User interrupted the wait.
|
||||
case err == wait.ErrWaitTimeout:
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Timeout while removing Namespace %s", mizu.Config.MizuResourcesNamespace))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Timeout while removing Namespace %s", config.Config.MizuResourcesNamespace))
|
||||
default:
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error while waiting for Namespace %s to be deleted: %v", mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error while waiting for Namespace %s to be deleted: %v", config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reportTappedPods() {
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort)
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort)
|
||||
tappedPodsUrl := fmt.Sprintf("http://%s/status/tappedPods", mizuProxiedUrl)
|
||||
|
||||
podInfos := make([]shared.PodInfo, 0)
|
||||
@@ -343,30 +352,30 @@ func reportTappedPods() {
|
||||
tapStatus := shared.TapStatus{Pods: podInfos}
|
||||
|
||||
if jsonValue, err := json.Marshal(tapStatus); err != nil {
|
||||
mizu.Log.Debugf("[ERROR] failed Marshal the tapped pods %v", err)
|
||||
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 {
|
||||
mizu.Log.Debugf("[ERROR] failed sending to API server the tapped pods %v", err)
|
||||
logger.Log.Debugf("[ERROR] failed sending to API server the tapped pods %v", err)
|
||||
} else if response.StatusCode != 200 {
|
||||
mizu.Log.Debugf("[ERROR] failed sending to API server the tapped pods, response status code %v", response.StatusCode)
|
||||
logger.Log.Debugf("[ERROR] failed sending to API server the tapped pods, response status code %v", response.StatusCode)
|
||||
} else {
|
||||
mizu.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos))
|
||||
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, mizu.Config.Tap.PodRegex())
|
||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, targetNamespaces, config.Config.Tap.PodRegex())
|
||||
|
||||
restartTappers := func() {
|
||||
err, changeFound := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Failed to update currently tapped pods: %v", err))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Failed to update currently tapped pods: %v", err))
|
||||
cancel()
|
||||
}
|
||||
|
||||
if !changeFound {
|
||||
mizu.Log.Debugf("Nothing changed update tappers not needed")
|
||||
logger.Log.Debugf("Nothing changed update tappers not needed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -374,11 +383,11 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
|
||||
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error building node to ips map: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error building node to ips map: %v", errormessage.FormatError(err)))
|
||||
cancel()
|
||||
}
|
||||
if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap); err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error updating daemonset: %v", errormessage.FormatError(err)))
|
||||
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error updating daemonset: %v", errormessage.FormatError(err)))
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
@@ -387,13 +396,13 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
for {
|
||||
select {
|
||||
case pod := <-added:
|
||||
mizu.Log.Debugf("Added matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||
logger.Log.Debugf("Added matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||
restartTappersDebouncer.SetOn()
|
||||
case pod := <-removed:
|
||||
mizu.Log.Debugf("Removed matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||
logger.Log.Debugf("Removed matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||
restartTappersDebouncer.SetOn()
|
||||
case pod := <-modified:
|
||||
mizu.Log.Debugf("Modified matching pod %s, ns: %s, phase: %s, ip: %s", pod.Name, pod.Namespace, pod.Status.Phase, pod.Status.PodIP)
|
||||
logger.Log.Debugf("Modified matching pod %s, ns: %s, phase: %s, ip: %s", pod.Name, pod.Namespace, pod.Status.Phase, pod.Status.PodIP)
|
||||
// Act only if the modified pod has already obtained an IP address.
|
||||
// After filtering for IPs, on a normal pod restart this includes the following events:
|
||||
// - Pod deletion
|
||||
@@ -405,13 +414,13 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
}
|
||||
|
||||
case err := <-errorChan:
|
||||
mizu.Log.Debugf("Watching pods loop, got error %v, stopping `restart tappers debouncer`", err)
|
||||
logger.Log.Debugf("Watching pods loop, got error %v, stopping `restart tappers debouncer`", err)
|
||||
restartTappersDebouncer.Cancel()
|
||||
// TODO: Does this also perform cleanup?
|
||||
cancel()
|
||||
|
||||
case <-ctx.Done():
|
||||
mizu.Log.Debugf("Watching pods loop, context done, stopping `restart tappers debouncer`")
|
||||
logger.Log.Debugf("Watching pods loop, context done, stopping `restart tappers debouncer`")
|
||||
restartTappersDebouncer.Cancel()
|
||||
return
|
||||
}
|
||||
@@ -420,18 +429,18 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
||||
|
||||
func updateCurrentlyTappedPods(kubernetesProvider *kubernetes.Provider, ctx context.Context, targetNamespaces []string) (error, bool) {
|
||||
changeFound := false
|
||||
if matchingPods, err := kubernetesProvider.ListAllRunningPodsMatchingRegex(ctx, mizu.Config.Tap.PodRegex(), targetNamespaces); err != nil {
|
||||
if matchingPods, err := kubernetesProvider.ListAllRunningPodsMatchingRegex(ctx, config.Config.Tap.PodRegex(), targetNamespaces); err != nil {
|
||||
return err, false
|
||||
} else {
|
||||
podsToTap := excludeMizuPods(matchingPods)
|
||||
addedPods, removedPods := getPodArrayDiff(state.currentlyTappedPods, podsToTap)
|
||||
for _, addedPod := range addedPods {
|
||||
changeFound = true
|
||||
mizu.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", addedPod.Name))
|
||||
logger.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", addedPod.Name))
|
||||
}
|
||||
for _, removedPod := range removedPods {
|
||||
changeFound = true
|
||||
mizu.Log.Infof(uiUtils.Red, fmt.Sprintf("-%s", removedPod.Name))
|
||||
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("-%s", removedPod.Name))
|
||||
}
|
||||
state.currentlyTappedPods = podsToTap
|
||||
}
|
||||
@@ -479,86 +488,86 @@ func getMissingPods(pods1 []core.Pod, pods2 []core.Pod) []core.Pod {
|
||||
|
||||
func createProxyToApiServerPod(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{mizu.Config.MizuResourcesNamespace}, podExactRegex)
|
||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, []string{config.Config.MizuResourcesNamespace}, podExactRegex)
|
||||
isPodReady := false
|
||||
timeAfter := time.After(25 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
mizu.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||
logger.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||
return
|
||||
case <-added:
|
||||
mizu.Log.Debugf("Watching API Server pod loop, added")
|
||||
logger.Log.Debugf("Watching API Server pod loop, added")
|
||||
continue
|
||||
case <-removed:
|
||||
mizu.Log.Infof("%s removed", mizu.ApiServerPodName)
|
||||
logger.Log.Infof("%s removed", mizu.ApiServerPodName)
|
||||
cancel()
|
||||
return
|
||||
case modifiedPod := <-modified:
|
||||
if modifiedPod == nil {
|
||||
mizu.Log.Debugf("Watching API Server pod loop, modifiedPod with nil")
|
||||
logger.Log.Debugf("Watching API Server pod loop, modifiedPod with nil")
|
||||
continue
|
||||
}
|
||||
mizu.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase)
|
||||
logger.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase)
|
||||
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
|
||||
isPodReady = true
|
||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort))
|
||||
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()
|
||||
}
|
||||
case <-timeAfter:
|
||||
if !isPodReady {
|
||||
mizu.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time")
|
||||
logger.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time")
|
||||
cancel()
|
||||
}
|
||||
case <-errorChan:
|
||||
mizu.Log.Debugf("[ERROR] Agent creation, watching %v namespace", mizu.Config.MizuResourcesNamespace)
|
||||
logger.Log.Debugf("[ERROR] Agent creation, watching %v namespace", config.Config.MizuResourcesNamespace)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||
err := kubernetes.StartProxy(kubernetesProvider, mizu.Config.Tap.GuiPort, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running k8s proxy %v\n"+
|
||||
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() {
|
||||
if !mizu.Config.Tap.Analysis {
|
||||
if !config.Config.Tap.Analysis {
|
||||
return
|
||||
}
|
||||
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort)
|
||||
urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(mizu.Config.Tap.AnalysisDestination), mizu.Config.Tap.SleepIntervalSec)
|
||||
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 {
|
||||
mizu.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr)
|
||||
logger.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr)
|
||||
}
|
||||
|
||||
mizu.Log.Debugf("Sending get request to %v", u.String())
|
||||
logger.Log.Debugf("Sending get request to %v", u.String())
|
||||
if response, requestErr := http.Get(u.String()); requestErr != nil {
|
||||
mizu.Log.Errorf("Failed to notify agent for analysis, err: %v", requestErr)
|
||||
logger.Log.Errorf("Failed to notify agent for analysis, err: %v", requestErr)
|
||||
} else if response.StatusCode != 200 {
|
||||
mizu.Log.Errorf("Failed to notify agent for analysis, status code: %v", response.StatusCode)
|
||||
logger.Log.Errorf("Failed to notify agent for analysis, status code: %v", response.StatusCode)
|
||||
} else {
|
||||
mizu.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
|
||||
logger.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis")
|
||||
}
|
||||
}
|
||||
|
||||
func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) (bool, error) {
|
||||
if !mizu.Config.IsNsRestrictedMode() {
|
||||
err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
||||
if !config.Config.IsNsRestrictedMode() {
|
||||
err := kubernetesProvider.CreateMizuRBAC(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
err := kubernetesProvider.CreateMizuRBACNamespaceRestricted(ctx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.RoleName, mizu.RoleBindingName, mizu.RBACVersion)
|
||||
err := kubernetesProvider.CreateMizuRBACNamespaceRestricted(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.RoleName, mizu.RoleBindingName, mizu.RBACVersion)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -593,10 +602,10 @@ func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
|
||||
}
|
||||
|
||||
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
|
||||
if mizu.Config.Tap.AllNamespaces {
|
||||
if config.Config.Tap.AllNamespaces {
|
||||
return []string{mizu.K8sAllNamespaces}
|
||||
} else if len(mizu.Config.Tap.Namespaces) > 0 {
|
||||
return mizu.Config.Tap.Namespaces
|
||||
} else if len(config.Config.Tap.Namespaces) > 0 {
|
||||
return config.Config.Tap.Namespaces
|
||||
} else {
|
||||
return []string{kubernetesProvider.CurrentNamespace()}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"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"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version info",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
go mizu.ReportRun("version", mizu.Config.Version)
|
||||
if mizu.Config.Version.DebugInfo {
|
||||
go telemetry.ReportRun("version", config.Config.Version)
|
||||
|
||||
if config.Config.Version.DebugInfo {
|
||||
timeStampInt, _ := strconv.ParseInt(mizu.BuildTimestamp, 10, 0)
|
||||
mizu.Log.Infof("Version: %s \nBranch: %s (%s)", mizu.SemVer, mizu.Branch, mizu.GitCommitHash)
|
||||
mizu.Log.Infof("Build Time: %s (%s)", mizu.BuildTimestamp, time.Unix(timeStampInt, 0))
|
||||
logger.Log.Infof("Version: %s \nBranch: %s (%s)", mizu.SemVer, mizu.Branch, mizu.GitCommitHash)
|
||||
logger.Log.Infof("Build Time: %s (%s)", mizu.BuildTimestamp, time.Unix(timeStampInt, 0))
|
||||
|
||||
} else {
|
||||
mizu.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch)
|
||||
logger.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -3,15 +3,16 @@ package cmd
|
||||
import (
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/telemetry"
|
||||
)
|
||||
|
||||
var viewCmd = &cobra.Command{
|
||||
Use: "view",
|
||||
Short: "Open GUI in browser",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
go mizu.ReportRun("view", mizu.Config.View)
|
||||
go telemetry.ReportRun("view", config.Config.View)
|
||||
runMizuView()
|
||||
return nil
|
||||
},
|
||||
|
||||
@@ -3,46 +3,49 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func runMizuView() {
|
||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath)
|
||||
kubernetesProvider, err := kubernetes.NewProvider(config.Config.View.KubeConfigPath)
|
||||
if err != nil {
|
||||
mizu.Log.Error(err)
|
||||
logger.Log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
exists, err := kubernetesProvider.DoesServicesExist(ctx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||
exists, err := kubernetesProvider.DoesServicesExist(ctx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf("Failed to found mizu service %v", err)
|
||||
logger.Log.Errorf("Failed to found mizu service %v", err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
mizu.Log.Infof("%s service not found, you should run `mizu tap` command first", mizu.ApiServerPodName)
|
||||
logger.Log.Infof("%s service not found, you should run `mizu tap` command first", mizu.ApiServerPodName)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort)
|
||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.View.GuiPort)
|
||||
_, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl))
|
||||
if err == nil {
|
||||
mizu.Log.Infof("Found a running service %s and open port %d", mizu.ApiServerPodName, mizu.Config.View.GuiPort)
|
||||
logger.Log.Infof("Found a running service %s and open port %d", mizu.ApiServerPodName, config.Config.View.GuiPort)
|
||||
return
|
||||
}
|
||||
mizu.Log.Debugf("Found service %s, creating k8s proxy", mizu.ApiServerPodName)
|
||||
logger.Log.Debugf("Found service %s, creating k8s proxy", mizu.ApiServerPodName)
|
||||
|
||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||
|
||||
mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort))
|
||||
if isCompatible, err := mizu.CheckVersionCompatibility(mizu.Config.View.GuiPort); err != nil {
|
||||
mizu.Log.Errorf("Failed to check versions compatibility %v", err)
|
||||
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 {
|
||||
logger.Log.Errorf("Failed to check versions compatibility %v", err)
|
||||
cancel()
|
||||
return
|
||||
} else if !isCompatible {
|
||||
|
||||
337
cli/config/config.go
Normal file
337
cli/config/config.go
Normal file
@@ -0,0 +1,337 @@
|
||||
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"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
Separator = "="
|
||||
SetCommandName = "set"
|
||||
FieldNameTag = "yaml"
|
||||
ReadonlyTag = "readonly"
|
||||
)
|
||||
|
||||
var (
|
||||
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()
|
||||
|
||||
if err := defaults.Set(&Config); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
cmd.Flags().Visit(initFlag)
|
||||
|
||||
finalConfigPrettified, _ := uiUtils.PrettyJson(Config)
|
||||
logger.Log.Debugf("Init config finished\n Final config: %v", finalConfigPrettified)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetConfigWithDefaults() (string, error) {
|
||||
defaultConf := ConfigStruct{}
|
||||
if err := defaults.Set(&defaultConf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
configElem := reflect.ValueOf(&defaultConf).Elem()
|
||||
setZeroForReadonlyFields(configElem)
|
||||
|
||||
return uiUtils.PrettyYaml(defaultConf)
|
||||
}
|
||||
|
||||
func GetConfigFilePath() string {
|
||||
return path.Join(mizu.GetMizuFolderPath(), "config.yaml")
|
||||
}
|
||||
|
||||
func mergeConfigFile() error {
|
||||
reader, openErr := os.Open(GetConfigFilePath())
|
||||
if openErr != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf, readErr := ioutil.ReadAll(reader)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(buf, &Config); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Log.Debugf("Found config file, merged to default options")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initFlag(f *pflag.Flag) {
|
||||
configElemValue := reflect.ValueOf(&Config).Elem()
|
||||
|
||||
flagPath := []string {cmdName, f.Name}
|
||||
|
||||
sliceValue, isSliceValue := f.Value.(pflag.SliceValue)
|
||||
if !isSliceValue {
|
||||
if err := mergeFlagValue(configElemValue, flagPath, strings.Join(flagPath, "."), f.Value.String()); err != nil {
|
||||
logger.Log.Warningf(uiUtils.Warning, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if f.Name == SetCommandName {
|
||||
if err := mergeSetFlag(configElemValue, sliceValue.GetSlice()); err != nil {
|
||||
logger.Log.Warningf(uiUtils.Warning, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := mergeFlagValues(configElemValue, flagPath, strings.Join(flagPath, "."), sliceValue.GetSlice()); err != nil {
|
||||
logger.Log.Warningf(uiUtils.Warning, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeSetFlag(configElemValue reflect.Value, setValues []string) error {
|
||||
var setErrors []string
|
||||
setMap := map[string][]string{}
|
||||
|
||||
for _, setValue := range setValues {
|
||||
if !strings.Contains(setValue, Separator) {
|
||||
setErrors = append(setErrors, fmt.Sprintf("Ignoring set argument %s (set argument format: <flag name>=<flag value>)", setValue))
|
||||
continue
|
||||
}
|
||||
|
||||
split := strings.SplitN(setValue, Separator, 2)
|
||||
argumentKey, argumentValue := split[0], split[1]
|
||||
|
||||
setMap[argumentKey] = append(setMap[argumentKey], argumentValue)
|
||||
}
|
||||
|
||||
for argumentKey, argumentValues := range setMap {
|
||||
flagPath := strings.Split(argumentKey, ".")
|
||||
|
||||
if len(argumentValues) > 1 {
|
||||
if err := mergeFlagValues(configElemValue, flagPath, argumentKey, argumentValues); err != nil {
|
||||
setErrors = append(setErrors, fmt.Sprintf("%v", err))
|
||||
}
|
||||
} else {
|
||||
if err := mergeFlagValue(configElemValue, flagPath, argumentKey, argumentValues[0]); err != nil {
|
||||
setErrors = append(setErrors, fmt.Sprintf("%v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(setErrors) > 0 {
|
||||
return fmt.Errorf(strings.Join(setErrors, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeFlagValue(configElemValue reflect.Value, flagPath []string, fullFlagName string, flagValue string) error {
|
||||
mergeFunction := func(flagName string, currentFieldStruct reflect.StructField, currentFieldElemValue reflect.Value, currentElemValue reflect.Value) error {
|
||||
currentFieldKind := currentFieldStruct.Type.Kind()
|
||||
|
||||
if currentFieldKind == reflect.Slice {
|
||||
return mergeFlagValues(currentElemValue, []string{flagName}, fullFlagName, []string{flagValue})
|
||||
}
|
||||
|
||||
parsedValue, err := getParsedValue(currentFieldKind, flagValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value %s for flag name %s, expected %s", flagValue, flagName, currentFieldKind)
|
||||
}
|
||||
|
||||
currentFieldElemValue.Set(parsedValue)
|
||||
return nil
|
||||
}
|
||||
|
||||
return mergeFlag(configElemValue, flagPath, fullFlagName, mergeFunction)
|
||||
}
|
||||
|
||||
func mergeFlagValues(configElemValue reflect.Value, flagPath []string, fullFlagName string, flagValues []string) error {
|
||||
mergeFunction := func(flagName string, currentFieldStruct reflect.StructField, currentFieldElemValue reflect.Value, currentElemValue reflect.Value) error {
|
||||
currentFieldKind := currentFieldStruct.Type.Kind()
|
||||
|
||||
if currentFieldKind != reflect.Slice {
|
||||
return fmt.Errorf("invalid values %s for flag name %s, expected %s", strings.Join(flagValues, ","), flagName, currentFieldKind)
|
||||
}
|
||||
|
||||
flagValueKind := currentFieldStruct.Type.Elem().Kind()
|
||||
|
||||
parsedValues := reflect.MakeSlice(reflect.SliceOf(currentFieldStruct.Type.Elem()), 0, 0)
|
||||
for _, flagValue := range flagValues {
|
||||
parsedValue, err := getParsedValue(flagValueKind, flagValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value %s for flag name %s, expected %s", flagValue, flagName, flagValueKind)
|
||||
}
|
||||
|
||||
parsedValues = reflect.Append(parsedValues, parsedValue)
|
||||
}
|
||||
|
||||
currentFieldElemValue.Set(parsedValues)
|
||||
return nil
|
||||
}
|
||||
|
||||
return mergeFlag(configElemValue, flagPath, fullFlagName, mergeFunction)
|
||||
}
|
||||
|
||||
func mergeFlag(currentElemValue reflect.Value, currentFlagPath []string, fullFlagName string, mergeFunction func(flagName string, currentFieldStruct reflect.StructField, currentFieldElemValue reflect.Value, currentElemValue reflect.Value) error) error {
|
||||
if len(currentFlagPath) == 0 {
|
||||
return fmt.Errorf("flag \"%s\" not found", fullFlagName)
|
||||
}
|
||||
|
||||
for i := 0; i < currentElemValue.NumField(); i++ {
|
||||
currentFieldStruct := currentElemValue.Type().Field(i)
|
||||
currentFieldElemValue := currentElemValue.FieldByName(currentFieldStruct.Name)
|
||||
|
||||
if currentFieldStruct.Type.Kind() == reflect.Struct && getFieldNameByTag(currentFieldStruct) == currentFlagPath[0] {
|
||||
return mergeFlag(currentFieldElemValue, currentFlagPath[1:], fullFlagName, mergeFunction)
|
||||
}
|
||||
|
||||
if len(currentFlagPath) > 1 || getFieldNameByTag(currentFieldStruct) != currentFlagPath[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
return mergeFunction(currentFlagPath[0], currentFieldStruct, currentFieldElemValue, currentElemValue)
|
||||
}
|
||||
|
||||
return fmt.Errorf("flag \"%s\" not found", fullFlagName)
|
||||
}
|
||||
|
||||
func getFieldNameByTag(field reflect.StructField) string {
|
||||
return strings.Split(field.Tag.Get(FieldNameTag), ",")[0]
|
||||
}
|
||||
|
||||
func getParsedValue(kind reflect.Kind, value string) (reflect.Value, error) {
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(value), nil
|
||||
case reflect.Bool:
|
||||
boolArgumentValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(boolArgumentValue), nil
|
||||
case reflect.Int:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int(intArgumentValue)), nil
|
||||
case reflect.Int8:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 8)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int8(intArgumentValue)), nil
|
||||
case reflect.Int16:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 16)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int16(intArgumentValue)), nil
|
||||
case reflect.Int32:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 32)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int32(intArgumentValue)), nil
|
||||
case reflect.Int64:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(intArgumentValue), nil
|
||||
case reflect.Uint:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint(uintArgumentValue)), nil
|
||||
case reflect.Uint8:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 8)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint8(uintArgumentValue)), nil
|
||||
case reflect.Uint16:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint16(uintArgumentValue)), nil
|
||||
case reflect.Uint32:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint32(uintArgumentValue)), nil
|
||||
case reflect.Uint64:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uintArgumentValue), nil
|
||||
}
|
||||
|
||||
return reflect.ValueOf(nil), errors.New("value to parse does not match type")
|
||||
}
|
||||
|
||||
func setZeroForReadonlyFields(currentElem reflect.Value) {
|
||||
for i := 0; i < currentElem.NumField(); i++ {
|
||||
currentField := currentElem.Type().Field(i)
|
||||
currentFieldByName := currentElem.FieldByName(currentField.Name)
|
||||
|
||||
if currentField.Type.Kind() == reflect.Struct {
|
||||
setZeroForReadonlyFields(currentFieldByName)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := currentField.Tag.Lookup(ReadonlyTag); ok {
|
||||
currentFieldByName.Set(reflect.Zero(currentField.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
package mizu
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
)
|
||||
|
||||
const (
|
||||
AgentImageConfigName = "agent-image"
|
||||
MizuResourcesNamespaceConfigName = "mizu-resources-namespace"
|
||||
TelemetryConfigName = "telemetry"
|
||||
DumpLogsConfigName = "dump-logs"
|
||||
KubeConfigPathName = "kube-config-path"
|
||||
)
|
||||
|
||||
type ConfigStruct struct {
|
||||
@@ -19,7 +15,7 @@ type ConfigStruct struct {
|
||||
Fetch configStructs.FetchConfig `yaml:"fetch"`
|
||||
Version configStructs.VersionConfig `yaml:"version"`
|
||||
View configStructs.ViewConfig `yaml:"view"`
|
||||
AgentImage string `yaml:"agent-image,omitempty"`
|
||||
AgentImage string `yaml:"agent-image,omitempty" readonly:""`
|
||||
MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"`
|
||||
Telemetry bool `yaml:"telemetry" default:"true"`
|
||||
DumpLogs bool `yaml:"dump-logs" default:"false"`
|
||||
@@ -27,7 +23,7 @@ type ConfigStruct struct {
|
||||
}
|
||||
|
||||
func (config *ConfigStruct) SetDefaults() {
|
||||
config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", Branch, SemVer)
|
||||
config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer)
|
||||
}
|
||||
|
||||
func (config *ConfigStruct) IsNsRestrictedMode() bool {
|
||||
@@ -10,15 +10,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AnalysisDestinationTapName = "dest"
|
||||
SleepIntervalSecTapName = "upload-interval"
|
||||
GuiPortTapName = "gui-port"
|
||||
NamespacesTapName = "namespaces"
|
||||
AnalysisTapName = "analysis"
|
||||
AllNamespacesTapName = "all-namespaces"
|
||||
PlainTextFilterRegexesTapName = "regex-masking"
|
||||
DisableRedactionTapName = "no-redact"
|
||||
IgnoredUserAgentsTapName = "ignored-user-agents"
|
||||
HumanMaxEntriesDBSizeTapName = "max-entries-db-size"
|
||||
DirectionTapName = "direction"
|
||||
DryRunTapName = "dry-run"
|
||||
@@ -26,20 +23,29 @@ const (
|
||||
)
|
||||
|
||||
type TapConfig struct {
|
||||
AnalysisDestination string `yaml:"dest" default:"up9.app"`
|
||||
SleepIntervalSec int `yaml:"upload-interval" default:"10"`
|
||||
PodRegexStr string `yaml:"regex" default:".*"`
|
||||
GuiPort uint16 `yaml:"gui-port" default:"8899"`
|
||||
Namespaces []string `yaml:"namespaces"`
|
||||
Analysis bool `yaml:"analysis" default:"false"`
|
||||
AllNamespaces bool `yaml:"all-namespaces" default:"false"`
|
||||
PlainTextFilterRegexes []string `yaml:"regex-masking"`
|
||||
HealthChecksUserAgentHeaders []string `yaml:"ignored-user-agents" default:"[]"`
|
||||
DisableRedaction bool `yaml:"no-redact" default:"false"`
|
||||
HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"`
|
||||
Direction string `yaml:"direction" default:"in"`
|
||||
DryRun bool `yaml:"dry-run" default:"false"`
|
||||
EnforcePolicyFile string `yaml:"test-rules"`
|
||||
AnalysisDestination string `yaml:"dest" default:"up9.app"`
|
||||
SleepIntervalSec int `yaml:"upload-interval" default:"10"`
|
||||
PodRegexStr string `yaml:"regex" default:".*"`
|
||||
GuiPort uint16 `yaml:"gui-port" default:"8899"`
|
||||
Namespaces []string `yaml:"namespaces"`
|
||||
Analysis bool `yaml:"analysis" default:"false"`
|
||||
AllNamespaces bool `yaml:"all-namespaces" default:"false"`
|
||||
PlainTextFilterRegexes []string `yaml:"regex-masking"`
|
||||
HealthChecksUserAgentHeaders []string `yaml:"ignored-user-agents"`
|
||||
DisableRedaction bool `yaml:"no-redact" default:"false"`
|
||||
HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"`
|
||||
Direction string `yaml:"direction" default:"in"`
|
||||
DryRun bool `yaml:"dry-run" default:"false"`
|
||||
EnforcePolicyFile string `yaml:"test-rules"`
|
||||
ApiServerResources Resources `yaml:"api-server-resources"`
|
||||
TapperResources Resources `yaml:"tapper-resources"`
|
||||
}
|
||||
|
||||
type Resources struct {
|
||||
CpuLimit string `yaml:"cpu-limit" default:"750m"`
|
||||
MemoryLimit string `yaml:"memory-limit" default:"1Gi"`
|
||||
CpuRequests string `yaml:"cpu-requests" default:"50m"`
|
||||
MemoryRequests string `yaml:"memory-requests" default:"50Mi"`
|
||||
}
|
||||
|
||||
func (config *TapConfig) PodRegex() *regexp.Regexp {
|
||||
385
cli/config/config_internal_test.go
Normal file
385
cli/config/config_internal_test.go
Normal file
@@ -0,0 +1,385 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ConfigMock struct {
|
||||
SectionMock SectionMock `yaml:"section"`
|
||||
Test string `yaml:"test"`
|
||||
StringField string `yaml:"string-field"`
|
||||
IntField int `yaml:"int-field"`
|
||||
BoolField bool `yaml:"bool-field"`
|
||||
UintField uint `yaml:"uint-field"`
|
||||
StringSliceField []string `yaml:"string-slice-field"`
|
||||
IntSliceField []int `yaml:"int-slice-field"`
|
||||
BoolSliceField []bool `yaml:"bool-slice-field"`
|
||||
UintSliceField []uint `yaml:"uint-slice-field"`
|
||||
}
|
||||
|
||||
type SectionMock struct {
|
||||
Test string `yaml:"test"`
|
||||
}
|
||||
|
||||
type FieldSetValues struct {
|
||||
SetValues []string
|
||||
FieldName string
|
||||
FieldValue interface{}
|
||||
}
|
||||
|
||||
func TestMergeSetFlagNoSeparator(t *testing.T) {
|
||||
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 _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
err := mergeSetFlag(configMockElemValue, test.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 := []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 _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
err := mergeSetFlag(configMockElemValue, test.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 := []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 _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
err := mergeSetFlag(configMockElemValue, test.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 {
|
||||
Name string
|
||||
FieldsSetValues []FieldSetValues
|
||||
}{
|
||||
{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 {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
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 {
|
||||
Name string
|
||||
FieldsSetValues []FieldSetValues
|
||||
}{
|
||||
{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)}},
|
||||
}},
|
||||
{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 {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
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 !reflect.DeepEqual(fieldValue, fieldSetValues.FieldValue) {
|
||||
t.Errorf("unexpected result - expected: %v, actual: %v", fieldSetValues.FieldValue, fieldValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeSetFlagMixValues(t *testing.T) {
|
||||
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}},
|
||||
{SetValues: []string{"uint-slice-field=6"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6)}},
|
||||
{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)},
|
||||
}},
|
||||
{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}},
|
||||
{SetValues: []string{"uint-slice-field=6", "uint-slice-field=66"}, FieldName: "UintSliceField", FieldValue: []uint{uint(6), uint(66)}},
|
||||
{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 {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
configMock := ConfigMock{}
|
||||
configMockElemValue := reflect.ValueOf(&configMock).Elem()
|
||||
|
||||
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 !reflect.DeepEqual(fieldValue, fieldSetValues.FieldValue) {
|
||||
t.Errorf("unexpected result - expected: %v, actual: %v", fieldSetValues.FieldValue, fieldValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetParsedValueValidValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
StringValue string
|
||||
Kind reflect.Kind
|
||||
ActualValue interface{}
|
||||
}{
|
||||
{StringValue: "test", Kind: reflect.String, ActualValue: "test"},
|
||||
{StringValue: "123", Kind: reflect.String, ActualValue: "123"},
|
||||
{StringValue: "true", Kind: reflect.Bool, ActualValue: true},
|
||||
{StringValue: "false", Kind: reflect.Bool, ActualValue: false},
|
||||
{StringValue: "6", Kind: reflect.Int, ActualValue: 6},
|
||||
{StringValue: "-6", Kind: reflect.Int, ActualValue: -6},
|
||||
{StringValue: "6", Kind: reflect.Int8, ActualValue: int8(6)},
|
||||
{StringValue: "-6", Kind: reflect.Int8, ActualValue: int8(-6)},
|
||||
{StringValue: "6", Kind: reflect.Int16, ActualValue: int16(6)},
|
||||
{StringValue: "-6", Kind: reflect.Int16, ActualValue: int16(-6)},
|
||||
{StringValue: "6", Kind: reflect.Int32, ActualValue: int32(6)},
|
||||
{StringValue: "-6", Kind: reflect.Int32, ActualValue: int32(-6)},
|
||||
{StringValue: "6", Kind: reflect.Int64, ActualValue: int64(6)},
|
||||
{StringValue: "-6", Kind: reflect.Int64, ActualValue: int64(-6)},
|
||||
{StringValue: "6", Kind: reflect.Uint, ActualValue: uint(6)},
|
||||
{StringValue: "66", Kind: reflect.Uint, ActualValue: uint(66)},
|
||||
{StringValue: "6", Kind: reflect.Uint8, ActualValue: uint8(6)},
|
||||
{StringValue: "66", Kind: reflect.Uint8, ActualValue: uint8(66)},
|
||||
{StringValue: "6", Kind: reflect.Uint16, ActualValue: uint16(6)},
|
||||
{StringValue: "66", Kind: reflect.Uint16, ActualValue: uint16(66)},
|
||||
{StringValue: "6", Kind: reflect.Uint32, ActualValue: uint32(6)},
|
||||
{StringValue: "66", Kind: reflect.Uint32, ActualValue: uint32(66)},
|
||||
{StringValue: "6", Kind: reflect.Uint64, ActualValue: uint64(6)},
|
||||
{StringValue: "66", Kind: reflect.Uint64, ActualValue: uint64(66)},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedValue.Interface() != test.ActualValue {
|
||||
t.Errorf("unexpected result - expected: %v, actual: %v", test.ActualValue, parsedValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetParsedValueInvalidValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
StringValue string
|
||||
Kind reflect.Kind
|
||||
}{
|
||||
{StringValue: "test", Kind: reflect.Bool},
|
||||
{StringValue: "123", Kind: reflect.Bool},
|
||||
{StringValue: "test", Kind: reflect.Int},
|
||||
{StringValue: "true", Kind: reflect.Int},
|
||||
{StringValue: "test", Kind: reflect.Int8},
|
||||
{StringValue: "true", Kind: reflect.Int8},
|
||||
{StringValue: "test", Kind: reflect.Int16},
|
||||
{StringValue: "true", Kind: reflect.Int16},
|
||||
{StringValue: "test", Kind: reflect.Int32},
|
||||
{StringValue: "true", Kind: reflect.Int32},
|
||||
{StringValue: "test", Kind: reflect.Int64},
|
||||
{StringValue: "true", Kind: reflect.Int64},
|
||||
{StringValue: "test", Kind: reflect.Uint},
|
||||
{StringValue: "-6", Kind: reflect.Uint},
|
||||
{StringValue: "test", Kind: reflect.Uint8},
|
||||
{StringValue: "-6", Kind: reflect.Uint8},
|
||||
{StringValue: "test", Kind: reflect.Uint16},
|
||||
{StringValue: "-6", Kind: reflect.Uint16},
|
||||
{StringValue: "test", Kind: reflect.Uint32},
|
||||
{StringValue: "-6", Kind: reflect.Uint32},
|
||||
{StringValue: "test", Kind: reflect.Uint64},
|
||||
{StringValue: "-6", Kind: reflect.Uint64},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if parsedValue != reflect.ValueOf(nil) {
|
||||
t.Errorf("unexpected parsed value - parsedValue: %v", parsedValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
cli/config/config_test.go
Normal file
41
cli/config/config_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package config_test
|
||||
|
||||
import (
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigWriteIgnoresReadonlyFields(t *testing.T) {
|
||||
var readonlyFields []string
|
||||
|
||||
configElem := reflect.ValueOf(&config.ConfigStruct{}).Elem()
|
||||
getFieldsWithReadonlyTag(configElem, &readonlyFields)
|
||||
|
||||
configWithDefaults, _ := config.GetConfigWithDefaults()
|
||||
for _, readonlyField := range readonlyFields {
|
||||
t.Run(readonlyField, func(t *testing.T) {
|
||||
if strings.Contains(configWithDefaults, readonlyField) {
|
||||
t.Errorf("unexpected result - readonly field: %v, config: %v", readonlyField, configWithDefaults)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getFieldsWithReadonlyTag(currentElem reflect.Value, readonlyFields *[]string) {
|
||||
for i := 0; i < currentElem.NumField(); i++ {
|
||||
currentField := currentElem.Type().Field(i)
|
||||
currentFieldByName := currentElem.FieldByName(currentField.Name)
|
||||
|
||||
if currentField.Type.Kind() == reflect.Struct {
|
||||
getFieldsWithReadonlyTag(currentFieldByName, readonlyFields)
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := currentField.Tag.Lookup(config.ReadonlyTag); ok {
|
||||
fieldNameByTag := strings.Split(currentField.Tag.Get(config.FieldNameTag), ",")[0]
|
||||
*readonlyFields = append(*readonlyFields, fieldNameByTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@ package errormessage
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
regexpsyntax "regexp/syntax"
|
||||
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -20,9 +18,9 @@ func FormatError(err error) error {
|
||||
"supply the required permission or control Mizu's access to namespaces by setting %s "+
|
||||
"in the config file or setting the tapped namespace with --%s %s=<NAMEPSACE>",
|
||||
err,
|
||||
mizu.MizuResourcesNamespaceConfigName,
|
||||
mizu.SetCommandName,
|
||||
mizu.MizuResourcesNamespaceConfigName)
|
||||
config.MizuResourcesNamespaceConfigName,
|
||||
config.SetCommandName,
|
||||
config.MizuResourcesNamespaceConfigName)
|
||||
} else if syntaxError, isSyntaxError := asRegexSyntaxError(err); isSyntaxError {
|
||||
errorNew = fmt.Errorf("regex %s is invalid: %w", syntaxError.Expr, err)
|
||||
} else {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package fsUtils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func DumpLogs(provider *kubernetes.Provider, ctx context.Context, filePath string) error {
|
||||
podExactRegex := regexp.MustCompile("^" + mizu.MizuResourcesPrefix)
|
||||
pods, err := provider.ListAllPodsMatchingRegex(ctx, podExactRegex, []string{mizu.Config.MizuResourcesNamespace})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pods) == 0 {
|
||||
return fmt.Errorf("no mizu pods found in namespace %s", mizu.Config.MizuResourcesNamespace)
|
||||
}
|
||||
|
||||
newZipFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, pod := range pods {
|
||||
logs, err := provider.GetPodLogs(pod.Namespace, pod.Name, ctx)
|
||||
if err != nil {
|
||||
mizu.Log.Errorf("Failed to get logs, %v", err)
|
||||
continue
|
||||
} else {
|
||||
mizu.Log.Debugf("Successfully read log length %d for pod: %s.%s", len(logs), pod.Namespace, pod.Name)
|
||||
}
|
||||
if err := AddStrToZip(zipWriter, logs, fmt.Sprintf("%s.%s.log", pod.Namespace, pod.Name)); err != nil {
|
||||
mizu.Log.Errorf("Failed write logs, %v", err)
|
||||
} else {
|
||||
mizu.Log.Infof("Successfully added log length %d from pod: %s.%s", len(logs), pod.Namespace, pod.Name)
|
||||
}
|
||||
}
|
||||
if err := AddFileToZip(zipWriter, mizu.GetConfigFilePath()); err != nil {
|
||||
mizu.Log.Debugf("Failed write file, %v", err)
|
||||
} else {
|
||||
mizu.Log.Infof("Successfully added file %s", mizu.GetConfigFilePath())
|
||||
}
|
||||
if err := AddFileToZip(zipWriter, mizu.GetLogFilePath()); err != nil {
|
||||
mizu.Log.Debugf("Failed write file, %v", err)
|
||||
} else {
|
||||
mizu.Log.Infof("Successfully added file %s", mizu.GetLogFilePath())
|
||||
}
|
||||
mizu.Log.Infof("You can find the zip with all logs in %s\n", filePath)
|
||||
return nil
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -142,7 +144,7 @@ type ApiServerOptions struct {
|
||||
MaxEntriesDBSizeBytes int64
|
||||
}
|
||||
|
||||
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions) (*core.Pod, error) {
|
||||
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions, resources configStructs.Resources) (*core.Pod, error) {
|
||||
marshaledFilteringOptions, err := json.Marshal(opts.MizuApiFilteringOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -152,19 +154,19 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiS
|
||||
configMapOptional := true
|
||||
configMapVolumeName.Optional = &configMapOptional
|
||||
|
||||
cpuLimit, err := resource.ParseQuantity("750m")
|
||||
cpuLimit, err := resource.ParseQuantity(resources.CpuLimit)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("invalid cpu limit for %s container", opts.PodName))
|
||||
}
|
||||
memLimit, err := resource.ParseQuantity("512Mi")
|
||||
memLimit, err := resource.ParseQuantity(resources.MemoryLimit)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("invalid memory limit for %s container", opts.PodName))
|
||||
}
|
||||
cpuRequests, err := resource.ParseQuantity("50m")
|
||||
cpuRequests, err := resource.ParseQuantity(resources.CpuRequests)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("invalid cpu request for %s container", opts.PodName))
|
||||
}
|
||||
memRequests, err := resource.ParseQuantity("50Mi")
|
||||
memRequests, err := resource.ParseQuantity(resources.MemoryRequests)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("invalid memory request for %s container", opts.PodName))
|
||||
}
|
||||
@@ -561,8 +563,8 @@ 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) error {
|
||||
mizu.Log.Debugf("Applying %d tapper deamonsets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeToTappedPodIPMap), namespace, daemonSetName, podImage, tapperPodName)
|
||||
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 {
|
||||
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 {
|
||||
return fmt.Errorf("Daemon set %s must tap at least 1 pod", daemonSetName)
|
||||
@@ -600,19 +602,19 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
||||
),
|
||||
),
|
||||
)
|
||||
cpuLimit, err := resource.ParseQuantity("500m")
|
||||
cpuLimit, err := resource.ParseQuantity(resources.CpuLimit)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("invalid cpu limit for %s container", tapperPodName))
|
||||
}
|
||||
memLimit, err := resource.ParseQuantity("1Gi")
|
||||
memLimit, err := resource.ParseQuantity(resources.MemoryLimit)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("invalid memory limit for %s container", tapperPodName))
|
||||
}
|
||||
cpuRequests, err := resource.ParseQuantity("50m")
|
||||
cpuRequests, err := resource.ParseQuantity(resources.CpuRequests)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("invalid cpu request for %s container", tapperPodName))
|
||||
}
|
||||
memRequests, err := resource.ParseQuantity("50Mi")
|
||||
memRequests, err := resource.ParseQuantity(resources.MemoryRequests)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("invalid memory request for %s container", tapperPodName))
|
||||
}
|
||||
@@ -745,7 +747,7 @@ func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig {
|
||||
kubeConfigPath = filepath.Join(home, ".kube", "config")
|
||||
}
|
||||
|
||||
mizu.Log.Debugf("Using kube config %s", kubeConfigPath)
|
||||
logger.Log.Debugf("Using kube config %s", kubeConfigPath)
|
||||
configPathList := filepath.SplitList(kubeConfigPath)
|
||||
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}
|
||||
if len(configPathList) <= 1 {
|
||||
|
||||
@@ -2,7 +2,7 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"k8s.io/kubectl/pkg/proxy"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -14,7 +14,7 @@ const k8sProxyApiPrefix = "/"
|
||||
const mizuServicePort = 80
|
||||
|
||||
func StartProxy(kubernetesProvider *Provider, mizuPort uint16, mizuNamespace string, mizuServiceName string) error {
|
||||
mizu.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
|
||||
logger.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort)
|
||||
filter := &proxy.FilterServer{
|
||||
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
|
||||
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package mizu
|
||||
package logger
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
@@ -13,7 +14,7 @@ var format = logging.MustStringFormatter(
|
||||
)
|
||||
|
||||
func GetLogFilePath() string {
|
||||
return path.Join(GetMizuFolderPath(), "mizu_cli.log")
|
||||
return path.Join(mizu.GetMizuFolderPath(), "mizu_cli.log")
|
||||
}
|
||||
|
||||
func InitLogger() {
|
||||
@@ -34,5 +35,5 @@ func InitLogger() {
|
||||
logging.SetBackend(backend1Leveled, backend2Formatter)
|
||||
|
||||
Log.Debugf("\n\n\n")
|
||||
Log.Debugf("Running mizu version %v", SemVer)
|
||||
Log.Debugf("Running mizu version %v", mizu.SemVer)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/up9inc/mizu/cli/cmd"
|
||||
"github.com/up9inc/mizu/cli/goUtils"
|
||||
"github.com/up9inc/mizu/cli/mizu/goUtils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
package mizu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
||||
"github.com/up9inc/mizu/cli/uiUtils"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
Separator = "="
|
||||
SetCommandName = "set"
|
||||
)
|
||||
|
||||
var allowedSetFlags = []string{
|
||||
AgentImageConfigName,
|
||||
MizuResourcesNamespaceConfigName,
|
||||
TelemetryConfigName,
|
||||
DumpLogsConfigName,
|
||||
KubeConfigPathName,
|
||||
configStructs.AnalysisDestinationTapName,
|
||||
configStructs.SleepIntervalSecTapName,
|
||||
configStructs.IgnoredUserAgentsTapName,
|
||||
}
|
||||
|
||||
var Config = ConfigStruct{}
|
||||
|
||||
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 {
|
||||
if err := defaults.Set(&Config); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
cmd.Flags().Visit(initFlag)
|
||||
|
||||
finalConfigPrettified, _ := uiUtils.PrettyJson(Config)
|
||||
Log.Debugf("Init config finished\n Final config: %v", finalConfigPrettified)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetConfigWithDefaults() (string, error) {
|
||||
defaultConf := ConfigStruct{}
|
||||
if err := defaults.Set(&defaultConf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO: change to generic solution
|
||||
defaultConf.AgentImage = ""
|
||||
|
||||
return uiUtils.PrettyYaml(defaultConf)
|
||||
}
|
||||
|
||||
func GetConfigFilePath() string {
|
||||
return path.Join(GetMizuFolderPath(), "config.yaml")
|
||||
}
|
||||
|
||||
func mergeConfigFile() error {
|
||||
reader, openErr := os.Open(GetConfigFilePath())
|
||||
if openErr != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
buf, readErr := ioutil.ReadAll(reader)
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(buf, &Config); err != nil {
|
||||
return err
|
||||
}
|
||||
Log.Debugf("Found config file, merged to default options")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initFlag(f *pflag.Flag) {
|
||||
configElem := reflect.ValueOf(&Config).Elem()
|
||||
|
||||
sliceValue, isSliceValue := f.Value.(pflag.SliceValue)
|
||||
if !isSliceValue {
|
||||
mergeFlagValue(configElem, f.Name, f.Value.String())
|
||||
return
|
||||
}
|
||||
|
||||
if f.Name == SetCommandName {
|
||||
mergeSetFlag(sliceValue.GetSlice())
|
||||
return
|
||||
}
|
||||
|
||||
mergeFlagValues(configElem, f.Name, sliceValue.GetSlice())
|
||||
}
|
||||
|
||||
func mergeSetFlag(setValues []string) {
|
||||
configElem := reflect.ValueOf(&Config).Elem()
|
||||
|
||||
for _, setValue := range setValues {
|
||||
if !strings.Contains(setValue, Separator) {
|
||||
Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s (set argument format: <flag name>=<flag value>)", setValue))
|
||||
}
|
||||
|
||||
split := strings.SplitN(setValue, Separator, 2)
|
||||
if len(split) != 2 {
|
||||
Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s (set argument format: <flag name>=<flag value>)", setValue))
|
||||
}
|
||||
|
||||
argumentKey, argumentValue := split[0], split[1]
|
||||
|
||||
if !Contains(allowedSetFlags, argumentKey) {
|
||||
Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s, flag name must be one of the following: \"%s\"", setValue, strings.Join(allowedSetFlags, "\", \"")))
|
||||
}
|
||||
|
||||
mergeFlagValue(configElem, argumentKey, argumentValue)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFlagValue(currentElem reflect.Value, flagKey string, flagValue string) {
|
||||
for i := 0; i < currentElem.NumField(); i++ {
|
||||
currentField := currentElem.Type().Field(i)
|
||||
currentFieldByName := currentElem.FieldByName(currentField.Name)
|
||||
|
||||
if currentField.Type.Kind() == reflect.Struct {
|
||||
mergeFlagValue(currentFieldByName, flagKey, flagValue)
|
||||
continue
|
||||
}
|
||||
|
||||
if currentField.Tag.Get("yaml") != flagKey {
|
||||
continue
|
||||
}
|
||||
|
||||
flagValueKind := currentField.Type.Kind()
|
||||
|
||||
parsedValue, err := getParsedValue(flagValueKind, flagValue)
|
||||
if err != nil {
|
||||
Log.Warningf(uiUtils.Red, fmt.Sprintf("Invalid value %v for flag name %s, expected %s", flagValue, flagKey, flagValueKind))
|
||||
return
|
||||
}
|
||||
|
||||
currentFieldByName.Set(parsedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFlagValues(currentElem reflect.Value, flagKey string, flagValues []string) {
|
||||
for i := 0; i < currentElem.NumField(); i++ {
|
||||
currentField := currentElem.Type().Field(i)
|
||||
currentFieldByName := currentElem.FieldByName(currentField.Name)
|
||||
|
||||
if currentField.Type.Kind() == reflect.Struct {
|
||||
mergeFlagValues(currentFieldByName, flagKey, flagValues)
|
||||
continue
|
||||
}
|
||||
|
||||
if currentField.Tag.Get("yaml") != flagKey {
|
||||
continue
|
||||
}
|
||||
|
||||
flagValueKind := currentField.Type.Elem().Kind()
|
||||
|
||||
parsedValues := reflect.MakeSlice(reflect.SliceOf(currentField.Type.Elem()), 0, 0)
|
||||
for _, flagValue := range flagValues {
|
||||
parsedValue, err := getParsedValue(flagValueKind, flagValue)
|
||||
if err != nil {
|
||||
Log.Warningf(uiUtils.Red, fmt.Sprintf("Invalid value %v for flag name %s, expected %s", flagValue, flagKey, flagValueKind))
|
||||
return
|
||||
}
|
||||
|
||||
parsedValues = reflect.Append(parsedValues, parsedValue)
|
||||
}
|
||||
|
||||
currentFieldByName.Set(parsedValues)
|
||||
}
|
||||
}
|
||||
|
||||
func getParsedValue(kind reflect.Kind, value string) (reflect.Value, error) {
|
||||
switch kind {
|
||||
case reflect.String:
|
||||
return reflect.ValueOf(value), nil
|
||||
case reflect.Bool:
|
||||
boolArgumentValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(boolArgumentValue), nil
|
||||
case reflect.Int:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int(intArgumentValue)), nil
|
||||
case reflect.Int8:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 8)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int8(intArgumentValue)), nil
|
||||
case reflect.Int16:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 16)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int16(intArgumentValue)), nil
|
||||
case reflect.Int32:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 32)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(int32(intArgumentValue)), nil
|
||||
case reflect.Int64:
|
||||
intArgumentValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(intArgumentValue), nil
|
||||
case reflect.Uint:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint(uintArgumentValue)), nil
|
||||
case reflect.Uint8:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 8)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint8(uintArgumentValue)), nil
|
||||
case reflect.Uint16:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint16(uintArgumentValue)), nil
|
||||
case reflect.Uint32:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uint32(uintArgumentValue)), nil
|
||||
case reflect.Uint64:
|
||||
uintArgumentValue, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return reflect.ValueOf(uintArgumentValue), nil
|
||||
}
|
||||
|
||||
return reflect.ValueOf(nil), errors.New("value to parse does not match type")
|
||||
}
|
||||
60
cli/mizu/fsUtils/mizuLogsUtils.go
Normal file
60
cli/mizu/fsUtils/mizuLogsUtils.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package fsUtils
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/config"
|
||||
"github.com/up9inc/mizu/cli/kubernetes"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func DumpLogs(provider *kubernetes.Provider, ctx context.Context, filePath string) error {
|
||||
podExactRegex := regexp.MustCompile("^" + mizu.MizuResourcesPrefix)
|
||||
pods, err := provider.ListAllPodsMatchingRegex(ctx, podExactRegex, []string{config.Config.MizuResourcesNamespace})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(pods) == 0 {
|
||||
return fmt.Errorf("no mizu pods found in namespace %s", config.Config.MizuResourcesNamespace)
|
||||
}
|
||||
|
||||
newZipFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer newZipFile.Close()
|
||||
zipWriter := zip.NewWriter(newZipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, pod := range pods {
|
||||
logs, err := provider.GetPodLogs(pod.Namespace, pod.Name, ctx)
|
||||
if err != nil {
|
||||
logger.Log.Errorf("Failed to get logs, %v", err)
|
||||
continue
|
||||
} else {
|
||||
logger.Log.Debugf("Successfully read log length %d for pod: %s.%s", len(logs), pod.Namespace, pod.Name)
|
||||
}
|
||||
if err := AddStrToZip(zipWriter, logs, fmt.Sprintf("%s.%s.log", pod.Namespace, pod.Name)); err != nil {
|
||||
logger.Log.Errorf("Failed write logs, %v", err)
|
||||
} else {
|
||||
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 {
|
||||
logger.Log.Debugf("Failed write file, %v", err)
|
||||
} else {
|
||||
logger.Log.Debugf("Successfully added file %s", config.GetConfigFilePath())
|
||||
}
|
||||
if err := AddFileToZip(zipWriter, logger.GetLogFilePath()); err != nil {
|
||||
logger.Log.Debugf("Failed write file, %v", err)
|
||||
} else {
|
||||
logger.Log.Debugf("Successfully added file %s", logger.GetLogFilePath())
|
||||
}
|
||||
logger.Log.Infof("You can find the zip file with all logs in %s\n", filePath)
|
||||
return nil
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package goUtils
|
||||
|
||||
import (
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
)
|
||||
@@ -10,7 +10,7 @@ func HandleExcWrapper(fn interface{}, params ...interface{}) (result []reflect.V
|
||||
defer func() {
|
||||
if panicMessage := recover(); panicMessage != nil {
|
||||
stack := debug.Stack()
|
||||
mizu.Log.Fatalf("Unhandled panic: %v\n stack: %s", panicMessage, stack)
|
||||
logger.Log.Fatalf("Unhandled panic: %v\n stack: %s", panicMessage, stack)
|
||||
}
|
||||
}()
|
||||
f := reflect.ValueOf(fn)
|
||||
90
cli/mizu/sliceUtils_test.go
Normal file
90
cli/mizu/sliceUtils_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package mizu_test
|
||||
|
||||
import (
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainsExists(t *testing.T) {
|
||||
tests := []struct {
|
||||
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},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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{"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 {
|
||||
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: "cat", Expected: false},
|
||||
{Slice: []string{}, ContainsValue: "dog", Expected: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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: nil, ContainsValue: "cat", Expected: false},
|
||||
{Slice: nil, ContainsValue: "dog", Expected: false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package mizu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const telemetryUrl = "https://us-east4-up9-prod.cloudfunctions.net/mizu-telemetry"
|
||||
|
||||
func ReportRun(cmd string, args interface{}) {
|
||||
if !Config.Telemetry {
|
||||
Log.Debugf("not reporting due to config value")
|
||||
return
|
||||
}
|
||||
|
||||
argsBytes, _ := json.Marshal(args)
|
||||
argsMap := map[string]string{
|
||||
"telemetry_type": "execution",
|
||||
"cmd": cmd,
|
||||
"args": string(argsBytes),
|
||||
"component": "mizu_cli",
|
||||
"BuildTimestamp": BuildTimestamp,
|
||||
"Branch": Branch,
|
||||
"version": SemVer}
|
||||
argsMap["message"] = fmt.Sprintf("mizu %v - %v", argsMap["cmd"], string(argsBytes))
|
||||
|
||||
jsonValue, _ := json.Marshal(argsMap)
|
||||
|
||||
if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||
Log.Debugf("error sending telemetry err: %v, response %v", err, resp)
|
||||
} else {
|
||||
Log.Debugf("Successfully reported telemetry")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package mizu
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/up9inc/mizu/cli/logger"
|
||||
"github.com/up9inc/mizu/cli/mizu"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -41,22 +43,22 @@ func CheckVersionCompatibility(port uint16) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if semver.SemVersion(apiSemVer).Major() == semver.SemVersion(SemVer).Major() &&
|
||||
semver.SemVersion(apiSemVer).Minor() == semver.SemVersion(SemVer).Minor() {
|
||||
if semver.SemVersion(apiSemVer).Major() == semver.SemVersion(mizu.SemVer).Major() &&
|
||||
semver.SemVersion(apiSemVer).Minor() == semver.SemVersion(mizu.SemVer).Minor() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Log.Errorf(uiUtils.Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)", SemVer, apiSemVer))
|
||||
logger.Log.Errorf(uiUtils.Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)", mizu.SemVer, apiSemVer))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func CheckNewerVersion() {
|
||||
Log.Debugf("Checking for newer version...")
|
||||
logger.Log.Debugf("Checking for newer version...")
|
||||
start := time.Now()
|
||||
client := github.NewClient(nil)
|
||||
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "up9inc", "mizu")
|
||||
if err != nil {
|
||||
Log.Debugf("[ERROR] Failed to get latest release")
|
||||
logger.Log.Debugf("[ERROR] Failed to get latest release")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,26 +70,26 @@ func CheckNewerVersion() {
|
||||
}
|
||||
}
|
||||
if versionFileUrl == "" {
|
||||
Log.Debugf("[ERROR] Version file not found in the latest release")
|
||||
logger.Log.Debugf("[ERROR] Version file not found in the latest release")
|
||||
return
|
||||
}
|
||||
|
||||
res, err := http.Get(versionFileUrl)
|
||||
if err != nil {
|
||||
Log.Debugf("[ERROR] Failed to get the version file %v", err)
|
||||
logger.Log.Debugf("[ERROR] Failed to get the version file %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
Log.Debugf("[ERROR] Failed to read the version file -> %v", err)
|
||||
logger.Log.Debugf("[ERROR] Failed to read the version file -> %v", err)
|
||||
return
|
||||
}
|
||||
gitHubVersion := string(data)
|
||||
gitHubVersion = gitHubVersion[:len(gitHubVersion)-1]
|
||||
Log.Debugf("Finished version validation, took %v", time.Since(start))
|
||||
if SemVer < gitHubVersion {
|
||||
Log.Infof(uiUtils.Yellow, fmt.Sprintf("Update available! %v -> %v (%v)", SemVer, gitHubVersion, *latestRelease.HTMLURL))
|
||||
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))
|
||||
}
|
||||
}
|
||||
109
cli/telemetry/telemetry.go
Normal file
109
cli/telemetry/telemetry.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
const telemetryUrl = "https://us-east4-up9-prod.cloudfunctions.net/mizu-telemetry"
|
||||
|
||||
func ReportRun(cmd string, args interface{}) {
|
||||
if !shouldRunTelemetry() {
|
||||
logger.Log.Debugf("not reporting telemetry")
|
||||
return
|
||||
}
|
||||
|
||||
argsBytes, _ := json.Marshal(args)
|
||||
argsMap := map[string]interface{}{
|
||||
"cmd": cmd,
|
||||
"args": string(argsBytes),
|
||||
}
|
||||
|
||||
if err := sendTelemetry("Execution", argsMap); err != nil {
|
||||
logger.Log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log.Debugf("successfully reported telemetry for cmd %v", cmd)
|
||||
}
|
||||
|
||||
func ReportAPICalls(mizuPort uint16) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
argsMap := map[string]interface{}{
|
||||
"apiCallsCount": generalStats["EntriesCount"],
|
||||
"firstAPICallTimestamp": generalStats["FirstEntryTimestamp"],
|
||||
"lastAPICallTimestamp": generalStats["LastEntryTimestamp"],
|
||||
}
|
||||
|
||||
if err := sendTelemetry("APICalls", argsMap); err != nil {
|
||||
logger.Log.Debug(err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Log.Debugf("successfully reported telemetry of api calls")
|
||||
}
|
||||
|
||||
func shouldRunTelemetry() bool {
|
||||
if !config.Config.Telemetry {
|
||||
return false
|
||||
}
|
||||
|
||||
if mizu.Branch != "main" && mizu.Branch != "develop" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func sendTelemetry(telemetryType string, argsMap map[string]interface{}) error {
|
||||
argsMap["telemetryType"] = telemetryType
|
||||
argsMap["component"] = "mizu_cli"
|
||||
argsMap["buildTimestamp"] = mizu.BuildTimestamp
|
||||
argsMap["branch"] = mizu.Branch
|
||||
argsMap["version"] = mizu.SemVer
|
||||
|
||||
jsonValue, _ := json.Marshal(argsMap)
|
||||
|
||||
if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||
return fmt.Errorf("ERROR: failed sending telemetry, err: %v, response %v", err, resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
codecov.yml
Normal file
8
codecov.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 1%
|
||||
patch:
|
||||
default:
|
||||
enabled: no
|
||||
78
docs/POLICY_RULES.md
Normal file
78
docs/POLICY_RULES.md
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
# API rules validation
|
||||
|
||||
This feature allows you to define set of simple rules, and test the API against them.
|
||||
Such validation may test response for specific JSON fields, headers, etc.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
Example 1: HTTP request (REST API call) that didn’t pass validation is highlighted in red
|
||||
|
||||

|
||||
|
||||
- - -
|
||||
|
||||
|
||||
Example 2: Details pane shows the validation rule details and whether it passed or failed
|
||||
|
||||

|
||||
|
||||
|
||||
## How to use
|
||||
To use this feature - create simple rules file (see details below) and pass this file as parameter to `mizu tap` command. For example, if rules are stored in file named `rules.yaml` — run the following command:
|
||||
|
||||
|
||||
```shell
|
||||
mizu tap --test-rules rules.yaml PODNAME
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Rules file structure
|
||||
|
||||
The structure of the test-rules-file is:
|
||||
|
||||
* `name`: string, name of the rule
|
||||
* `type`: string, type of the rule, must be `json` or `header` or `latency`
|
||||
* `key`: string, [jsonpath](https://code.google.com/archive/p/jsonpath/wikis/Javascript.wiki) used only in `json` or `header` type
|
||||
* `value`: string, [regex](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) used only in `json` or `header` type
|
||||
* `service`: string, [regex](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) service name to filter
|
||||
* `path`: string, [regex](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) URL path to filter
|
||||
* `latency`: integer, time in ms of the expected latency.
|
||||
|
||||
|
||||
### For example:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- name: holy-in-name-property
|
||||
type: json
|
||||
key: "$.name"
|
||||
value: "Holy"
|
||||
service: "catalogue.*"
|
||||
path: "catalogue.*"
|
||||
- name: content-length-header
|
||||
type: header
|
||||
key: "Content-Le.*"
|
||||
value: "(\\d+(?:\\.\\d+)?)"
|
||||
- name: latency-test
|
||||
type: latency
|
||||
latency: 1
|
||||
service: "carts.*"
|
||||
```
|
||||
|
||||
### Explanation:
|
||||
|
||||
* First rule `holy-in-name-property`:
|
||||
|
||||
> This rule will be applied to all request made to `catalogue.*` services with `catalogue.*` on the URL path with a json response containing a `$.name` field. If the value of `$.name` is `Holy` than is marked as success, marked as failure otherwise.
|
||||
|
||||
* Second rule `content-length-header`:
|
||||
|
||||
> This rule will be applied to all request that has `Content-Le.*` on header. If the value of `Content-Le.*` is `(\\d+(?:\\.\\d+)?)` (number), will be marked as success, marked as failure otherwise.
|
||||
|
||||
* Third rule `latency-test`:
|
||||
|
||||
> This rule will be applied to all request made to `carts.*` services. If the latency of the response is greater than `1` will be marked as failure, marked as success otherwise.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'components/style/variables.module'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.mizuApp
|
||||
background-color: $main-background-color
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}>
|
||||
23
ui/src/components/EntryDetailed/EntryDetailed.module.sass
Normal file
23
ui/src/components/EntryDetailed/EntryDetailed.module.sass
Normal 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
|
||||
56
ui/src/components/EntryDetailed/EntryDetailed.tsx
Normal file
56
ui/src/components/EntryDetailed/EntryDetailed.tsx
Normal 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>
|
||||
</>
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../style/variables.module'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.title
|
||||
display: flex
|
||||
@@ -92,3 +92,6 @@
|
||||
tr td:first-child
|
||||
white-space: nowrap
|
||||
padding-right: .5rem
|
||||
|
||||
.noRules
|
||||
padding: 0 1rem 1rem
|
||||
213
ui/src/components/EntryDetailed/EntrySections.tsx
Normal file
213
ui/src/components/EntryDetailed/EntrySections.tsx
Normal 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 && <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>
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const KafkaEntryDetailsContent: React.FC<any> = ({entryData}) => {
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const KafkaEntryDetailsTitle: React.FC<any> = ({entryData}) => {
|
||||
|
||||
return <></>
|
||||
}
|
||||
@@ -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 : []}/>
|
||||
</>}
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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>
|
||||
<div style={{opacity: 0.5}}>{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied</div>
|
||||
</>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'variables.module'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.row
|
||||
display: flex
|
||||
@@ -24,12 +24,40 @@
|
||||
margin-right: 3px
|
||||
|
||||
.ruleSuccessRow
|
||||
border: 1px $success-color solid
|
||||
border-left: 5px $success-color solid
|
||||
background: #E8FFF1
|
||||
|
||||
.ruleSuccessRowSelected
|
||||
border: 1px #6FCF97 solid
|
||||
border-left: 5px #6FCF97 solid
|
||||
margin-left: 10px
|
||||
margin-right: 3px
|
||||
|
||||
.ruleFailureRow
|
||||
background: #FFE9EF
|
||||
|
||||
.ruleFailureRowSelected
|
||||
border: 1px $failure-color solid
|
||||
border-left: 5px $failure-color solid
|
||||
margin-left: 10px
|
||||
margin-right: 3px
|
||||
|
||||
.ruleNumberTextFailure
|
||||
color: #DB2156
|
||||
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
|
||||
padding-right: 12px
|
||||
|
||||
.service
|
||||
text-overflow: ellipsis
|
||||
@@ -45,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
|
||||
@@ -60,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
|
||||
85
ui/src/components/EntryListItem/EntryListItem.tsx
Normal file
85
ui/src/components/EntryListItem/EntryListItem.tsx
Normal 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 ? 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>
|
||||
</>
|
||||
};
|
||||
|
||||
15
ui/src/components/EntryListItem/KafkaEntryContent.tsx
Normal file
15
ui/src/components/EntryListItem/KafkaEntryContent.tsx
Normal 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 <>
|
||||
</>
|
||||
}
|
||||
82
ui/src/components/EntryListItem/RestEntryContent.tsx
Normal file
82
ui/src/components/EntryListItem/RestEntryContent.tsx
Normal 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>
|
||||
</>
|
||||
}
|
||||
@@ -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}
|
||||
@@ -1,87 +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
|
||||
}
|
||||
|
||||
interface HAREntryProps {
|
||||
entry: HAREntry;
|
||||
setFocusedEntryId: (id: string) => void;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export const HarEntry: React.FC<HAREntryProps> = ({entry, setFocusedEntryId, isSelected}) => {
|
||||
const classification = getClassification(entry.statusCode)
|
||||
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 backgroundColor = "";
|
||||
if ('latency' in entry.rules) {
|
||||
if (entry.rules.latency !== -1) {
|
||||
backgroundColor = entry.rules.latency >= entry.latency ? styles.ruleSuccessRow : styles.ruleFailureRow
|
||||
} else {
|
||||
backgroundColor = entry.rules.status ? styles.ruleSuccessRow : styles.ruleFailureRow
|
||||
}
|
||||
}
|
||||
return <>
|
||||
<div id={entry.id} className={`${styles.row} ${isSelected ? styles.rowSelected : backgroundColor}`} 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>
|
||||
<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>
|
||||
</>
|
||||
};
|
||||
@@ -1,62 +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 style={{opacity: 0.5}}>{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied</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}
|
||||
/>}
|
||||
</>
|
||||
</>
|
||||
};
|
||||
@@ -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/>
|
||||
}
|
||||
</React.Fragment>
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
};
|
||||
@@ -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}/>}
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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}
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'variables.module.scss'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.statusBar
|
||||
position: absolute
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'variables.module'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.base
|
||||
border-radius: 4px
|
||||
@@ -1,4 +1,4 @@
|
||||
@import 'variables.module'
|
||||
@import 'src/variables.module'
|
||||
|
||||
.protocol
|
||||
border-radius: 4px
|
||||
3
ui/src/components/assets/downImg.svg
Normal file
3
ui/src/components/assets/downImg.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2.82846L7.82843 3.6478e-05L9.24264 1.41425L5 5.65689L4.99997 5.65686L3.58579 4.24268L0.75733 1.41422L2.17154 5.00679e-06L5 2.82846Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
16
ui/src/components/assets/kafkaIcon.svg
Normal file
16
ui/src/components/assets/kafkaIcon.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="97" height="29" viewBox="0 0 97 29" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.4823 21H39.2623L33.1223 13.24L38.9223 7H35.3223L29.1223 13.54V7H25.9023V21H29.1223V17.46L31.0023 15.5L35.4823 21Z" fill="#090B14"/>
|
||||
<path d="M50.542 21H53.942L47.682 7H44.482L38.242 21H41.562L42.802 18H49.302L50.542 21ZM43.842 15.54L46.062 10.18L48.282 15.54H43.842Z" fill="#090B14"/>
|
||||
<path d="M65.9745 9.6V7H55.3945V21H58.6345V15.9H65.1145V13.3H58.6345V9.6H65.9745Z" fill="#090B14"/>
|
||||
<path d="M77.748 21H81.528L75.388 13.24L81.188 7H77.588L71.388 13.54V7H68.168V21H71.388V17.46L73.268 15.5L77.748 21Z" fill="#090B14"/>
|
||||
<path d="M92.8077 21H96.2077L89.9477 7H86.7477L80.5077 21H83.8277L85.0677 18H91.5677L92.8077 21ZM86.1077 15.54L88.3277 10.18L90.5477 15.54H86.1077Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 2.07143C3.41286 2.07143 2.66667 2.84427 2.66667 3.79762C2.66667 4.75097 3.41286 5.52381 4.33333 5.52381C5.25381 5.52381 6 4.75097 6 3.79762C6 2.84427 5.25381 2.07143 4.33333 2.07143ZM0.666667 3.79762C0.666667 1.70025 2.30829 0 4.33333 0C6.35838 0 8 1.70025 8 3.79762C8 5.89499 6.35838 7.59524 4.33333 7.59524C2.30829 7.59524 0.666667 5.89499 0.666667 3.79762Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 23.4762C3.41286 23.4762 2.66667 24.249 2.66667 25.2024C2.66667 26.1557 3.41286 26.9286 4.33333 26.9286C5.25381 26.9286 6 26.1557 6 25.2024C6 24.249 5.25381 23.4762 4.33333 23.4762ZM0.666667 25.2024C0.666667 23.105 2.30829 21.4048 4.33333 21.4048C6.35838 21.4048 8 23.105 8 25.2024C8 27.2997 6.35838 29 4.33333 29C2.30829 29 0.666667 27.2997 0.666667 25.2024Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.33333 12.0833C3.04467 12.0833 2 13.1653 2 14.5C2 15.8347 3.04467 16.9167 4.33333 16.9167C5.622 16.9167 6.66667 15.8347 6.66667 14.5C6.66667 13.1653 5.622 12.0833 4.33333 12.0833ZM0 14.5C0 12.0213 1.9401 10.0119 4.33333 10.0119C6.72657 10.0119 8.66667 12.0213 8.66667 14.5C8.66667 16.9787 6.72657 18.9881 4.33333 18.9881C1.9401 18.9881 0 16.9787 0 14.5Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 7.25C12.4129 7.25 11.6667 8.02284 11.6667 8.97619C11.6667 9.92954 12.4129 10.7024 13.3333 10.7024C14.2538 10.7024 15 9.92954 15 8.97619C15 8.02284 14.2538 7.25 13.3333 7.25ZM9.66667 8.97619C9.66667 6.87882 11.3083 5.17857 13.3333 5.17857C15.3584 5.17857 17 6.87882 17 8.97619C17 11.0736 15.3584 12.7738 13.3333 12.7738C11.3083 12.7738 9.66667 11.0736 9.66667 8.97619Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 18.2976C12.4129 18.2976 11.6667 19.0705 11.6667 20.0238C11.6667 20.9772 12.4129 21.75 13.3333 21.75C14.2538 21.75 15 20.9772 15 20.0238C15 19.0705 14.2538 18.2976 13.3333 18.2976ZM9.66667 20.0238C9.66667 17.9264 11.3083 16.2262 13.3333 16.2262C15.3584 16.2262 17 17.9264 17 20.0238C17 22.1212 15.3584 23.8214 13.3333 23.8214C11.3083 23.8214 9.66667 22.1212 9.66667 20.0238Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66667 10.3571V6.55952H5V10.3571H3.66667Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.33336 11.9107L10.5088 10.0119L11.1755 11.2078L8.00003 13.1067L7.33336 11.9107Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00003 15.881L11.1755 17.7798L10.5088 18.9757L7.33336 17.0769L8.00003 15.881Z" fill="#090B14"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66667 22.4405V18.6429H5V22.4405H3.66667Z" fill="#090B14"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
9
ui/src/components/assets/restIcon.svg
Normal file
9
ui/src/components/assets/restIcon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 17 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user