mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-06-24 15:24:17 +00:00
Merge branch 'develop'
This commit is contained in:
commit
e84c7d3310
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.
|
32
.github/workflows/acceptance_tests.yml
vendored
Normal file
32
.github/workflows/acceptance_tests.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: acceptance tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'main'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-acceptance-tests-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-acceptance-tests:
|
||||||
|
name: Run acceptance tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup acceptance test
|
||||||
|
run: source ./acceptanceTests/setup.sh
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make acceptance-test
|
80
.github/workflows/pr_validation.yml
vendored
Normal file
80
.github/workflows/pr_validation.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
name: PR validation
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
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
|
7
.github/workflows/publish.yml
vendored
7
.github/workflows/publish.yml
vendored
@ -1,9 +1,15 @@
|
|||||||
name: publish
|
name: publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'develop'
|
- 'develop'
|
||||||
- 'main'
|
- 'main'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -78,4 +84,3 @@ jobs:
|
|||||||
tag: ${{ steps.versioning.outputs.version }}
|
tag: ${{ steps.versioning.outputs.version }}
|
||||||
prerelease: ${{ github.ref != 'refs/heads/main' }}
|
prerelease: ${{ github.ref != 'refs/heads/main' }}
|
||||||
bodyFile: 'cli/bin/README.md'
|
bodyFile: 'cli/bin/README.md'
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -48,8 +48,6 @@ WORKDIR /app
|
|||||||
COPY --from=builder ["/app/agent-build/mizuagent", "."]
|
COPY --from=builder ["/app/agent-build/mizuagent", "."]
|
||||||
COPY --from=site-build ["/app/ui-build/build", "site"]
|
COPY --from=site-build ["/app/ui-build/build", "site"]
|
||||||
|
|
||||||
COPY agent/start.sh .
|
|
||||||
|
|
||||||
# gin-gonic runs in debug mode without this
|
# gin-gonic runs in debug mode without this
|
||||||
ENV GIN_MODE=release
|
ENV GIN_MODE=release
|
||||||
|
|
||||||
|
16
Makefile
16
Makefile
@ -28,6 +28,9 @@ ui: ## Build UI.
|
|||||||
cli: ## Build CLI.
|
cli: ## Build CLI.
|
||||||
@echo "building cli"; cd cli && $(MAKE) build
|
@echo "building cli"; cd cli && $(MAKE) build
|
||||||
|
|
||||||
|
build-cli-ci: ## Build CLI for CI.
|
||||||
|
@echo "building cli for ci"; cd cli && $(MAKE) build GIT_BRANCH=ci SUFFIX=ci
|
||||||
|
|
||||||
agent: ## Build agent.
|
agent: ## Build agent.
|
||||||
@(echo "building mizu agent .." )
|
@(echo "building mizu agent .." )
|
||||||
@(cd agent; go build -o build/mizuagent main.go)
|
@(cd agent; go build -o build/mizuagent main.go)
|
||||||
@ -42,6 +45,10 @@ push-docker: ## Build and publish agent docker image.
|
|||||||
@echo "publishing Docker image .. "
|
@echo "publishing Docker image .. "
|
||||||
./build-push-featurebranch.sh
|
./build-push-featurebranch.sh
|
||||||
|
|
||||||
|
build-docker-ci: ## Build agent docker image for CI.
|
||||||
|
@echo "building docker image for ci"
|
||||||
|
./build-agent-ci.sh
|
||||||
|
|
||||||
push-cli: ## Build and publish CLI.
|
push-cli: ## Build and publish CLI.
|
||||||
@echo "publishing CLI .. "
|
@echo "publishing CLI .. "
|
||||||
@cd cli; $(MAKE) build-all
|
@cd cli; $(MAKE) build-all
|
||||||
@ -50,7 +57,6 @@ push-cli: ## Build and publish CLI.
|
|||||||
gsutil cp -r ./cli/bin/* gs://${BUCKET_PATH}/
|
gsutil cp -r ./cli/bin/* gs://${BUCKET_PATH}/
|
||||||
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
|
gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\*
|
||||||
|
|
||||||
|
|
||||||
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts.
|
clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts.
|
||||||
|
|
||||||
clean-ui: ## Clean UI.
|
clean-ui: ## Clean UI.
|
||||||
@ -65,3 +71,11 @@ clean-cli: ## Clean CLI.
|
|||||||
clean-docker:
|
clean-docker:
|
||||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||||
|
|
||||||
|
test-cli:
|
||||||
|
@echo "running cli tests"; cd cli && $(MAKE) test
|
||||||
|
|
||||||
|
test-agent:
|
||||||
|
@echo "running agent tests"; cd agent && $(MAKE) test
|
||||||
|
|
||||||
|
acceptance-test:
|
||||||
|
@echo "running acceptance tests"; cd acceptanceTests && $(MAKE) test
|
||||||
|
77
README.md
77
README.md
@ -1,16 +1,17 @@
|
|||||||

|

|
||||||
|
|
||||||
# The API Traffic Viewer for Kubernetes
|
# 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
|
## Features
|
||||||
|
|
||||||
- Simple and powerful CLI
|
- 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
|
- No installation or code instrumentation
|
||||||
- Works completely on premises (on-prem)
|
- Works completely on premises
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
@ -32,10 +33,10 @@ https://github.com/up9inc/mizu/releases/latest/download/mizu_linux_amd64 \
|
|||||||
&& chmod 755 mizu
|
&& 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
|
### 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
|
## Prerequisites
|
||||||
1. Set `KUBECONFIG` environment variable to your Kubernetes configuration. If this is not set, Mizu assumes that configuration is at `${HOME}/.kube/config`
|
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
|
1. Find pods you'd like to tap to in your Kubernetes cluster
|
||||||
2. Run `mizu tap` or `mizu tap PODNAME`
|
2. Run `mizu tap` or `mizu tap PODNAME`
|
||||||
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI ..
|
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI
|
||||||
4. Watch the API traffic flowing ..
|
4. Watch the API traffic flowing
|
||||||
5. Type ^C to stop
|
5. Type ^C to stop
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -75,7 +76,7 @@ To tap all pods in current namespace -
|
|||||||
|
|
||||||
|
|
||||||
To tap specific pod -
|
To tap specific pod -
|
||||||
```
|
```bash
|
||||||
$ kubectl get pods
|
$ kubectl get pods
|
||||||
NAME READY STATUS RESTARTS AGE
|
NAME READY STATUS RESTARTS AGE
|
||||||
front-end-649fc5fd6-kqbtn 2/2 Running 0 7m
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 7m
|
||||||
@ -88,7 +89,7 @@ To tap specific pod -
|
|||||||
```
|
```
|
||||||
|
|
||||||
To tap multiple pods using regex -
|
To tap multiple pods using regex -
|
||||||
```
|
```bash
|
||||||
$ kubectl get pods
|
$ kubectl get pods
|
||||||
NAME READY STATUS RESTARTS AGE
|
NAME READY STATUS RESTARTS AGE
|
||||||
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
||||||
@ -106,30 +107,64 @@ To tap multiple pods using regex -
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Mizu can work with config file which should be stored in ${HOME}/.mizu/config.yaml (macOS: ~/.mizu/config.yaml) <br />
|
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 no config file found, defaults will be used <br />
|
||||||
In case of partial configuration defined, all other fields will be used with defaults. <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.
|
You can always override the defaults or config file with CLI flags
|
||||||
|
|
||||||
To get the default config params run `mizu config` <br />
|
To get the default config params run `mizu config` <br />
|
||||||
To generate a new config file with default values use `mizu config -r`
|
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`)
|
### Telemetry
|
||||||
* **mizu-resources-namespace**: Type - String, See [Namespace-Restricted Mode](#namespace-restricted-mode)
|
|
||||||
* **telemetry**: Type - Boolean, Reports 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
|
||||||
* **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)
|
|
||||||
|
|
||||||
## Advanced Usage
|
## Advanced Usage
|
||||||
|
|
||||||
### Namespace-Restricted Mode
|
### 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
|
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
|
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
|
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
|
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 using command-line options:
|
||||||
|
|
||||||
|
```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
acceptanceTests/Makefile
Normal file
2
acceptanceTests/Makefile
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
test: ## Run acceptance tests.
|
||||||
|
@go test ./...
|
3
acceptanceTests/go.mod
Normal file
3
acceptanceTests/go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module github.com/up9inc/mizu/tests
|
||||||
|
|
||||||
|
go 1.16
|
48
acceptanceTests/setup.sh
Normal file
48
acceptanceTests/setup.sh
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PREFIX=$HOME/local/bin
|
||||||
|
VERSION=v1.22.0
|
||||||
|
|
||||||
|
echo "Attempting to install minikube and assorted tools to $PREFIX"
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v kubectl)" ]; then
|
||||||
|
echo "Installing kubectl version $VERSION"
|
||||||
|
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "kubetcl is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v minikube)" ]; then
|
||||||
|
echo "Installing minikube version $VERSION"
|
||||||
|
curl -Lo minikube https://storage.googleapis.com/minikube/releases/$VERSION/minikube-linux-amd64
|
||||||
|
chmod +x minikube
|
||||||
|
mv minikube "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "minikube is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting minikube..."
|
||||||
|
minikube start
|
||||||
|
|
||||||
|
echo "Creating mizu tests namespace"
|
||||||
|
kubectl create namespace mizu-tests
|
||||||
|
|
||||||
|
echo "Creating httpbin deployment"
|
||||||
|
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests
|
||||||
|
|
||||||
|
echo "Creating httpbin service"
|
||||||
|
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests
|
||||||
|
|
||||||
|
echo "Starting proxy"
|
||||||
|
kubectl proxy --port=8080 &
|
||||||
|
|
||||||
|
echo "Setting minikube docker env"
|
||||||
|
eval $(minikube docker-env)
|
||||||
|
|
||||||
|
echo "Build agent image"
|
||||||
|
make build-docker-ci
|
||||||
|
|
||||||
|
echo "Build cli"
|
||||||
|
make build-cli-ci
|
125
acceptanceTests/tap_test.go
Normal file
125
acceptanceTests/tap_test.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTapAndFetch(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []int{1, 100}
|
||||||
|
|
||||||
|
for _, entriesCount := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := GetCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := GetDefaultTapCommandArgs()
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := CleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(30 * time.Second)
|
||||||
|
|
||||||
|
proxyUrl := "http://localhost:8080/api/v1/namespaces/mizu-tests/services/httpbin/proxy/get"
|
||||||
|
for i := 0; i < entriesCount; i++ {
|
||||||
|
if _, requestErr := ExecuteHttpRequest(proxyUrl); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("http://localhost:8899/mizu/api/entries?limit=%v&operator=lt×tamp=%v", entriesCount, timestamp)
|
||||||
|
requestResult, requestErr := ExecuteHttpRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, ok := requestResult.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid entries type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) != entriesCount {
|
||||||
|
t.Errorf("unexpected entries result - Expected: %v, actual: %v", entriesCount, len(entries))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := entries[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid entry type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("http://localhost:8899/mizu/api/entries/%v", entry["id"])
|
||||||
|
requestResult, requestErr = ExecuteHttpRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestResult == nil {
|
||||||
|
t.Errorf("unexpected nil entry result")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCmdArgs := GetDefaultFetchCommandArgs()
|
||||||
|
fetchCmd := exec.Command(cliPath, fetchCmdArgs...)
|
||||||
|
t.Logf("running command: %v", fetchCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := CleanupCommand(fetchCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup fetch command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := fetchCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start fetch command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
harBytes, readFileErr := ioutil.ReadFile("./unknown_source.har")
|
||||||
|
if readFileErr != nil {
|
||||||
|
t.Errorf("failed to read har file, err: %v", readFileErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
harEntries, err := GetEntriesFromHarBytes(harBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get entries from har, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(harEntries) != entriesCount {
|
||||||
|
t.Errorf("unexpected har entries result - Expected: %v, actual: %v", entriesCount, len(harEntries))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
113
acceptanceTests/testsUtils.go
Normal file
113
acceptanceTests/testsUtils.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCliPath() (string, error) {
|
||||||
|
dir, filePathErr := os.Getwd()
|
||||||
|
if filePathErr != nil {
|
||||||
|
return "", filePathErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath := path.Join(dir, "../cli/bin/mizu_ci")
|
||||||
|
return cliPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultCommandArgs() []string {
|
||||||
|
setFlag := "--set"
|
||||||
|
telemetry := "telemetry=false"
|
||||||
|
|
||||||
|
return []string{setFlag, telemetry}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultTapCommandArgs() []string {
|
||||||
|
tapCommand := "tap"
|
||||||
|
setFlag := "--set"
|
||||||
|
namespaces := "tap.namespaces=mizu-tests"
|
||||||
|
agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0"
|
||||||
|
imagePullPolicy := "image-pull-policy=Never"
|
||||||
|
|
||||||
|
defaultCmdArgs := GetDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand, setFlag, namespaces, setFlag, agentImage, setFlag, imagePullPolicy}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDefaultFetchCommandArgs() []string {
|
||||||
|
tapCommand := "fetch"
|
||||||
|
|
||||||
|
defaultCmdArgs := GetDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func JsonBytesToInterface(jsonBytes []byte) (interface{}, error) {
|
||||||
|
var result interface{}
|
||||||
|
if parseErr := json.Unmarshal(jsonBytes, &result); parseErr != nil {
|
||||||
|
return nil, parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExecuteHttpRequest(url string) (interface{}, error) {
|
||||||
|
response, requestErr := http.Get(url)
|
||||||
|
if requestErr != nil {
|
||||||
|
return nil, requestErr
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("invalid status code %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonBytesToInterface(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupCommand(cmd *exec.Cmd) error {
|
||||||
|
if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntriesFromHarBytes(harBytes []byte) ([]interface{}, error){
|
||||||
|
harInterface, convertErr := JsonBytesToInterface(harBytes)
|
||||||
|
if convertErr != nil {
|
||||||
|
return nil, convertErr
|
||||||
|
}
|
||||||
|
|
||||||
|
har, ok := harInterface.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid har type")
|
||||||
|
}
|
||||||
|
|
||||||
|
harLogInterface := har["log"]
|
||||||
|
harLog, ok := harLogInterface.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid har log type")
|
||||||
|
}
|
||||||
|
|
||||||
|
harEntriesInterface := harLog["entries"]
|
||||||
|
harEntries, ok := harEntriesInterface.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid har entries type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return harEntries, nil
|
||||||
|
}
|
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
|
@ -540,8 +540,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM=
|
||||||
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw=
|
||||||
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
|
||||||
|
@ -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 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 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 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() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
||||||
tapOpts := &tap.TapOpts{HostMode: hostMode}
|
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 {
|
if *standaloneMode {
|
||||||
@ -77,6 +80,13 @@ func main() {
|
|||||||
go api.StartReadingEntries(filteredHarChannel, nil)
|
go api.StartReadingEntries(filteredHarChannel, nil)
|
||||||
|
|
||||||
hostApi(socketHarOutChannel)
|
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)
|
signalChan := make(chan os.Signal, 1)
|
||||||
@ -149,15 +159,13 @@ func getTrafficFilteringOptions() *shared.TrafficFilteringOptions {
|
|||||||
return &filteringOptions
|
return &filteringOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
var userAgentsToFilter = []string{"kube-probe", "prometheus"}
|
|
||||||
|
|
||||||
func filterHarItems(inChannel <-chan *tap.OutputChannelItem, outChannel chan *tap.OutputChannelItem, filterOptions *shared.TrafficFilteringOptions) {
|
func filterHarItems(inChannel <-chan *tap.OutputChannelItem, outChannel chan *tap.OutputChannelItem, filterOptions *shared.TrafficFilteringOptions) {
|
||||||
for message := range inChannel {
|
for message := range inChannel {
|
||||||
if message.ConnectionInfo.IsOutgoing && api.CheckIsServiceIP(message.ConnectionInfo.ServerIP) {
|
if message.ConnectionInfo.IsOutgoing && api.CheckIsServiceIP(message.ConnectionInfo.ServerIP) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// TODO: move this to tappers https://up9.atlassian.net/browse/TRA-3441
|
// TODO: move this to tappers https://up9.atlassian.net/browse/TRA-3441
|
||||||
if filterOptions.HideHealthChecks && isHealthCheckByUserAgent(message) {
|
if isHealthCheckByUserAgent(message, filterOptions.HealthChecksUserAgentHeaders) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,11 +177,11 @@ func filterHarItems(inChannel <-chan *tap.OutputChannelItem, outChannel chan *ta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHealthCheckByUserAgent(message *tap.OutputChannelItem) bool {
|
func isHealthCheckByUserAgent(message *tap.OutputChannelItem, userAgentsToIgnore []string) bool {
|
||||||
for _, header := range message.HarEntry.Request.Headers {
|
for _, header := range message.HarEntry.Request.Headers {
|
||||||
if strings.ToLower(header.Name) == "user-agent" {
|
if strings.ToLower(header.Name) == "user-agent" {
|
||||||
for _, userAgent := range userAgentsToFilter {
|
for _, userAgent := range userAgentsToIgnore {
|
||||||
if strings.Contains(strings.ToLower(header.Value), userAgent) {
|
if strings.Contains(strings.ToLower(header.Value), strings.ToLower(userAgent)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mizuserver/pkg/holder"
|
"mizuserver/pkg/holder"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -108,6 +109,7 @@ func startReadingChannel(outputItems <-chan *tap.OutputChannelItem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for item := range outputItems {
|
for item := range outputItems {
|
||||||
|
providers.EntryAdded()
|
||||||
saveHarToDb(item.HarEntry, item.ConnectionInfo)
|
saveHarToDb(item.HarEntry, item.ConnectionInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,14 +241,7 @@ func DeleteAllEntries(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetGeneralStats(c *gin.Context) {
|
func GetGeneralStats(c *gin.Context) {
|
||||||
sqlQuery := "SELECT count(*) as count, min(timestamp) as min, max(timestamp) as max from mizu_entries"
|
c.JSON(http.StatusOK, providers.GetGeneralStats())
|
||||||
var result struct {
|
|
||||||
Count int
|
|
||||||
Min int
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
database.GetEntriesTable().Raw(sqlQuery).Scan(&result)
|
|
||||||
c.JSON(http.StatusOK, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTappingStatus(c *gin.Context) {
|
func GetTappingStatus(c *gin.Context) {
|
||||||
|
@ -56,12 +56,14 @@ type BaseEntryDetails struct {
|
|||||||
type ApplicableRules struct {
|
type ApplicableRules struct {
|
||||||
Latency int64 `json:"latency,omitempty"`
|
Latency int64 `json:"latency,omitempty"`
|
||||||
Status bool `json:"status,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 := ApplicableRules{}
|
||||||
ar.Status = status
|
ar.Status = status
|
||||||
ar.Latency = latency
|
ar.Latency = latency
|
||||||
|
ar.NumberOfRules = number
|
||||||
return ar
|
return ar
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,7 +220,7 @@ func (fewp *FullEntryWithPolicy) UnmarshalData(entry *MizuEntry) error {
|
|||||||
|
|
||||||
func RunValidationRulesState(harEntry har.Entry, service string) ApplicableRules {
|
func RunValidationRulesState(harEntry har.Entry, service string) ApplicableRules {
|
||||||
numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service)
|
numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service)
|
||||||
statusPolicyToSend, latency := rules.PassedValidationRules(resultPolicyToSend, numberOfRules)
|
statusPolicyToSend, latency, numberOfRules := rules.PassedValidationRules(resultPolicyToSend, numberOfRules)
|
||||||
ar := NewApplicableRules(statusPolicyToSend, latency)
|
ar := NewApplicableRules(statusPolicyToSend, latency, numberOfRules)
|
||||||
return ar
|
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
|
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 {
|
if len(rulesMatched) == 0 {
|
||||||
return false, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
for _, rule := range rulesMatched {
|
for _, rule := range rulesMatched {
|
||||||
if rule.Matched == false {
|
if rule.Matched == false {
|
||||||
return false, -1
|
return false, -1, len(rulesMatched)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, rule := range rulesMatched {
|
for _, rule := range rulesMatched {
|
||||||
if strings.ToLower(rule.Rule.Type) == "latency" {
|
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)
|
||||||
}
|
}
|
||||||
|
@ -158,9 +158,11 @@ func filterJsonBody(bytes []byte) ([]byte, error) {
|
|||||||
|
|
||||||
func filterJsonMap(jsonMap map[string] interface{}) {
|
func filterJsonMap(jsonMap map[string] interface{}) {
|
||||||
for key, value := range jsonMap {
|
for key, value := range jsonMap {
|
||||||
|
// Do not replace nil values with maskedFieldPlaceholderValue
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
nestedMap, isNested := value.(map[string] interface{})
|
nestedMap, isNested := value.(map[string] interface{})
|
||||||
if isNested {
|
if isNested {
|
||||||
filterJsonMap(nestedMap)
|
filterJsonMap(nestedMap)
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
./mizuagent -i any -hardump -targets ${TAPPED_ADDRESSES}
|
|
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 |
15
build-agent-ci.sh
Executable file
15
build-agent-ci.sh
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GCP_PROJECT=up9-docker-hub
|
||||||
|
REPOSITORY=gcr.io/$GCP_PROJECT
|
||||||
|
SERVER_NAME=mizu
|
||||||
|
GIT_BRANCH=ci
|
||||||
|
|
||||||
|
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
|
||||||
|
SEM_VER=${SEM_VER=0.0.0}
|
||||||
|
|
||||||
|
DOCKER_TAGGED_BUILD="$DOCKER_REPO:$SEM_VER"
|
||||||
|
|
||||||
|
echo "building $DOCKER_TAGGED_BUILD"
|
||||||
|
docker build -t ${DOCKER_TAGGED_BUILD} --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} .
|
@ -1,12 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
SERVER_NAME=mizu
|
|
||||||
GCP_PROJECT=up9-docker-hub
|
GCP_PROJECT=up9-docker-hub
|
||||||
REPOSITORY=gcr.io/$GCP_PROJECT
|
REPOSITORY=gcr.io/$GCP_PROJECT
|
||||||
|
SERVER_NAME=mizu
|
||||||
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
|
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
|
||||||
SEM_VER=${SEM_VER=0.0.0}
|
|
||||||
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
|
DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH
|
||||||
|
SEM_VER=${SEM_VER=0.0.0}
|
||||||
|
|
||||||
DOCKER_TAGGED_BUILDS=("$DOCKER_REPO:latest" "$DOCKER_REPO:$SEM_VER")
|
DOCKER_TAGGED_BUILDS=("$DOCKER_REPO:latest" "$DOCKER_REPO:$SEM_VER")
|
||||||
|
|
||||||
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
|
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
|
||||||
|
@ -24,7 +24,7 @@ build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables
|
|||||||
|
|
||||||
build-all: ## Build for all supported platforms.
|
build-all: ## Build for all supported platforms.
|
||||||
@echo "Compiling for every OS and Platform"
|
@echo "Compiling for every OS and Platform"
|
||||||
@mkdir -p bin && echo "SHA256 checksums available for compiled binaries \n\nRun \`shasum -a 256 -c mizu_OS_ARCH.sha256\` to verify\n\n" > bin/README.md
|
@mkdir -p bin && sed s/_SEM_VER_/$(SEM_VER)/g README.md.TEMPLATE > bin/README.md
|
||||||
@$(MAKE) build GOOS=darwin GOARCH=amd64
|
@$(MAKE) build GOOS=darwin GOARCH=amd64
|
||||||
@$(MAKE) build GOOS=linux GOARCH=amd64
|
@$(MAKE) build GOOS=linux GOARCH=amd64
|
||||||
@# $(MAKE) build GOOS=darwin GOARCH=arm64
|
@# $(MAKE) build GOOS=darwin GOARCH=arm64
|
||||||
@ -39,3 +39,6 @@ build-all: ## Build for all supported platforms.
|
|||||||
clean: ## Clean all build artifacts.
|
clean: ## Clean all build artifacts.
|
||||||
go clean
|
go clean
|
||||||
rm -rf ./bin/*
|
rm -rf ./bin/*
|
||||||
|
|
||||||
|
test: ## Run cli tests.
|
||||||
|
@go test ./... -coverpkg=./... -race -coverprofile=coverage.out -covermode=atomic
|
||||||
|
20
cli/README.md.TEMPLATE
Normal file
20
cli/README.md.TEMPLATE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Mizu release _SEM_VER_
|
||||||
|
|
||||||
|
Download Mizu for your platform
|
||||||
|
|
||||||
|
**Mac** (on Intel chip)
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_darwin_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux**
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_linux_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
SHA256 checksums available for compiled binaries.
|
||||||
|
Run `shasum -a 256 -c mizu_OS_ARCH.sha256` to verify.
|
||||||
|
|
||||||
|
|
@ -2,32 +2,36 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
"github.com/up9inc/mizu/cli/uiUtils"
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var regenerateFile bool
|
|
||||||
|
|
||||||
var configCmd = &cobra.Command{
|
var configCmd = &cobra.Command{
|
||||||
Use: "config",
|
Use: "config",
|
||||||
Short: "Generate config with default values",
|
Short: "Generate config with default values",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
template, err := mizu.GetConfigWithDefaults()
|
go telemetry.ReportRun("config", config.Config.Config)
|
||||||
|
|
||||||
|
template, err := config.GetConfigWithDefaults()
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
if regenerateFile {
|
if config.Config.Config.Regenerate {
|
||||||
data := []byte(template)
|
data := []byte(template)
|
||||||
if err := ioutil.WriteFile(mizu.GetConfigFilePath(), data, 0644); err != nil {
|
if err := ioutil.WriteFile(config.GetConfigFilePath(), data, 0644); err != nil {
|
||||||
mizu.Log.Errorf("Failed writing config %v", err)
|
logger.Log.Errorf("Failed writing config %v", err)
|
||||||
return nil
|
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 {
|
} else {
|
||||||
mizu.Log.Debugf("Writing template config.\n%v", template)
|
logger.Log.Debugf("Writing template config.\n%v", template)
|
||||||
fmt.Printf("%v", template)
|
fmt.Printf("%v", template)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -36,5 +40,9 @@ var configCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(configCmd)
|
rootCmd.AddCommand(configCmd)
|
||||||
configCmd.Flags().BoolVarP(®enerateFile, "regenerate", "r", false, fmt.Sprintf("Regenerate the config file with default values %s", mizu.GetConfigFilePath()))
|
|
||||||
|
defaultConfigConfig := configStructs.ConfigConfig{}
|
||||||
|
defaults.Set(&defaultConfigConfig)
|
||||||
|
|
||||||
|
configCmd.Flags().BoolP(configStructs.RegenerateConfigName, "r", defaultConfigConfig.Regenerate, fmt.Sprintf("Regenerate the config file with default values %s", config.GetConfigFilePath()))
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,19 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/version"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
var fetchCmd = &cobra.Command{
|
var fetchCmd = &cobra.Command{
|
||||||
Use: "fetch",
|
Use: "fetch",
|
||||||
Short: "Download recorded traffic to files",
|
Short: "Download recorded traffic to files",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
go mizu.ReportRun("fetch", mizu.Config.Fetch)
|
go telemetry.ReportRun("fetch", config.Config.Fetch)
|
||||||
if isCompatible, err := mizu.CheckVersionCompatibility(mizu.Config.Fetch.GuiPort); err != nil {
|
|
||||||
|
if isCompatible, err := version.CheckVersionCompatibility(config.Config.Fetch.GuiPort); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !isCompatible {
|
} else if !isCompatible {
|
||||||
return nil
|
return nil
|
||||||
|
@ -4,8 +4,9 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/kubernetes"
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
@ -16,8 +17,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RunMizuFetch() {
|
func RunMizuFetch() {
|
||||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Fetch.GuiPort)
|
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.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))
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -34,7 +35,7 @@ func RunMizuFetch() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = Unzip(zipReader, mizu.Config.Fetch.Directory)
|
_ = Unzip(zipReader, config.Config.Fetch.Directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Unzip(reader *zip.Reader, dest string) error {
|
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())
|
_ = os.MkdirAll(path, f.Mode())
|
||||||
} else {
|
} else {
|
||||||
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
|
_ = 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())
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -73,7 +74,7 @@ func Unzip(reader *zip.Reader, dest string) error {
|
|||||||
if err := f.Close(); err != nil {
|
if err := f.Close(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
mizu.Log.Info(" done")
|
logger.Log.Info(" done")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err = io.Copy(f, rc)
|
_, err = io.Copy(f, rc)
|
||||||
|
@ -2,38 +2,38 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/fsUtils"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
"github.com/up9inc/mizu/cli/kubernetes"
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"os"
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
"path"
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
var filePath string
|
|
||||||
|
|
||||||
var logsCmd = &cobra.Command{
|
var logsCmd = &cobra.Command{
|
||||||
Use: "logs",
|
Use: "logs",
|
||||||
Short: "Create a zip file with logs for Github issue or troubleshoot",
|
Short: "Create a zip file with logs for Github issue or troubleshoot",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath)
|
go telemetry.ReportRun("logs", config.Config.Logs)
|
||||||
|
|
||||||
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Log.Error(err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx, _ := context.WithCancel(context.Background())
|
ctx, _ := context.WithCancel(context.Background())
|
||||||
|
|
||||||
if filePath == "" {
|
if validationErr := config.Config.Logs.Validate(); validationErr != nil {
|
||||||
pwd, err := os.Getwd()
|
return errormessage.FormatError(validationErr)
|
||||||
if err != nil {
|
|
||||||
mizu.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)
|
|
||||||
|
|
||||||
if err := fsUtils.DumpLogs(kubernetesProvider, ctx, filePath); err != nil {
|
logger.Log.Debugf("Using file path %s", config.Config.Logs.FilePath())
|
||||||
mizu.Log.Errorf("Failed dump logs %v", err)
|
|
||||||
|
if dumpLogsErr := fsUtils.DumpLogs(kubernetesProvider, ctx, config.Config.Logs.FilePath()); dumpLogsErr != nil {
|
||||||
|
logger.Log.Errorf("Failed dump logs %v", dumpLogsErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -42,5 +42,9 @@ var logsCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(logsCmd)
|
rootCmd.AddCommand(logsCmd)
|
||||||
logsCmd.Flags().StringVarP(&filePath, "file", "f", "", "Path for zip file (default current <pwd>\\mizu_logs.zip)")
|
|
||||||
|
defaultLogsConfig := configStructs.LogsConfig{}
|
||||||
|
defaults.Set(&defaultLogsConfig)
|
||||||
|
|
||||||
|
logsCmd.Flags().StringP(configStructs.FileLogsName, "f", defaultLogsConfig.FileStr, "Path for zip file (default current <pwd>\\mizu_logs.zip)")
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,12 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/spf13/cobra"
|
"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"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/version"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@ -13,24 +17,35 @@ var rootCmd = &cobra.Command{
|
|||||||
Long: `A web traffic viewer for kubernetes
|
Long: `A web traffic viewer for kubernetes
|
||||||
Further info is available at https://github.com/up9inc/mizu`,
|
Further info is available at https://github.com/up9inc/mizu`,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil {
|
if err := config.InitConfig(cmd); err != nil {
|
||||||
mizu.Log.Errorf("Failed to use mizu folder, %v", err)
|
logger.Log.Fatal(err)
|
||||||
}
|
}
|
||||||
mizu.InitLogger()
|
|
||||||
if err := mizu.InitConfig(cmd); err != nil {
|
|
||||||
mizu.Log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNewVersionIfNeeded(versionChan chan string) {
|
||||||
|
versionMsg := <-versionChan
|
||||||
|
if versionMsg != "" {
|
||||||
|
logger.Log.Infof(uiUtils.Yellow, versionMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
// This is called by main.main(). It only needs to happen once to the tapCmd.
|
// This is called by main.main(). It only needs to happen once to the tapCmd.
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil {
|
||||||
|
logger.Log.Errorf("Failed to use mizu folder, %v", err)
|
||||||
|
}
|
||||||
|
logger.InitLogger()
|
||||||
|
|
||||||
|
versionChan := make(chan string)
|
||||||
|
defer printNewVersionIfNeeded(versionChan)
|
||||||
|
go version.CheckNewerVersion(versionChan)
|
||||||
|
|
||||||
cobra.CheckErr(rootCmd.Execute())
|
cobra.CheckErr(rootCmd.Execute())
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,15 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"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"
|
"os"
|
||||||
|
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/errormessage"
|
"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"
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,31 +22,27 @@ var tapCmd = &cobra.Command{
|
|||||||
Long: `Record the ingoing traffic of a kubernetes pod.
|
Long: `Record the ingoing traffic of a kubernetes pod.
|
||||||
Supported protocols are HTTP and gRPC.`,
|
Supported protocols are HTTP and gRPC.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
go mizu.ReportRun("tap", mizu.Config.Tap)
|
go telemetry.ReportRun("tap", config.Config.Tap)
|
||||||
RunMizuTap()
|
RunMizuTap()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
mizu.Config.Tap.PodRegexStr = args[0]
|
config.Config.Tap.PodRegexStr = args[0]
|
||||||
} else if len(args) > 1 {
|
} else if len(args) > 1 {
|
||||||
return errors.New("unexpected number of arguments")
|
return errors.New("unexpected number of arguments")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mizu.Config.Validate(); err != nil {
|
if err := config.Config.Tap.Validate(); err != nil {
|
||||||
return errormessage.FormatError(err)
|
return errormessage.FormatError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := mizu.Config.Tap.Validate(); err != nil {
|
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)
|
||||||
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)
|
if config.Config.Tap.Analysis {
|
||||||
|
logger.Log.Infof(analysisMessageToConfirm)
|
||||||
if mizu.Config.Tap.Analysis {
|
|
||||||
mizu.Log.Infof(analysisMessageToConfirm)
|
|
||||||
if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") {
|
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)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,11 +58,10 @@ func init() {
|
|||||||
defaults.Set(&defaultTapConfig)
|
defaults.Set(&defaultTapConfig)
|
||||||
|
|
||||||
tapCmd.Flags().Uint16P(configStructs.GuiPortTapName, "p", defaultTapConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
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().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().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.HideHealthChecksTapName, defaultTapConfig.HideHealthChecks, "Hides requests with kube-probe or prometheus user-agent headers")
|
|
||||||
tapCmd.Flags().Bool(configStructs.DisableRedactionTapName, defaultTapConfig.DisableRedaction, "Disables redaction of potentially sensitive request/response headers and body values")
|
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.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")
|
tapCmd.Flags().String(configStructs.DirectionTapName, defaultTapConfig.Direction, "Record traffic that goes in this direction (relative to the tapped pod): in/any")
|
||||||
|
@ -5,9 +5,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/up9inc/mizu/cli/fsUtils"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/goUtils"
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
"github.com/up9inc/mizu/cli/mizu/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/telemetry"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -46,21 +49,21 @@ var state tapState
|
|||||||
func RunMizuTap() {
|
func RunMizuTap() {
|
||||||
mizuApiFilteringOptions, err := getMizuApiFilteringOptions()
|
mizuApiFilteringOptions, err := getMizuApiFilteringOptions()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
var mizuValidationRules string
|
var mizuValidationRules string
|
||||||
if mizu.Config.Tap.EnforcePolicyFile != "" {
|
if config.Config.Tap.EnforcePolicyFile != "" {
|
||||||
mizuValidationRules, err = readValidationRules(mizu.Config.Tap.EnforcePolicyFile)
|
mizuValidationRules, err = readValidationRules(config.Config.Tap.EnforcePolicyFile)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.KubeConfigPath)
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mizu.Log.Error(err)
|
logger.Log.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,37 +72,45 @@ func RunMizuTap() {
|
|||||||
|
|
||||||
targetNamespaces := getNamespaces(kubernetesProvider)
|
targetNamespaces := getNamespaces(kubernetesProvider)
|
||||||
|
|
||||||
|
if config.Config.IsNsRestrictedMode() {
|
||||||
|
if len(targetNamespaces) != 1 || !mizu.Contains(targetNamespaces, config.Config.MizuResourcesNamespace) {
|
||||||
|
logger.Log.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+
|
||||||
|
"You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, config.MizuResourcesNamespaceConfigName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var namespacesStr string
|
var namespacesStr string
|
||||||
if targetNamespaces[0] != mizu.K8sAllNamespaces {
|
if !mizu.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
|
||||||
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
|
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
|
||||||
} else {
|
} else {
|
||||||
namespacesStr = "all namespaces"
|
namespacesStr = "all namespaces"
|
||||||
}
|
}
|
||||||
mizu.CheckNewerVersion()
|
|
||||||
mizu.Log.Infof("Tapping pods in %s", namespacesStr)
|
logger.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||||
|
|
||||||
if err, _ := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(state.currentlyTappedPods) == 0 {
|
if len(state.currentlyTappedPods) == 0 {
|
||||||
var suggestionStr string
|
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"
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
||||||
|
|
||||||
defer cleanUpMizuResources(kubernetesProvider)
|
defer cleanUpMizu(kubernetesProvider)
|
||||||
if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions, mizuValidationRules); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +131,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 {
|
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 {
|
if err := createMizuNamespace(ctx, kubernetesProvider); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -135,7 +146,7 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := createMizuConfigmap(ctx, kubernetesProvider, mizuValidationRules); err != nil {
|
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
|
state.doNotRemoveConfigMap = true
|
||||||
} else if mizuValidationRules == "" {
|
} else if mizuValidationRules == "" {
|
||||||
state.doNotRemoveConfigMap = true
|
state.doNotRemoveConfigMap = true
|
||||||
@ -145,12 +156,12 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, data string) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Provider) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +170,7 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
|
|
||||||
state.mizuServiceAccountExists, err = createRBACIfNecessary(ctx, kubernetesProvider)
|
state.mizuServiceAccountExists, err = createRBACIfNecessary(ctx, kubernetesProvider)
|
||||||
if err != nil {
|
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
|
var serviceAccountName string
|
||||||
@ -170,25 +181,27 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
opts := &kubernetes.ApiServerOptions{
|
opts := &kubernetes.ApiServerOptions{
|
||||||
Namespace: mizu.Config.MizuResourcesNamespace,
|
Namespace: config.Config.MizuResourcesNamespace,
|
||||||
PodName: mizu.ApiServerPodName,
|
PodName: mizu.ApiServerPodName,
|
||||||
PodImage: mizu.Config.AgentImage,
|
PodImage: config.Config.AgentImage,
|
||||||
ServiceAccountName: serviceAccountName,
|
ServiceAccountName: serviceAccountName,
|
||||||
IsNamespaceRestricted: mizu.Config.IsNsRestrictedMode(),
|
IsNamespaceRestricted: config.Config.IsNsRestrictedMode(),
|
||||||
MizuApiFilteringOptions: mizuApiFilteringOptions,
|
MizuApiFilteringOptions: mizuApiFilteringOptions,
|
||||||
MaxEntriesDBSizeBytes: mizu.Config.Tap.MaxEntriesDBSizeBytes(),
|
MaxEntriesDBSizeBytes: config.Config.Tap.MaxEntriesDBSizeBytes(),
|
||||||
|
Resources: config.Config.Tap.ApiServerResources,
|
||||||
|
ImagePullPolicy: config.Config.ImagePullPolicy(),
|
||||||
}
|
}
|
||||||
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts)
|
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mizu.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName)
|
logger.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -196,9 +209,9 @@ func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) {
|
func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) {
|
||||||
var compiledRegexSlice []*shared.SerializableRegexp
|
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)
|
compiledRegexSlice = make([]*shared.SerializableRegexp, 0)
|
||||||
for _, regexStr := range mizu.Config.Tap.PlainTextFilterRegexes {
|
for _, regexStr := range config.Config.Tap.PlainTextFilterRegexes {
|
||||||
compiledRegex, err := shared.CompileRegexToSerializableRegexp(regexStr)
|
compiledRegex, err := shared.CompileRegexToSerializableRegexp(regexStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -207,7 +220,11 @@ func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &shared.TrafficFilteringOptions{PlainTextMaskingRegexes: compiledRegexSlice, HideHealthChecks: mizu.Config.Tap.HideHealthChecks, DisableRedaction: mizu.Config.Tap.DisableRedaction}, nil
|
return &shared.TrafficFilteringOptions{
|
||||||
|
PlainTextMaskingRegexes: compiledRegexSlice,
|
||||||
|
HealthChecksUserAgentHeaders: config.Config.Tap.HealthChecksUserAgentHeaders,
|
||||||
|
DisableRedaction: config.Config.Tap.DisableRedaction,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string) error {
|
func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string) error {
|
||||||
@ -221,20 +238,22 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
|||||||
|
|
||||||
if err := kubernetesProvider.ApplyMizuTapperDaemonSet(
|
if err := kubernetesProvider.ApplyMizuTapperDaemonSet(
|
||||||
ctx,
|
ctx,
|
||||||
mizu.Config.MizuResourcesNamespace,
|
config.Config.MizuResourcesNamespace,
|
||||||
mizu.TapperDaemonSetName,
|
mizu.TapperDaemonSetName,
|
||||||
mizu.Config.AgentImage,
|
config.Config.AgentImage,
|
||||||
mizu.TapperPodName,
|
mizu.TapperPodName,
|
||||||
fmt.Sprintf("%s.%s.svc.cluster.local", state.apiServerService.Name, state.apiServerService.Namespace),
|
fmt.Sprintf("%s.%s.svc.cluster.local", state.apiServerService.Name, state.apiServerService.Namespace),
|
||||||
nodeToTappedPodIPMap,
|
nodeToTappedPodIPMap,
|
||||||
serviceAccountName,
|
serviceAccountName,
|
||||||
mizu.Config.Tap.TapOutgoing(),
|
config.Config.Tap.TapOutgoing(),
|
||||||
|
config.Config.Tap.TapperResources,
|
||||||
|
config.Config.ImagePullPolicy(),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mizu.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap))
|
logger.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap))
|
||||||
} else {
|
} 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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,70 +261,74 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
|||||||
return nil
|
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)
|
removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if mizu.Config.DumpLogs {
|
if config.Config.DumpLogs {
|
||||||
mizuDir := mizu.GetMizuFolderPath()
|
mizuDir := mizu.GetMizuFolderPath()
|
||||||
filePath = path.Join(mizuDir, fmt.Sprintf("mizu_logs_%s.zip", time.Now().Format("2006_01_02__15_04_05")))
|
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 {
|
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 !config.Config.IsNsRestrictedMode() {
|
||||||
if err := kubernetesProvider.RemoveNamespace(removalCtx, mizu.Config.MizuResourcesNamespace); err != nil {
|
if err := kubernetesProvider.RemoveNamespace(removalCtx, config.Config.MizuResourcesNamespace); err != nil {
|
||||||
mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Namespace %s: %v", mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Namespace %s: %v", config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := kubernetesProvider.RemovePod(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
if err := kubernetesProvider.RemovePod(removalCtx, config.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)))
|
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 {
|
if err := kubernetesProvider.RemoveService(removalCtx, config.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)))
|
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 {
|
if err := kubernetesProvider.RemoveDaemonSet(removalCtx, config.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)))
|
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 !state.doNotRemoveConfigMap {
|
||||||
if err := kubernetesProvider.RemoveConfigMap(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ConfigMapName); err != nil {
|
if err := kubernetesProvider.RemoveConfigMap(removalCtx, config.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)))
|
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 state.mizuServiceAccountExists {
|
||||||
if !mizu.Config.IsNsRestrictedMode() {
|
if !config.Config.IsNsRestrictedMode() {
|
||||||
if err := kubernetesProvider.RemoveNonNamespacedResources(removalCtx, mizu.ClusterRoleName, mizu.ClusterRoleBindingName); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := kubernetesProvider.RemoveServicAccount(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName); err != nil {
|
if err := kubernetesProvider.RemoveServicAccount(removalCtx, config.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)))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := kubernetesProvider.RemoveRole(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.RoleName); err != nil {
|
if err := kubernetesProvider.RemoveRole(removalCtx, config.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)))
|
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 {
|
if err := kubernetesProvider.RemoveRoleBinding(removalCtx, config.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)))
|
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)
|
waitUntilNamespaceDeleted(removalCtx, cancel, kubernetesProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,20 +339,20 @@ func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, k
|
|||||||
waitForFinish(ctx, cancel)
|
waitForFinish(ctx, cancel)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, mizu.Config.MizuResourcesNamespace); err != nil {
|
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, config.Config.MizuResourcesNamespace); err != nil {
|
||||||
switch {
|
switch {
|
||||||
case ctx.Err() == context.Canceled:
|
case ctx.Err() == context.Canceled:
|
||||||
// Do nothing. User interrupted the wait.
|
// Do nothing. User interrupted the wait.
|
||||||
case err == wait.ErrWaitTimeout:
|
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:
|
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() {
|
func reportTappedPods() {
|
||||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort)
|
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort)
|
||||||
tappedPodsUrl := fmt.Sprintf("http://%s/status/tappedPods", mizuProxiedUrl)
|
tappedPodsUrl := fmt.Sprintf("http://%s/status/tappedPods", mizuProxiedUrl)
|
||||||
|
|
||||||
podInfos := make([]shared.PodInfo, 0)
|
podInfos := make([]shared.PodInfo, 0)
|
||||||
@ -339,30 +362,30 @@ func reportTappedPods() {
|
|||||||
tapStatus := shared.TapStatus{Pods: podInfos}
|
tapStatus := shared.TapStatus{Pods: podInfos}
|
||||||
|
|
||||||
if jsonValue, err := json.Marshal(tapStatus); err != nil {
|
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 {
|
} else {
|
||||||
if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
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 {
|
} 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 {
|
} 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) {
|
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() {
|
restartTappers := func() {
|
||||||
err, changeFound := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces)
|
err, changeFound := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces)
|
||||||
if err != nil {
|
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()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !changeFound {
|
if !changeFound {
|
||||||
mizu.Log.Debugf("Nothing changed update tappers not needed")
|
logger.Log.Debugf("Nothing changed update tappers not needed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,11 +393,11 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
|
|
||||||
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
||||||
if err != nil {
|
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()
|
cancel()
|
||||||
}
|
}
|
||||||
if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap); err != nil {
|
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()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -383,13 +406,13 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case pod := <-added:
|
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()
|
restartTappersDebouncer.SetOn()
|
||||||
case pod := <-removed:
|
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()
|
restartTappersDebouncer.SetOn()
|
||||||
case pod := <-modified:
|
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.
|
// 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:
|
// After filtering for IPs, on a normal pod restart this includes the following events:
|
||||||
// - Pod deletion
|
// - Pod deletion
|
||||||
@ -401,13 +424,13 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
case err := <-errorChan:
|
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()
|
restartTappersDebouncer.Cancel()
|
||||||
// TODO: Does this also perform cleanup?
|
// TODO: Does this also perform cleanup?
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
case <-ctx.Done():
|
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()
|
restartTappersDebouncer.Cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -416,18 +439,18 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
|
|
||||||
func updateCurrentlyTappedPods(kubernetesProvider *kubernetes.Provider, ctx context.Context, targetNamespaces []string) (error, bool) {
|
func updateCurrentlyTappedPods(kubernetesProvider *kubernetes.Provider, ctx context.Context, targetNamespaces []string) (error, bool) {
|
||||||
changeFound := false
|
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
|
return err, false
|
||||||
} else {
|
} else {
|
||||||
podsToTap := excludeMizuPods(matchingPods)
|
podsToTap := excludeMizuPods(matchingPods)
|
||||||
addedPods, removedPods := getPodArrayDiff(state.currentlyTappedPods, podsToTap)
|
addedPods, removedPods := getPodArrayDiff(state.currentlyTappedPods, podsToTap)
|
||||||
for _, addedPod := range addedPods {
|
for _, addedPod := range addedPods {
|
||||||
changeFound = true
|
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 {
|
for _, removedPod := range removedPods {
|
||||||
changeFound = true
|
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
|
state.currentlyTappedPods = podsToTap
|
||||||
}
|
}
|
||||||
@ -475,86 +498,86 @@ func getMissingPods(pods1 []core.Pod, pods2 []core.Pod) []core.Pod {
|
|||||||
|
|
||||||
func createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
func createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.ApiServerPodName))
|
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
|
isPodReady := false
|
||||||
timeAfter := time.After(25 * time.Second)
|
timeAfter := time.After(25 * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
mizu.Log.Debugf("Watching API Server pod loop, ctx done")
|
logger.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||||
return
|
return
|
||||||
case <-added:
|
case <-added:
|
||||||
mizu.Log.Debugf("Watching API Server pod loop, added")
|
logger.Log.Debugf("Watching API Server pod loop, added")
|
||||||
continue
|
continue
|
||||||
case <-removed:
|
case <-removed:
|
||||||
mizu.Log.Infof("%s removed", mizu.ApiServerPodName)
|
logger.Log.Infof("%s removed", mizu.ApiServerPodName)
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
case modifiedPod := <-modified:
|
case modifiedPod := <-modified:
|
||||||
if modifiedPod == nil {
|
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
|
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 {
|
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
|
||||||
isPodReady = true
|
isPodReady = true
|
||||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
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
|
time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready
|
||||||
requestForAnalysis()
|
requestForAnalysis()
|
||||||
reportTappedPods()
|
reportTappedPods()
|
||||||
}
|
}
|
||||||
case <-timeAfter:
|
case <-timeAfter:
|
||||||
if !isPodReady {
|
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()
|
cancel()
|
||||||
}
|
}
|
||||||
case <-errorChan:
|
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()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
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 {
|
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))
|
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestForAnalysis() {
|
func requestForAnalysis() {
|
||||||
if !mizu.Config.Tap.Analysis {
|
if !config.Config.Tap.Analysis {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort)
|
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.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)
|
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)
|
u, parseErr := url.ParseRequestURI(urlPath)
|
||||||
if parseErr != nil {
|
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 {
|
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 {
|
} 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 {
|
} 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) {
|
func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) (bool, error) {
|
||||||
if !mizu.Config.IsNsRestrictedMode() {
|
if !config.Config.IsNsRestrictedMode() {
|
||||||
err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
err := kubernetesProvider.CreateMizuRBAC(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -589,10 +612,10 @@ func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
|
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
|
||||||
if mizu.Config.Tap.AllNamespaces {
|
if config.Config.Tap.AllNamespaces {
|
||||||
return []string{mizu.K8sAllNamespaces}
|
return []string{mizu.K8sAllNamespaces}
|
||||||
} else if len(mizu.Config.Tap.Namespaces) > 0 {
|
} else if len(config.Config.Tap.Namespaces) > 0 {
|
||||||
return mizu.Config.Tap.Namespaces
|
return config.Config.Tap.Namespaces
|
||||||
} else {
|
} else {
|
||||||
return []string{kubernetesProvider.CurrentNamespace()}
|
return []string{kubernetesProvider.CurrentNamespace()}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,31 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
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"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var versionCmd = &cobra.Command{
|
var versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "Print version info",
|
Short: "Print version info",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
go mizu.ReportRun("version", mizu.Config.Version)
|
go telemetry.ReportRun("version", config.Config.Version)
|
||||||
if mizu.Config.Version.DebugInfo {
|
|
||||||
|
if config.Config.Version.DebugInfo {
|
||||||
timeStampInt, _ := strconv.ParseInt(mizu.BuildTimestamp, 10, 0)
|
timeStampInt, _ := strconv.ParseInt(mizu.BuildTimestamp, 10, 0)
|
||||||
mizu.Log.Infof("Version: %s \nBranch: %s (%s)", mizu.SemVer, mizu.Branch, mizu.GitCommitHash)
|
logger.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("Build Time: %s (%s)", mizu.BuildTimestamp, time.Unix(timeStampInt, 0))
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
mizu.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch)
|
logger.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
@ -3,15 +3,16 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"github.com/creasty/defaults"
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
var viewCmd = &cobra.Command{
|
var viewCmd = &cobra.Command{
|
||||||
Use: "view",
|
Use: "view",
|
||||||
Short: "Open GUI in browser",
|
Short: "Open GUI in browser",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
go mizu.ReportRun("view", mizu.Config.View)
|
go telemetry.ReportRun("view", config.Config.View)
|
||||||
runMizuView()
|
runMizuView()
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -24,5 +25,4 @@ func init() {
|
|||||||
defaults.Set(&defaultViewConfig)
|
defaults.Set(&defaultViewConfig)
|
||||||
|
|
||||||
viewCmd.Flags().Uint16P(configStructs.GuiPortViewName, "p", defaultViewConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
viewCmd.Flags().Uint16P(configStructs.GuiPortViewName, "p", defaultViewConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
||||||
viewCmd.Flags().StringP(configStructs.KubeConfigPathViewName, "k", defaultViewConfig.KubeConfigPath, "Path to kube-config file")
|
|
||||||
}
|
}
|
||||||
|
@ -3,46 +3,51 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/kubernetes"
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/version"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func runMizuView() {
|
func runMizuView() {
|
||||||
kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath)
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mizu.Log.Error(err)
|
logger.Log.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
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 {
|
if err != nil {
|
||||||
mizu.Log.Errorf("Failed to found mizu service %v", err)
|
logger.Log.Errorf("Failed to found mizu service %v", err)
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !exists {
|
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()
|
cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort)
|
mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.View.GuiPort)
|
||||||
_, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl))
|
_, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl))
|
||||||
if err == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
mizu.Log.Debugf("Found service %s, creating k8s proxy", mizu.ApiServerPodName)
|
logger.Log.Infof("Establishing connection to k8s cluster...")
|
||||||
|
|
||||||
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||||
|
|
||||||
mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort))
|
time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready
|
||||||
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()
|
cancel()
|
||||||
return
|
return
|
||||||
} else if !isCompatible {
|
} else if !isCompatible {
|
||||||
|
325
cli/config/config.go
Normal file
325
cli/config/config.go
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"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 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 by removing it (%v) and using `mizu config -r`", 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,17 @@
|
|||||||
package mizu
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
"github.com/up9inc/mizu/cli/mizu/configStructs"
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/client-go/util/homedir"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AgentImageConfigName = "agent-image"
|
|
||||||
MizuResourcesNamespaceConfigName = "mizu-resources-namespace"
|
MizuResourcesNamespaceConfigName = "mizu-resources-namespace"
|
||||||
TelemetryConfigName = "telemetry"
|
|
||||||
DumpLogsConfigName = "dump-logs"
|
|
||||||
KubeConfigPathName = "kube-config-path"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigStruct struct {
|
type ConfigStruct struct {
|
||||||
@ -19,17 +19,38 @@ type ConfigStruct struct {
|
|||||||
Fetch configStructs.FetchConfig `yaml:"fetch"`
|
Fetch configStructs.FetchConfig `yaml:"fetch"`
|
||||||
Version configStructs.VersionConfig `yaml:"version"`
|
Version configStructs.VersionConfig `yaml:"version"`
|
||||||
View configStructs.ViewConfig `yaml:"view"`
|
View configStructs.ViewConfig `yaml:"view"`
|
||||||
AgentImage string `yaml:"agent-image,omitempty"`
|
Logs configStructs.LogsConfig `yaml:"logs"`
|
||||||
|
Config configStructs.ConfigConfig `yaml:"config,omitempty"`
|
||||||
|
AgentImage string `yaml:"agent-image,omitempty" readonly:""`
|
||||||
|
ImagePullPolicyStr string `yaml:"image-pull-policy" default:"Always"`
|
||||||
MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"`
|
MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"`
|
||||||
Telemetry bool `yaml:"telemetry" default:"true"`
|
Telemetry bool `yaml:"telemetry" default:"true"`
|
||||||
DumpLogs bool `yaml:"dump-logs" default:"false"`
|
DumpLogs bool `yaml:"dump-logs" default:"false"`
|
||||||
KubeConfigPath string `yaml:"kube-config-path" default:""`
|
KubeConfigPathStr string `yaml:"kube-config-path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *ConfigStruct) SetDefaults() {
|
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) ImagePullPolicy() v1.PullPolicy {
|
||||||
|
return v1.PullPolicy(config.ImagePullPolicyStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *ConfigStruct) IsNsRestrictedMode() bool {
|
func (config *ConfigStruct) IsNsRestrictedMode() bool {
|
||||||
return config.MizuResourcesNamespace != "mizu" // Notice "mizu" string must match the default MizuResourcesNamespace
|
return config.MizuResourcesNamespace != "mizu" // Notice "mizu" string must match the default MizuResourcesNamespace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *ConfigStruct) KubeConfigPath() string {
|
||||||
|
if config.KubeConfigPathStr != "" {
|
||||||
|
return config.KubeConfigPathStr
|
||||||
|
}
|
||||||
|
|
||||||
|
envKubeConfigPath := os.Getenv("KUBECONFIG")
|
||||||
|
if envKubeConfigPath != "" {
|
||||||
|
return envKubeConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
home := homedir.HomeDir()
|
||||||
|
return filepath.Join(home, ".kube", "config")
|
||||||
|
}
|
9
cli/config/configStructs/configConfig.go
Normal file
9
cli/config/configStructs/configConfig.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package configStructs
|
||||||
|
|
||||||
|
const (
|
||||||
|
RegenerateConfigName = "regenerate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigConfig struct {
|
||||||
|
Regenerate bool `yaml:"regenerate,omitempty" default:"false" readonly:""`
|
||||||
|
}
|
35
cli/config/configStructs/logsConfig.go
Normal file
35
cli/config/configStructs/logsConfig.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package configStructs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileLogsName = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogsConfig struct {
|
||||||
|
FileStr string `yaml:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *LogsConfig) Validate() error {
|
||||||
|
if config.FileStr == "" {
|
||||||
|
_, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get PWD, %v (try using `mizu logs -f <full path dest zip file>)`", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (config *LogsConfig) FilePath() string {
|
||||||
|
if config.FileStr == "" {
|
||||||
|
pwd, _ := os.Getwd()
|
||||||
|
return path.Join(pwd, "mizu_logs.zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.FileStr
|
||||||
|
}
|
@ -10,14 +10,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AnalysisDestinationTapName = "dest"
|
|
||||||
SleepIntervalSecTapName = "upload-interval"
|
|
||||||
GuiPortTapName = "gui-port"
|
GuiPortTapName = "gui-port"
|
||||||
NamespacesTapName = "namespaces"
|
NamespacesTapName = "namespaces"
|
||||||
AnalysisTapName = "analysis"
|
AnalysisTapName = "analysis"
|
||||||
AllNamespacesTapName = "all-namespaces"
|
AllNamespacesTapName = "all-namespaces"
|
||||||
PlainTextFilterRegexesTapName = "regex-masking"
|
PlainTextFilterRegexesTapName = "regex-masking"
|
||||||
HideHealthChecksTapName = "hide-healthchecks"
|
|
||||||
DisableRedactionTapName = "no-redact"
|
DisableRedactionTapName = "no-redact"
|
||||||
HumanMaxEntriesDBSizeTapName = "max-entries-db-size"
|
HumanMaxEntriesDBSizeTapName = "max-entries-db-size"
|
||||||
DirectionTapName = "direction"
|
DirectionTapName = "direction"
|
||||||
@ -34,12 +31,21 @@ type TapConfig struct {
|
|||||||
Analysis bool `yaml:"analysis" default:"false"`
|
Analysis bool `yaml:"analysis" default:"false"`
|
||||||
AllNamespaces bool `yaml:"all-namespaces" default:"false"`
|
AllNamespaces bool `yaml:"all-namespaces" default:"false"`
|
||||||
PlainTextFilterRegexes []string `yaml:"regex-masking"`
|
PlainTextFilterRegexes []string `yaml:"regex-masking"`
|
||||||
HideHealthChecks bool `yaml:"hide-healthchecks" default:"false"`
|
HealthChecksUserAgentHeaders []string `yaml:"ignored-user-agents"`
|
||||||
DisableRedaction bool `yaml:"no-redact" default:"false"`
|
DisableRedaction bool `yaml:"no-redact" default:"false"`
|
||||||
HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"`
|
HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"`
|
||||||
Direction string `yaml:"direction" default:"in"`
|
Direction string `yaml:"direction" default:"in"`
|
||||||
DryRun bool `yaml:"dry-run" default:"false"`
|
DryRun bool `yaml:"dry-run" default:"false"`
|
||||||
EnforcePolicyFile string `yaml:"test-rules"`
|
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 {
|
func (config *TapConfig) PodRegex() *regexp.Regexp {
|
9
cli/config/configStructs/viewConfig.go
Normal file
9
cli/config/configStructs/viewConfig.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package configStructs
|
||||||
|
|
||||||
|
const (
|
||||||
|
GuiPortViewName = "gui-port"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ViewConfig struct {
|
||||||
|
GuiPort uint16 `yaml:"gui-port" default:"8899"`
|
||||||
|
}
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
|
||||||
|
|
||||||
regexpsyntax "regexp/syntax"
|
regexpsyntax "regexp/syntax"
|
||||||
|
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
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 "+
|
"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>",
|
"in the config file or setting the tapped namespace with --%s %s=<NAMEPSACE>",
|
||||||
err,
|
err,
|
||||||
mizu.MizuResourcesNamespaceConfigName,
|
config.MizuResourcesNamespaceConfigName,
|
||||||
mizu.SetCommandName,
|
config.SetCommandName,
|
||||||
mizu.MizuResourcesNamespaceConfigName)
|
config.MizuResourcesNamespaceConfigName)
|
||||||
} else if syntaxError, isSyntaxError := asRegexSyntaxError(err); isSyntaxError {
|
} else if syntaxError, isSyntaxError := asRegexSyntaxError(err); isSyntaxError {
|
||||||
errorNew = fmt.Errorf("regex %s is invalid: %w", syntaxError.Expr, err)
|
errorNew = fmt.Errorf("regex %s is invalid: %w", syntaxError.Expr, err)
|
||||||
} else {
|
} 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
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ go 1.16
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/creasty/defaults v1.5.1
|
github.com/creasty/defaults v1.5.1
|
||||||
|
github.com/denisbrodbeck/machineid v1.0.1
|
||||||
github.com/google/go-github/v37 v37.0.0
|
github.com/google/go-github/v37 v37.0.0
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
|
@ -88,6 +88,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
|
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
|
||||||
|
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||||
|
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
@ -217,7 +219,6 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
|
||||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
@ -411,8 +412,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
|
|||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||||
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
|
||||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
|
||||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
|
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
@ -7,7 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -36,7 +37,6 @@ import (
|
|||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
_ "k8s.io/client-go/tools/portforward"
|
_ "k8s.io/client-go/tools/portforward"
|
||||||
watchtools "k8s.io/client-go/tools/watch"
|
watchtools "k8s.io/client-go/tools/watch"
|
||||||
"k8s.io/client-go/util/homedir"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
@ -55,13 +55,23 @@ func NewProvider(kubeConfigPath string) (*Provider, error) {
|
|||||||
restClientConfig, err := kubernetesConfig.ClientConfig()
|
restClientConfig, err := kubernetesConfig.ClientConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if clientcmd.IsEmptyConfig(err) {
|
if clientcmd.IsEmptyConfig(err) {
|
||||||
return nil, fmt.Errorf("Couldn't find the kube config file, or file is empty. Try adding '--kube-config=<path to kube config file>'\n")
|
return nil, fmt.Errorf("couldn't find the kube config file, or file is empty (%s)\n" +
|
||||||
|
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
|
||||||
}
|
}
|
||||||
if clientcmd.IsConfigurationInvalid(err) {
|
if clientcmd.IsConfigurationInvalid(err) {
|
||||||
return nil, fmt.Errorf("Invalid kube config file. Try using a different config with '--kube-config=<path to kube config file>'\n")
|
return nil, fmt.Errorf("invalid kube config file (%s)\n" +
|
||||||
|
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("error while using kube config (%s)\n" +
|
||||||
|
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSet, err := getClientSet(restClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while using kube config (%s)\n" +
|
||||||
|
"you can set alternative kube config file path by adding the kube-config-path field to the mizu config file, err: %w", kubeConfigPath, err)
|
||||||
}
|
}
|
||||||
clientSet := getClientSet(restClientConfig)
|
|
||||||
|
|
||||||
return &Provider{
|
return &Provider{
|
||||||
clientSet: clientSet,
|
clientSet: clientSet,
|
||||||
@ -140,6 +150,8 @@ type ApiServerOptions struct {
|
|||||||
IsNamespaceRestricted bool
|
IsNamespaceRestricted bool
|
||||||
MizuApiFilteringOptions *shared.TrafficFilteringOptions
|
MizuApiFilteringOptions *shared.TrafficFilteringOptions
|
||||||
MaxEntriesDBSizeBytes int64
|
MaxEntriesDBSizeBytes int64
|
||||||
|
Resources configStructs.Resources
|
||||||
|
ImagePullPolicy core.PullPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions) (*core.Pod, error) {
|
func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions) (*core.Pod, error) {
|
||||||
@ -152,19 +164,19 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiS
|
|||||||
configMapOptional := true
|
configMapOptional := true
|
||||||
configMapVolumeName.Optional = &configMapOptional
|
configMapVolumeName.Optional = &configMapOptional
|
||||||
|
|
||||||
cpuLimit, err := resource.ParseQuantity("750m")
|
cpuLimit, err := resource.ParseQuantity(opts.Resources.CpuLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(fmt.Sprintf("invalid cpu limit for %s container", opts.PodName))
|
return nil, errors.New(fmt.Sprintf("invalid cpu limit for %s container", opts.PodName))
|
||||||
}
|
}
|
||||||
memLimit, err := resource.ParseQuantity("512Mi")
|
memLimit, err := resource.ParseQuantity(opts.Resources.MemoryLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(fmt.Sprintf("invalid memory limit for %s container", opts.PodName))
|
return nil, errors.New(fmt.Sprintf("invalid memory limit for %s container", opts.PodName))
|
||||||
}
|
}
|
||||||
cpuRequests, err := resource.ParseQuantity("50m")
|
cpuRequests, err := resource.ParseQuantity(opts.Resources.CpuRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(fmt.Sprintf("invalid cpu request for %s container", opts.PodName))
|
return nil, errors.New(fmt.Sprintf("invalid cpu request for %s container", opts.PodName))
|
||||||
}
|
}
|
||||||
memRequests, err := resource.ParseQuantity("50Mi")
|
memRequests, err := resource.ParseQuantity(opts.Resources.MemoryRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New(fmt.Sprintf("invalid memory request for %s container", opts.PodName))
|
return nil, errors.New(fmt.Sprintf("invalid memory request for %s container", opts.PodName))
|
||||||
}
|
}
|
||||||
@ -185,7 +197,7 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiS
|
|||||||
{
|
{
|
||||||
Name: opts.PodName,
|
Name: opts.PodName,
|
||||||
Image: opts.PodImage,
|
Image: opts.PodImage,
|
||||||
ImagePullPolicy: core.PullAlways,
|
ImagePullPolicy: opts.ImagePullPolicy,
|
||||||
VolumeMounts: []core.VolumeMount{
|
VolumeMounts: []core.VolumeMount{
|
||||||
{
|
{
|
||||||
Name: mizu.ConfigMapName,
|
Name: mizu.ConfigMapName,
|
||||||
@ -561,8 +573,8 @@ func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string,
|
|||||||
return nil
|
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 {
|
func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeToTappedPodIPMap map[string][]string, serviceAccountName string, tapOutgoing bool, resources configStructs.Resources, imagePullPolicy core.PullPolicy) error {
|
||||||
mizu.Log.Debugf("Applying %d tapper deamonsets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeToTappedPodIPMap), namespace, daemonSetName, podImage, tapperPodName)
|
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 {
|
if len(nodeToTappedPodIPMap) == 0 {
|
||||||
return fmt.Errorf("Daemon set %s must tap at least 1 pod", daemonSetName)
|
return fmt.Errorf("Daemon set %s must tap at least 1 pod", daemonSetName)
|
||||||
@ -577,7 +589,6 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
|||||||
"./mizuagent",
|
"./mizuagent",
|
||||||
"-i", "any",
|
"-i", "any",
|
||||||
"--tap",
|
"--tap",
|
||||||
"--hardump",
|
|
||||||
"--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp),
|
"--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp),
|
||||||
}
|
}
|
||||||
if tapOutgoing {
|
if tapOutgoing {
|
||||||
@ -587,7 +598,7 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
|||||||
agentContainer := applyconfcore.Container()
|
agentContainer := applyconfcore.Container()
|
||||||
agentContainer.WithName(tapperPodName)
|
agentContainer.WithName(tapperPodName)
|
||||||
agentContainer.WithImage(podImage)
|
agentContainer.WithImage(podImage)
|
||||||
agentContainer.WithImagePullPolicy(core.PullAlways)
|
agentContainer.WithImagePullPolicy(imagePullPolicy)
|
||||||
agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(true))
|
agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(true))
|
||||||
agentContainer.WithCommand(mizuCmd...)
|
agentContainer.WithCommand(mizuCmd...)
|
||||||
agentContainer.WithEnv(
|
agentContainer.WithEnv(
|
||||||
@ -601,19 +612,19 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
cpuLimit, err := resource.ParseQuantity("500m")
|
cpuLimit, err := resource.ParseQuantity(resources.CpuLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New(fmt.Sprintf("invalid cpu limit for %s container", tapperPodName))
|
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 {
|
if err != nil {
|
||||||
return errors.New(fmt.Sprintf("invalid memory limit for %s container", tapperPodName))
|
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 {
|
if err != nil {
|
||||||
return errors.New(fmt.Sprintf("invalid cpu request for %s container", tapperPodName))
|
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 {
|
if err != nil {
|
||||||
return errors.New(fmt.Sprintf("invalid memory request for %s container", tapperPodName))
|
return errors.New(fmt.Sprintf("invalid memory request for %s container", tapperPodName))
|
||||||
}
|
}
|
||||||
@ -728,25 +739,17 @@ func (provider *Provider) GetPodLogs(namespace string, podName string, ctx conte
|
|||||||
return str, nil
|
return str, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientSet(config *restclient.Config) *kubernetes.Clientset {
|
func getClientSet(config *restclient.Config) (*kubernetes.Clientset, error) {
|
||||||
clientSet, err := kubernetes.NewForConfig(config)
|
clientSet, err := kubernetes.NewForConfig(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err.Error())
|
return nil, err
|
||||||
}
|
}
|
||||||
return clientSet
|
|
||||||
|
return clientSet, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig {
|
func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig {
|
||||||
if kubeConfigPath == "" {
|
logger.Log.Debugf("Using kube config %s", kubeConfigPath)
|
||||||
kubeConfigPath = os.Getenv("KUBECONFIG")
|
|
||||||
}
|
|
||||||
|
|
||||||
if kubeConfigPath == "" {
|
|
||||||
home := homedir.HomeDir()
|
|
||||||
kubeConfigPath = filepath.Join(home, ".kube", "config")
|
|
||||||
}
|
|
||||||
|
|
||||||
mizu.Log.Debugf("Using kube config %s", kubeConfigPath)
|
|
||||||
configPathList := filepath.SplitList(kubeConfigPath)
|
configPathList := filepath.SplitList(kubeConfigPath)
|
||||||
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}
|
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}
|
||||||
if len(configPathList) <= 1 {
|
if len(configPathList) <= 1 {
|
||||||
|
@ -2,7 +2,7 @@ package kubernetes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"k8s.io/kubectl/pkg/proxy"
|
"k8s.io/kubectl/pkg/proxy"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -14,7 +14,7 @@ const k8sProxyApiPrefix = "/"
|
|||||||
const mizuServicePort = 80
|
const mizuServicePort = 80
|
||||||
|
|
||||||
func StartProxy(kubernetesProvider *Provider, mizuPort uint16, mizuNamespace string, mizuServiceName string) error {
|
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{
|
filter := &proxy.FilterServer{
|
||||||
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
|
AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE),
|
||||||
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
|
RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE),
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package mizu
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
@ -13,7 +14,7 @@ var format = logging.MustStringFormatter(
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetLogFilePath() string {
|
func GetLogFilePath() string {
|
||||||
return path.Join(GetMizuFolderPath(), "mizu_cli.log")
|
return path.Join(mizu.GetMizuFolderPath(), "mizu_cli.log")
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitLogger() {
|
func InitLogger() {
|
||||||
@ -34,5 +35,5 @@ func InitLogger() {
|
|||||||
logging.SetBackend(backend1Leveled, backend2Formatter)
|
logging.SetBackend(backend1Leveled, backend2Formatter)
|
||||||
|
|
||||||
Log.Debugf("\n\n\n")
|
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 (
|
import (
|
||||||
"github.com/up9inc/mizu/cli/cmd"
|
"github.com/up9inc/mizu/cli/cmd"
|
||||||
"github.com/up9inc/mizu/cli/goUtils"
|
"github.com/up9inc/mizu/cli/mizu/goUtils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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, "\", \"")))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package configStructs
|
|
||||||
|
|
||||||
const (
|
|
||||||
GuiPortViewName = "gui-port"
|
|
||||||
KubeConfigPathViewName = "kube-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ViewConfig struct {
|
|
||||||
GuiPort uint16 `yaml:"gui-port" default:"8899"`
|
|
||||||
KubeConfigPath string `yaml:"kube-config"`
|
|
||||||
}
|
|
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
|
package goUtils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
)
|
)
|
||||||
@ -10,7 +10,7 @@ func HandleExcWrapper(fn interface{}, params ...interface{}) (result []reflect.V
|
|||||||
defer func() {
|
defer func() {
|
||||||
if panicMessage := recover(); panicMessage != nil {
|
if panicMessage := recover(); panicMessage != nil {
|
||||||
stack := debug.Stack()
|
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)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/up9inc/mizu/cli/logger"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -41,22 +43,22 @@ func CheckVersionCompatibility(port uint16) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if semver.SemVersion(apiSemVer).Major() == semver.SemVersion(SemVer).Major() &&
|
if semver.SemVersion(apiSemVer).Major() == semver.SemVersion(mizu.SemVer).Major() &&
|
||||||
semver.SemVersion(apiSemVer).Minor() == semver.SemVersion(SemVer).Minor() {
|
semver.SemVersion(apiSemVer).Minor() == semver.SemVersion(mizu.SemVer).Minor() {
|
||||||
return true, nil
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckNewerVersion() {
|
func CheckNewerVersion(versionChan chan string) {
|
||||||
Log.Debugf("Checking for newer version...")
|
logger.Log.Debugf("Checking for newer version...")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
client := github.NewClient(nil)
|
client := github.NewClient(nil)
|
||||||
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "up9inc", "mizu")
|
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "up9inc", "mizu")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Log.Debugf("[ERROR] Failed to get latest release")
|
logger.Log.Debugf("[ERROR] Failed to get latest release")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,26 +70,31 @@ func CheckNewerVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if versionFileUrl == "" {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := http.Get(versionFileUrl)
|
res, err := http.Get(versionFileUrl)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(res.Body)
|
data, err := ioutil.ReadAll(res.Body)
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
gitHubVersion := string(data)
|
gitHubVersion := string(data)
|
||||||
gitHubVersion = gitHubVersion[:len(gitHubVersion)-1]
|
gitHubVersion = gitHubVersion[:len(gitHubVersion)-1]
|
||||||
Log.Debugf("Finished version validation, took %v", time.Since(start))
|
|
||||||
if SemVer < gitHubVersion {
|
gitHubVersionSemVer := semver.SemVersion(gitHubVersion)
|
||||||
Log.Infof(uiUtils.Yellow, fmt.Sprintf("Update available! %v -> %v (%v)", SemVer, gitHubVersion, *latestRelease.HTMLURL))
|
currentSemVer := semver.SemVersion(mizu.SemVer)
|
||||||
|
logger.Log.Debugf("Finished version validation, github version %v, current version %v, took %v", gitHubVersion, currentSemVer, time.Since(start))
|
||||||
|
|
||||||
|
if gitHubVersionSemVer.GreaterThan(currentSemVer) {
|
||||||
|
versionChan <- fmt.Sprintf("Update available! %v -> %v (%v)", mizu.SemVer, gitHubVersion, *latestRelease.HTMLURL)
|
||||||
}
|
}
|
||||||
|
versionChan <- ""
|
||||||
}
|
}
|
114
cli/telemetry/telemetry.go
Normal file
114
cli/telemetry/telemetry.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package telemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/denisbrodbeck/machineid"
|
||||||
|
"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
|
||||||
|
|
||||||
|
if machineId, err := machineid.ProtectedID("mizu"); err == nil {
|
||||||
|
argsMap["machineId"] = machineId
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonValue, _ := json.Marshal(argsMap)
|
||||||
|
|
||||||
|
if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||||
|
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.
|
||||||
|
|
@ -3,8 +3,7 @@ module github.com/up9inc/mizu/shared
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/martian v2.1.0+incompatible // indirect
|
|
||||||
github.com/gorilla/websocket v1.4.2
|
|
||||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect
|
|
||||||
github.com/docker/go-units v0.4.0
|
github.com/docker/go-units v0.4.0
|
||||||
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
|
||||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -75,8 +75,8 @@ func CreateWebSocketMessageTypeAnalyzeStatus(analyzeStatus AnalyzeStatus) WebSoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TrafficFilteringOptions struct {
|
type TrafficFilteringOptions struct {
|
||||||
|
HealthChecksUserAgentHeaders []string
|
||||||
PlainTextMaskingRegexes []*SerializableRegexp
|
PlainTextMaskingRegexes []*SerializableRegexp
|
||||||
HideHealthChecks bool
|
|
||||||
DisableRedaction bool
|
DisableRedaction bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,3 +26,23 @@ func (v SemVersion) Patch() string {
|
|||||||
_, _, patch := v.Breakdown()
|
_, _, patch := v.Breakdown()
|
||||||
return patch
|
return patch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v SemVersion) GreaterThan(v2 SemVersion) bool {
|
||||||
|
if v.Major() > v2.Major() {
|
||||||
|
return true
|
||||||
|
} else if v.Major() < v2.Major() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Minor() > v2.Minor() {
|
||||||
|
return true
|
||||||
|
} else if v.Minor() < v2.Minor() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Patch() > v2.Patch() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -80,7 +80,7 @@ func (h *httpReader) Read(p []byte) (int, error) {
|
|||||||
err := clientHello.Unmarshall(msg.bytes)
|
err := clientHello.Unmarshall(msg.bytes)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
statsTracker.incTlsConnectionsCount()
|
statsTracker.incTlsConnectionsCount()
|
||||||
fmt.Printf("Detected TLS client hello with SNI %s\n", clientHello.SNI)
|
Debug("Detected TLS client hello with SNI %s\n", clientHello.SNI)
|
||||||
numericPort, _ := strconv.Atoi(h.tcpID.dstPort)
|
numericPort, _ := strconv.Atoi(h.tcpID.dstPort)
|
||||||
h.outboundLinkWriter.WriteOutboundLink(h.tcpID.srcIP, h.tcpID.dstIP, numericPort, clientHello.SNI, TLSProtocol)
|
h.outboundLinkWriter.WriteOutboundLink(h.tcpID.srcIP, h.tcpID.dstIP, numericPort, clientHello.SNI, TLSProtocol)
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,6 @@ var staleTimeoutSeconds = flag.Int("staletimout", 120, "Max time in seconds to k
|
|||||||
var memprofile = flag.String("memprofile", "", "Write memory profile")
|
var memprofile = flag.String("memprofile", "", "Write memory profile")
|
||||||
|
|
||||||
// output
|
// output
|
||||||
var dumpToHar = flag.Bool("hardump", false, "Dump traffic to har files")
|
|
||||||
var HarOutputDir = flag.String("hardir", "", "Directory in which to store output har files")
|
var HarOutputDir = flag.String("hardir", "", "Directory in which to store output har files")
|
||||||
var harEntriesPerFile = flag.Int("harentriesperfile", 200, "Number of max number of har entries to store in each file")
|
var harEntriesPerFile = flag.Int("harentriesperfile", 200, "Number of max number of har entries to store in each file")
|
||||||
|
|
||||||
@ -188,21 +187,14 @@ func (c *Context) GetCaptureInfo() gopacket.CaptureInfo {
|
|||||||
func StartPassiveTapper(opts *TapOpts) (<-chan *OutputChannelItem, <-chan *OutboundLink) {
|
func StartPassiveTapper(opts *TapOpts) (<-chan *OutputChannelItem, <-chan *OutboundLink) {
|
||||||
hostMode = opts.HostMode
|
hostMode = opts.HostMode
|
||||||
|
|
||||||
var harWriter *HarWriter
|
harWriter := NewHarWriter(*HarOutputDir, *harEntriesPerFile)
|
||||||
if *dumpToHar {
|
|
||||||
harWriter = NewHarWriter(*HarOutputDir, *harEntriesPerFile)
|
|
||||||
}
|
|
||||||
outboundLinkWriter := NewOutboundLinkWriter()
|
outboundLinkWriter := NewOutboundLinkWriter()
|
||||||
|
|
||||||
go startPassiveTapper(harWriter, outboundLinkWriter)
|
go startPassiveTapper(harWriter, outboundLinkWriter)
|
||||||
|
|
||||||
if harWriter != nil {
|
|
||||||
return harWriter.OutChan, outboundLinkWriter.OutChan
|
return harWriter.OutChan, outboundLinkWriter.OutChan
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, outboundLinkWriter.OutChan
|
|
||||||
}
|
|
||||||
|
|
||||||
func startMemoryProfiler() {
|
func startMemoryProfiler() {
|
||||||
dirname := "/app/pprof"
|
dirname := "/app/pprof"
|
||||||
rlog.Info("Profiling is on, results will be written to %s", dirname)
|
rlog.Info("Profiling is on, results will be written to %s", dirname)
|
||||||
@ -323,10 +315,8 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if *dumpToHar {
|
|
||||||
harWriter.Start()
|
harWriter.Start()
|
||||||
defer harWriter.Stop()
|
defer harWriter.Stop()
|
||||||
}
|
|
||||||
defer outboundLinkWriter.Stop()
|
defer outboundLinkWriter.Stop()
|
||||||
|
|
||||||
var dec gopacket.Decoder
|
var dec gopacket.Decoder
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import 'components/style/variables.module'
|
@import 'src/variables.module'
|
||||||
|
|
||||||
.mizuApp
|
.mizuApp
|
||||||
background-color: $main-background-color
|
background-color: $main-background-color
|
||||||
|
@ -2,8 +2,8 @@ import React, {useEffect, useState} from 'react';
|
|||||||
import './App.sass';
|
import './App.sass';
|
||||||
import logo from './components/assets/Mizu-logo.svg';
|
import logo from './components/assets/Mizu-logo.svg';
|
||||||
import {Button, Snackbar} from "@material-ui/core";
|
import {Button, Snackbar} from "@material-ui/core";
|
||||||
import {HarPage} from "./components/HarPage";
|
import {TrafficPage} from "./components/TrafficPage";
|
||||||
import Tooltip from "./components/Tooltip";
|
import Tooltip from "./components/UI/Tooltip";
|
||||||
import {makeStyles} from "@material-ui/core/styles";
|
import {makeStyles} from "@material-ui/core/styles";
|
||||||
import MuiAlert from '@material-ui/lab/Alert';
|
import MuiAlert from '@material-ui/lab/Alert';
|
||||||
import Api from "./helpers/api";
|
import Api from "./helpers/api";
|
||||||
@ -38,6 +38,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onTLSDetected = (destAddress: string) => {
|
const onTLSDetected = (destAddress: string) => {
|
||||||
@ -116,7 +117,7 @@ const App = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<HarPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
||||||
<Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
|
<Snackbar open={showTLSWarning && !userDismissedTLSWarning}>
|
||||||
<MuiAlert elevation={6} variant="filled" onClose={() => setUserDismissedTLSWarning(true)} severity="warning">
|
<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.
|
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 {EntryItem} from "./EntryListItem/EntryListItem";
|
||||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||||
import styles from './style/HarEntriesList.module.sass';
|
import styles from './style/EntriesList.module.sass';
|
||||||
import spinner from './assets/spinner.svg';
|
import spinner from './assets/spinner.svg';
|
||||||
import ScrollableFeed from "react-scrollable-feed";
|
import ScrollableFeed from "react-scrollable-feed";
|
||||||
import {StatusType} from "./HarFilters";
|
import {StatusType} from "./Filters";
|
||||||
import Api from "../helpers/api";
|
import Api from "../helpers/api";
|
||||||
|
import down from "./assets/downImg.svg";
|
||||||
|
|
||||||
interface HarEntriesListProps {
|
interface HarEntriesListProps {
|
||||||
entries: any[];
|
entries: any[];
|
||||||
setEntries: (entries: any[]) => void;
|
setEntries: (entries: any[]) => void;
|
||||||
focusedEntryId: string;
|
focusedEntry: any;
|
||||||
setFocusedEntryId: (id: string) => void;
|
setFocusedEntry: (entry: any) => void;
|
||||||
connectionOpen: boolean;
|
connectionOpen: boolean;
|
||||||
noMoreDataTop: boolean;
|
noMoreDataTop: boolean;
|
||||||
setNoMoreDataTop: (flag: boolean) => void;
|
setNoMoreDataTop: (flag: boolean) => void;
|
||||||
@ -19,6 +20,9 @@ interface HarEntriesListProps {
|
|||||||
methodsFilter: Array<string>;
|
methodsFilter: Array<string>;
|
||||||
statusFilter: Array<string>;
|
statusFilter: Array<string>;
|
||||||
pathFilter: string
|
pathFilter: string
|
||||||
|
listEntryREF: any;
|
||||||
|
onScrollEvent: (isAtBottom:boolean) => void;
|
||||||
|
scrollableList: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FetchOperator {
|
enum FetchOperator {
|
||||||
@ -28,10 +32,11 @@ enum FetchOperator {
|
|||||||
|
|
||||||
const api = new Api();
|
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 [loadMoreTop, setLoadMoreTop] = useState(false);
|
||||||
const [isLoadingTop, setIsLoadingTop] = useState(false);
|
const [isLoadingTop, setIsLoadingTop] = useState(false);
|
||||||
|
const scrollableRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const list = document.getElementById('list').firstElementChild;
|
const list = document.getElementById('list').firstElementChild;
|
||||||
@ -106,20 +111,25 @@ export const HarEntriesList: React.FC<HarEntriesListProps> = ({entries, setEntri
|
|||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
<div id="list" className={styles.list}>
|
<div id="list" ref={listEntryREF} className={styles.list} >
|
||||||
{isLoadingTop && <div className={styles.spinnerContainer}>
|
{isLoadingTop && <div className={styles.spinnerContainer}>
|
||||||
<img alt="spinner" src={spinner} style={{height: 25}}/>
|
<img alt="spinner" src={spinner} style={{height: 25}}/>
|
||||||
</div>}
|
</div>}
|
||||||
<ScrollableFeed>
|
<ScrollableFeed ref={scrollableRef} onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}>
|
||||||
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
|
{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}
|
entry={entry}
|
||||||
setFocusedEntryId={setFocusedEntryId}
|
setFocusedEntry = {setFocusedEntry}
|
||||||
isSelected={focusedEntryId === entry.id}/>)}
|
isSelected={focusedEntry.id === entry.id}/>)}
|
||||||
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
|
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
|
||||||
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
|
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
|
||||||
</div>}
|
</div>}
|
||||||
</ScrollableFeed>
|
</ScrollableFeed>
|
||||||
|
<button type="button"
|
||||||
|
className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`}
|
||||||
|
onClick={(_) => scrollableRef.current.scrollToBottom()}>
|
||||||
|
<img alt="down" src={down} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{entries?.length > 0 && <div className={styles.footer}>
|
{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
|
.title
|
||||||
display: flex
|
display: flex
|
||||||
@ -92,3 +92,6 @@
|
|||||||
tr td:first-child
|
tr td:first-child
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
padding-right: .5rem
|
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 > 0 ? <tr className={styles.dataValue}><td><b>Latency</b>:</td><td>{rule.Latency}</td></tr> : ''}
|
||||||
|
{rule.Method && <tr className={styles.dataValue}><td><b>Method:</b></td> <td>{rule.Method}</td></tr>}
|
||||||
|
{rule.Path && <tr className={styles.dataValue}><td><b>Path:</b></td> <td>{rule.Path}</td></tr>}
|
||||||
|
{rule.Service && <tr className={styles.dataValue}><td><b>Service:</b></td> <td>{service}</td></tr>}
|
||||||
|
{rule.Type && <tr className={styles.dataValue}><td><b>Type:</b></td> <td>{rule.Type}</td></tr>}
|
||||||
|
{rule.Value && <tr className={styles.dataValue}><td><b>Value:</b></td> <td>{rule.Value}</td></tr>}
|
||||||
|
</>
|
||||||
|
</HAREntryPolicySectionContainer>)})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</SectionContainer>
|
||||||
|
</> : <span className={styles.noRules}>No rules could be applied to this request.</span>}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
@ -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,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {singleEntryToHAR} from "../../../helpers/utils";
|
||||||
|
import StatusCode from "../../UI/StatusCode";
|
||||||
|
import {EndpointPath} from "../../UI/EndpointPath";
|
||||||
|
|
||||||
|
const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
|
||||||
|
|
||||||
|
export const RestEntryDetailsTitle: React.FC<any> = ({entryData}) => {
|
||||||
|
|
||||||
|
const har = singleEntryToHAR(entryData);
|
||||||
|
const {log: {entries}} = har;
|
||||||
|
const {response, request, timings: {receive}} = entries[0].entry;
|
||||||
|
const {status, statusText, bodySize} = response;
|
||||||
|
|
||||||
|
return har && <>
|
||||||
|
{status && <div style={{marginRight: 8}}>
|
||||||
|
<StatusCode statusCode={status}/>
|
||||||
|
</div>}
|
||||||
|
<div style={{flexGrow: 1, overflow: 'hidden'}}>
|
||||||
|
<EndpointPath method={request?.method} path={request?.url}/>
|
||||||
|
</div>
|
||||||
|
<div style={{margin: "0 18px", opacity: 0.5}}>{formatSize(bodySize)}</div>
|
||||||
|
<div style={{marginRight: 18, opacity: 0.5}}>{status} {statusText}</div>
|
||||||
|
<div style={{marginRight: 18, opacity: 0.5}}>{Math.round(receive)}ms</div>
|
||||||
|
</>
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
@import 'variables.module'
|
@import 'src/variables.module'
|
||||||
|
|
||||||
.row
|
.row
|
||||||
display: flex
|
display: flex
|
||||||
@ -24,12 +24,40 @@
|
|||||||
margin-right: 3px
|
margin-right: 3px
|
||||||
|
|
||||||
.ruleSuccessRow
|
.ruleSuccessRow
|
||||||
border: 1px $success-color solid
|
background: #E8FFF1
|
||||||
border-left: 5px $success-color solid
|
|
||||||
|
.ruleSuccessRowSelected
|
||||||
|
border: 1px #6FCF97 solid
|
||||||
|
border-left: 5px #6FCF97 solid
|
||||||
|
margin-left: 10px
|
||||||
|
margin-right: 3px
|
||||||
|
|
||||||
.ruleFailureRow
|
.ruleFailureRow
|
||||||
|
background: #FFE9EF
|
||||||
|
|
||||||
|
.ruleFailureRowSelected
|
||||||
border: 1px $failure-color solid
|
border: 1px $failure-color solid
|
||||||
border-left: 5px $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
|
.service
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis
|
||||||
@ -45,10 +73,11 @@
|
|||||||
.timestamp
|
.timestamp
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
color: $secondary-font-color
|
color: $secondary-font-color
|
||||||
padding-left: 12px
|
|
||||||
flex-shrink: 0
|
flex-shrink: 0
|
||||||
width: 145px
|
width: 145px
|
||||||
text-align: left
|
text-align: left
|
||||||
|
border-left: 1px solid $data-background-color
|
||||||
|
padding: 6px 0 6px 12px
|
||||||
|
|
||||||
.endpointServiceContainer
|
.endpointServiceContainer
|
||||||
display: flex
|
display: flex
|
||||||
@ -60,6 +89,12 @@
|
|||||||
|
|
||||||
.directionContainer
|
.directionContainer
|
||||||
display: flex
|
display: flex
|
||||||
border-right: 1px solid $data-background-color
|
padding: 4px 12px 4px 4px
|
||||||
padding: 4px
|
|
||||||
padding-right: 12px
|
.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 && !rule ? styles.rowSelected : additionalRulesProperties}`}
|
||||||
|
onClick={() => setFocusedEntry(entry)}>
|
||||||
|
{icon && <div style={{width: 80}}>{<img className={styles.icon} alt="icon" src={icon}/>}</div>}
|
||||||
|
{content}
|
||||||
|
<div className={styles.timestamp}>{new Date(+entry.timestamp)?.toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
};
|
||||||
|
|
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 React from "react";
|
||||||
import styles from './style/HarFilters.module.sass';
|
import styles from './style/Filters.module.sass';
|
||||||
import {HARFilterSelect} from "./HARFilterSelect";
|
import {FilterSelect} from "./UI/FilterSelect";
|
||||||
import {TextField} from "@material-ui/core";
|
import {TextField} from "@material-ui/core";
|
||||||
import {ALL_KEY} from "./Select";
|
import {ALL_KEY} from "./UI/Select";
|
||||||
|
|
||||||
interface HarFiltersProps {
|
interface HarFiltersProps {
|
||||||
methodsFilter: Array<string>;
|
methodsFilter: Array<string>;
|
||||||
@ -13,7 +13,7 @@ interface HarFiltersProps {
|
|||||||
setPathFilter: (val: string) => void;
|
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}>
|
return <div className={styles.container}>
|
||||||
<MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
|
<MethodFilter methodsFilter={methodsFilter} setMethodsFilter={setMethodsFilter}/>
|
||||||
@ -59,7 +59,7 @@ const MethodFilter: React.FC<MethodFilterProps> = ({methodsFilter, setMethodsFil
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <FilterContainer>
|
return <FilterContainer>
|
||||||
<HARFilterSelect
|
<FilterSelect
|
||||||
items={Object.values(HTTPMethod)}
|
items={Object.values(HTTPMethod)}
|
||||||
allowMultiple={true}
|
allowMultiple={true}
|
||||||
value={methodsFilter}
|
value={methodsFilter}
|
||||||
@ -91,7 +91,7 @@ const StatusTypesFilter: React.FC<StatusTypesFilterProps> = ({statusFilter, setS
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <FilterContainer>
|
return <FilterContainer>
|
||||||
<HARFilterSelect
|
<FilterSelect
|
||||||
items={Object.values(StatusType)}
|
items={Object.values(StatusType)}
|
||||||
allowMultiple={true}
|
allowMultiple={true}
|
||||||
value={statusFilter}
|
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>
|
|
||||||
};
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user