mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-05-05 16:57:48 +00:00
Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fbb00f8f0 | ||
|
|
4e50e17d81 | ||
|
|
da7d3590fc | ||
|
|
991eb2ab16 | ||
|
|
f0db3b81a8 | ||
|
|
9df1812d8e | ||
|
|
4f6da91d74 | ||
|
|
e2e69a3dc4 | ||
|
|
b6db64d868 | ||
|
|
160ae77145 | ||
|
|
2944493e2d | ||
|
|
3a9c113f77 | ||
|
|
47f2e69b7e | ||
|
|
6240d85377 | ||
|
|
29ba963c48 | ||
|
|
0473181f0a | ||
|
|
145e7cda01 | ||
|
|
b7ff076571 | ||
|
|
3aafbd7e1c | ||
|
|
58e9363fda | ||
|
|
6a85ab53eb | ||
|
|
212e4687d8 | ||
|
|
167b17dfd2 | ||
|
|
9d179c7227 | ||
|
|
147e812edb | ||
|
|
91196bb306 | ||
|
|
26834a6e04 | ||
|
|
754f385865 | ||
|
|
b30b62ef77 | ||
|
|
26788bb3a6 | ||
|
|
2706cd4d50 | ||
|
|
b40104b74c | ||
|
|
d308468f1b | ||
|
|
10e695d7a0 | ||
|
|
837e35255b | ||
|
|
56e801a582 | ||
|
|
04c0f8cbcd | ||
|
|
da846da334 | ||
|
|
ba6b5c868c | ||
|
|
9d378ed75b | ||
|
|
70982c2844 | ||
|
|
61f24320b8 | ||
|
|
eb4a541376 | ||
|
|
77710cc411 | ||
|
|
256006ca3e | ||
|
|
213528c619 | ||
|
|
8b8c4609ce | ||
|
|
14b616a856 | ||
|
|
82d603c0fd | ||
|
|
8b47dba05d | ||
|
|
f1a2ee7fb4 | ||
|
|
15021daa2e | ||
|
|
f83e565cd4 | ||
|
|
8636a4731e | ||
|
|
aa3510e936 | ||
|
|
fd48cc6d87 | ||
|
|
111d000c12 | ||
|
|
9c98a4c2b1 | ||
|
|
d2d4ed5aee | ||
|
|
30fce5d765 | ||
|
|
90040798b8 | ||
|
|
9eecddddd5 | ||
|
|
cc49e815d6 | ||
|
|
c26eb843e3 | ||
|
|
26efaa101d | ||
|
|
352567c56e | ||
|
|
51fc3307be | ||
|
|
cdf1c39a52 | ||
|
|
db1f7d34cf | ||
|
|
9212c195b4 | ||
|
|
7b333556d0 | ||
|
|
8ba96acf05 | ||
|
|
f164e54fee | ||
|
|
649b733ba1 | ||
|
|
e8ea93cb64 | ||
|
|
5e5d5de91a | ||
|
|
5dacd41ba9 | ||
|
|
7f837fe947 | ||
|
|
02bd7883cb | ||
|
|
1841798646 | ||
|
|
749bee6d55 | ||
|
|
043b845c06 | ||
|
|
8c7f82c6f0 | ||
|
|
ec4fa2ee4f | ||
|
|
b50eced489 | ||
|
|
5392475486 | ||
|
|
65bb262652 | ||
|
|
842d95c836 | ||
|
|
9fa9b67328 | ||
|
|
6337b75f0e | ||
|
|
b9d2e671c7 | ||
|
|
0840642c98 | ||
|
|
d5b01347df | ||
|
|
7dca1ad889 | ||
|
|
616eccb2cf | ||
|
|
30f07479cb | ||
|
|
7f880417e9 | ||
|
|
6b52458642 | ||
|
|
858a64687d | ||
|
|
819ccf54cd | ||
|
|
7cc077c8a0 | ||
|
|
fae5f22d25 | ||
|
|
eba7a3b476 | ||
|
|
cf231538f4 | ||
|
|
073b0b72d3 | ||
|
|
c8705822b3 | ||
|
|
d4436d9f15 | ||
|
|
4e0ff74944 | ||
|
|
366c1d0c6c | ||
|
|
680ea71958 | ||
|
|
17fa163ee3 | ||
|
|
3644fdb533 | ||
|
|
ab7c4e72c6 | ||
|
|
e25e7925b6 | ||
|
|
80237c8090 | ||
|
|
5fb5dbbbf5 | ||
|
|
b3fe448ff1 | ||
|
|
101a54e8da | ||
|
|
3308cab826 | ||
|
|
5fdd8288f4 | ||
|
|
4cb32b40e6 | ||
|
|
a310953f05 | ||
|
|
a9e92b60f5 | ||
|
|
35e40cd230 | ||
|
|
2575ad722a | ||
|
|
afd5757315 | ||
|
|
dba8b1f215 | ||
|
|
afa81c7ec2 | ||
|
|
e84c7d3310 | ||
|
|
7d0a90cb78 | ||
|
|
6dd0ef1268 | ||
|
|
83cfaed1a3 | ||
|
|
41cb9ee12e | ||
|
|
667f0dc87d | ||
|
|
a34c2fc0dc | ||
|
|
7a31263e4a | ||
|
|
7f9fd82c0e | ||
|
|
a37d1f4aeb | ||
|
|
acdbdedd5d | ||
|
|
a9b5eba9d4 | ||
|
|
80201224c6 | ||
|
|
e6e7d8d58b | ||
|
|
bf27e94003 | ||
|
|
2ae0a2400d | ||
|
|
db1f4458c5 | ||
|
|
24f79922e9 | ||
|
|
5d5c11c37c | ||
|
|
b4f3b2c540 | ||
|
|
a427534605 | ||
|
|
1d6ca9d392 | ||
|
|
f74a52d4dc | ||
|
|
6d2e9af5d7 | ||
|
|
e4ff4a0745 | ||
|
|
f9677dbaa1 | ||
|
|
0afab6c068 | ||
|
|
1d1b62ec4f | ||
|
|
e2db5087b8 | ||
|
|
241477fb5c | ||
|
|
c8e5886a96 | ||
|
|
8a8cf4aa77 | ||
|
|
7b73004e85 | ||
|
|
56dc6843e0 | ||
|
|
0409eb239d | ||
|
|
cbe04af801 | ||
|
|
59dec1a547 | ||
|
|
c4afeee5b3 | ||
|
|
8c9b8d3217 | ||
|
|
d705ae3eb6 | ||
|
|
c3995009ee | ||
|
|
c53b2148d1 | ||
|
|
ca897dd3c7 | ||
|
|
4406919565 | ||
|
|
6e9fe2986e | ||
|
|
413fb5b3f5 | ||
|
|
e36c146979 | ||
|
|
603240fedb | ||
|
|
1cf9c29ef0 | ||
|
|
e61871a68e | ||
|
|
02e02718d2 | ||
|
|
1a0517f46b | ||
|
|
efbb432df9 | ||
|
|
dfea8884d4 | ||
|
|
d34dacbbe2 | ||
|
|
0595df8b87 | ||
|
|
ebbe6458a8 | ||
|
|
7f2021c312 | ||
|
|
824945141a | ||
|
|
0244f12167 | ||
|
|
60533a9591 | ||
|
|
90f0f603c7 | ||
|
|
683d199774 | ||
|
|
fa632b49a7 | ||
|
|
04579eb03c | ||
|
|
dea223bfe1 | ||
|
|
06c8056443 | ||
|
|
d18f1f8316 | ||
|
|
f9202900ee | ||
|
|
9e34662511 | ||
|
|
1e726e381b | ||
|
|
69a9deab4b | ||
|
|
f9396e01ca | ||
|
|
2d5b170406 | ||
|
|
dc59fb6931 | ||
|
|
793bb97e51 | ||
|
|
ceb8d714e3 | ||
|
|
8d8310ee02 | ||
|
|
0824524d62 | ||
|
|
71eff5ea04 | ||
|
|
50e404f51e | ||
|
|
ffa34039b1 | ||
|
|
d888706e1e | ||
|
|
1ef17542dd | ||
|
|
0566f63d72 | ||
|
|
6d49339e29 | ||
|
|
58f0de4d4e | ||
|
|
f175480f65 | ||
|
|
6dd2bf705b | ||
|
|
f64ee23c74 | ||
|
|
803681a239 | ||
|
|
a2150b4a78 | ||
|
|
ac358be877 | ||
|
|
2996c1a4bc | ||
|
|
e42c4f8648 | ||
|
|
7d5ed601df | ||
|
|
30651c0f75 | ||
|
|
594f1b973a | ||
|
|
77ced2a46d | ||
|
|
b195ed9905 | ||
|
|
59ef3a4244 | ||
|
|
360a4ea562 | ||
|
|
e883358cd6 | ||
|
|
efb1a0b58b | ||
|
|
eb67f76e2b | ||
|
|
5b9c134ab2 | ||
|
|
8db12a4b1a | ||
|
|
7fb85df3ac | ||
|
|
edc3d04d59 | ||
|
|
679bf35ce3 | ||
|
|
17e1ccf9ef | ||
|
|
80e97e7f7e | ||
|
|
5c86e20c92 | ||
|
|
4a030c02f7 | ||
|
|
ea5054866d | ||
|
|
a11e8f730e | ||
|
|
1e66ebd8b3 | ||
|
|
3be0c9ecd9 | ||
|
|
ef8314b554 | ||
|
|
b57cb0e615 | ||
|
|
1d24188a02 | ||
|
|
9d9f64098e | ||
|
|
379af59f07 | ||
|
|
fbf3d1729e | ||
|
|
eb02ecda20 | ||
|
|
ef9afe31a4 | ||
|
|
dc62195a8f | ||
|
|
38b58dba69 | ||
|
|
765feafbcc | ||
|
|
0a622b5017 | ||
|
|
a0a9d74662 | ||
|
|
5e7ef0fbb9 | ||
|
|
1d6c176c7f | ||
|
|
3b9f5ee32f | ||
|
|
1619df2d5e | ||
|
|
21b91ea6e4 | ||
|
|
79a8ee37f9 | ||
|
|
6e14fa95a1 | ||
|
|
cd34892943 | ||
|
|
62b17c1822 | ||
|
|
cef0e01cf6 | ||
|
|
6e279bfca5 | ||
|
|
4e7bc05ecf | ||
|
|
8886590ea2 | ||
|
|
8400e9e903 | ||
|
|
eaa120cad8 | ||
|
|
35f9e16e7c | ||
|
|
b29b15cf6c | ||
|
|
8fab07494c | ||
|
|
79816ae337 | ||
|
|
728b5b5d1c | ||
|
|
c4048e5c8e | ||
|
|
bd71e9a122 | ||
|
|
4a053734d9 | ||
|
|
5b439d8316 | ||
|
|
400774555a | ||
|
|
7cd6d123d1 | ||
|
|
90c9d8b0d0 | ||
|
|
96f47116f0 | ||
|
|
78456d7987 | ||
|
|
115692dbfc | ||
|
|
f809ed5eeb | ||
|
|
603206f2cb | ||
|
|
6aa38f071f | ||
|
|
d684dee7a4 | ||
|
|
e3049fb5a5 | ||
|
|
ec18d96b45 | ||
|
|
f03df50def | ||
|
|
b2f091746a | ||
|
|
60431b2836 | ||
|
|
77e01da6e8 | ||
|
|
18c1473bd9 | ||
|
|
1638e1be3b | ||
|
|
01a1cd8434 | ||
|
|
466214c4b5 | ||
|
|
de1295e29d | ||
|
|
7de3338752 | ||
|
|
3db6d5a5ea | ||
|
|
65ba0952b4 | ||
|
|
97798cb5b7 | ||
|
|
b1df4b69ae | ||
|
|
b3dcff2cd5 | ||
|
|
09702697ad | ||
|
|
7abf8b83e3 | ||
|
|
dca636b0fd | ||
|
|
12d873d344 | ||
|
|
672accba0c | ||
|
|
566eab3527 | ||
|
|
0f52533cd8 | ||
|
|
eef58496b5 | ||
|
|
1137f9386b | ||
|
|
93714ab902 | ||
|
|
fc03ba2eda | ||
|
|
3662fbcdf6 | ||
|
|
b762e3c194 | ||
|
|
35ef211477 | ||
|
|
feb386ba1f | ||
|
|
ed4a818a53 | ||
|
|
fa733025dc | ||
|
|
5f603e3291 | ||
|
|
b84c698c1a | ||
|
|
9b72cc7aa6 | ||
|
|
c59aadb221 | ||
|
|
6aaee4b519 | ||
|
|
6f47ad862e | ||
|
|
f18f3da99c | ||
|
|
3e32c889d9 | ||
|
|
d3c023b3ba | ||
|
|
f604a3a35d | ||
|
|
5d205b5082 | ||
|
|
756f5f5720 | ||
|
|
9a1c17cc61 | ||
|
|
64253cd919 | ||
|
|
accad7c058 | ||
|
|
485bc7fd2b | ||
|
|
bc3efc6d4c | ||
|
|
135b1a5e1e | ||
|
|
5f2a4deb19 | ||
|
|
91f290987e | ||
|
|
2f3215b71a | ||
|
|
2e87a01346 | ||
|
|
453003bf14 | ||
|
|
80ca377668 | ||
|
|
d21297bc9c |
@@ -2,7 +2,7 @@
|
|||||||
.dockerignore
|
.dockerignore
|
||||||
.editorconfig
|
.editorconfig
|
||||||
.gitignore
|
.gitignore
|
||||||
.env.*
|
**/.env*
|
||||||
Dockerfile
|
Dockerfile
|
||||||
Makefile
|
Makefile
|
||||||
LICENSE
|
LICENSE
|
||||||
|
|||||||
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. <kbd>CTRL</kbd>+<kbd>C</kbd> 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. macOS]
|
||||||
|
- Web Browser: [e.g. Google 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
|
||||||
46
.github/workflows/pr_validation.yml
vendored
Normal file
46
.github/workflows/pr_validation.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: PR validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-pr-validation-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-cli:
|
||||||
|
name: Build CLI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build CLI
|
||||||
|
run: make cli
|
||||||
|
|
||||||
|
build-agent:
|
||||||
|
name: Build Agent
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '1.16'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get install libpcap-dev
|
||||||
|
|
||||||
|
- name: Build Agent
|
||||||
|
run: make agent
|
||||||
39
.github/workflows/publish-docker.yml
vendored
39
.github/workflows/publish-docker.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: publish-docker
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'develop'
|
|
||||||
- 'main'
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Get base image name
|
|
||||||
shell: bash
|
|
||||||
run: echo "##[set-output name=image;]$(echo gcr.io/up9-docker-hub/mizu/${GITHUB_REF#refs/heads/})"
|
|
||||||
id: base_image_step
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: crazy-max/ghaction-docker-meta@v2
|
|
||||||
with:
|
|
||||||
images: ${{ steps.base_image_step.outputs.image }}
|
|
||||||
tags: |
|
|
||||||
type=sha
|
|
||||||
type=raw,${{ github.sha }}
|
|
||||||
type=raw,latest
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
|
||||||
registry: gcr.io
|
|
||||||
username: _json_key
|
|
||||||
password: ${{ secrets.GCR_JSON_KEY }}
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
name: public-cli
|
name: publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- 'develop'
|
||||||
- main
|
- 'main'
|
||||||
- my-temp-release-check
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-publish-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker:
|
docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set up Go 1.16
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '1.16'
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Set up Cloud SDK
|
- name: Set up Cloud SDK
|
||||||
@@ -28,14 +37,48 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
releaseType: ${{ steps.condval.outputs.value }}
|
releaseType: ${{ steps.condval.outputs.value }}
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Get base image name
|
- name: Get version parameters
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "##[set-output name=build_timestamp;]$(echo $(date +%s))"
|
echo "##[set-output name=build_timestamp;]$(echo $(date +%s))"
|
||||||
echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||||
id: version_parameters
|
id: version_parameters
|
||||||
|
- name: Get base image name
|
||||||
|
shell: bash
|
||||||
|
run: echo "##[set-output name=image;]$(echo gcr.io/up9-docker-hub/mizu/${GITHUB_REF#refs/heads/})"
|
||||||
|
id: base_image_step
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: crazy-max/ghaction-docker-meta@v2
|
||||||
|
with:
|
||||||
|
images: ${{ steps.base_image_step.outputs.image }}
|
||||||
|
tags: |
|
||||||
|
type=sha
|
||||||
|
type=raw,${{ github.sha }}
|
||||||
|
type=raw,${{ steps.versioning.outputs.version }}
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: gcr.io
|
||||||
|
username: _json_key
|
||||||
|
password: ${{ secrets.GCR_JSON_KEY }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
SEM_VER=${{ steps.versioning.outputs.version }}
|
||||||
|
BUILD_TIMESTAMP=${{ steps.version_parameters.outputs.build_timestamp }}
|
||||||
|
GIT_BRANCH=${{ steps.version_parameters.outputs.branch }}
|
||||||
|
COMMIT_HASH=${{ github.sha }}
|
||||||
- name: Build and Push CLI
|
- name: Build and Push CLI
|
||||||
run: make push-cli SEM_VER='${{ steps.versioning.outputs.version }}' BUILD_TIMESTAMP='${{ steps.version_parameters.outputs.build_timestamp }}'
|
run: make push-cli SEM_VER='${{ steps.versioning.outputs.version }}' BUILD_TIMESTAMP='${{ steps.version_parameters.outputs.build_timestamp }}'
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
echo '${{ steps.versioning.outputs.version }}' >> cli/bin/version.txt
|
||||||
- name: publish
|
- name: publish
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
56
.github/workflows/tests_validation.yml
vendored
Normal file
56
.github/workflows/tests_validation.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: tests validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'develop'
|
||||||
|
- 'main'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mizu-tests-validation-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
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
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -19,3 +19,13 @@ build
|
|||||||
|
|
||||||
# Mac OS
|
# Mac OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore the scripts that are created for development
|
||||||
|
*dev.*
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# pprof
|
||||||
|
pprof/*
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -2,8 +2,10 @@ FROM node:14-slim AS site-build
|
|||||||
|
|
||||||
WORKDIR /app/ui-build
|
WORKDIR /app/ui-build
|
||||||
|
|
||||||
COPY ui .
|
COPY ui/package.json .
|
||||||
|
COPY ui/package-lock.json .
|
||||||
RUN npm i
|
RUN npm i
|
||||||
|
COPY ui .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
@@ -11,22 +13,36 @@ FROM golang:1.16-alpine AS builder
|
|||||||
# Set necessary environment variables needed for our image.
|
# Set necessary environment variables needed for our image.
|
||||||
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
|
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
|
||||||
|
|
||||||
RUN apk add libpcap-dev gcc g++ make
|
RUN apk add libpcap-dev gcc g++ make bash
|
||||||
|
|
||||||
# Move to api working directory (/api-build).
|
# Move to agent working directory (/agent-build).
|
||||||
WORKDIR /app/api-build
|
WORKDIR /app/agent-build
|
||||||
|
|
||||||
COPY api/go.mod api/go.sum ./
|
COPY agent/go.mod agent/go.sum ./
|
||||||
COPY shared/go.mod shared/go.mod ../shared/
|
COPY shared/go.mod shared/go.mod ../shared/
|
||||||
|
COPY tap/go.mod tap/go.mod ../tap/
|
||||||
|
COPY tap/api/go.* ../tap/api/
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
# cheap trick to make the build faster (As long as go.mod wasn't changes)
|
# cheap trick to make the build faster (As long as go.mod wasn't changes)
|
||||||
RUN go list -f '{{.Path}}@{{.Version}}' -m all | sed 1d | grep -e 'go-cache' -e 'sqlite' | xargs go get
|
RUN go list -f '{{.Path}}@{{.Version}}' -m all | sed 1d | grep -e 'go-cache' -e 'sqlite' | xargs go get
|
||||||
|
|
||||||
# Copy and build api code
|
ARG COMMIT_HASH
|
||||||
COPY shared ../shared
|
ARG GIT_BRANCH
|
||||||
COPY api .
|
ARG BUILD_TIMESTAMP
|
||||||
RUN go build -ldflags="-s -w" -o mizuagent .
|
ARG SEM_VER
|
||||||
|
|
||||||
|
# Copy and build agent code
|
||||||
|
COPY shared ../shared
|
||||||
|
COPY tap ../tap
|
||||||
|
COPY agent .
|
||||||
|
RUN go build -ldflags="-s -w \
|
||||||
|
-X 'mizuserver/pkg/version.GitCommitHash=${COMMIT_HASH}' \
|
||||||
|
-X 'mizuserver/pkg/version.Branch=${GIT_BRANCH}' \
|
||||||
|
-X 'mizuserver/pkg/version.BuildTimestamp=${BUILD_TIMESTAMP}' \
|
||||||
|
-X 'mizuserver/pkg/version.SemVer=${SEM_VER}'" -o mizuagent .
|
||||||
|
|
||||||
|
COPY devops/build_extensions.sh ..
|
||||||
|
RUN cd .. && /bin/bash build_extensions.sh
|
||||||
|
|
||||||
FROM alpine:3.13.5
|
FROM alpine:3.13.5
|
||||||
|
|
||||||
@@ -34,10 +50,12 @@ RUN apk add bash libpcap-dev tcpdump
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy binary and config files from /build to root folder of scratch container.
|
# Copy binary and config files from /build to root folder of scratch container.
|
||||||
COPY --from=builder ["/app/api-build/mizuagent", "."]
|
COPY --from=builder ["/app/agent-build/mizuagent", "."]
|
||||||
|
COPY --from=builder ["/app/agent/build/extensions", "extensions"]
|
||||||
COPY --from=site-build ["/app/ui-build/build", "site"]
|
COPY --from=site-build ["/app/ui-build/build", "site"]
|
||||||
|
|
||||||
COPY api/start.sh .
|
# gin-gonic runs in debug mode without this
|
||||||
|
ENV GIN_MODE=release
|
||||||
|
|
||||||
# this script runs both apiserver and passivetapper and exits either if one of them exits, preventing a scenario where the container runs without one process
|
# this script runs both apiserver and passivetapper and exits either if one of them exits, preventing a scenario where the container runs without one process
|
||||||
ENTRYPOINT "/app/mizuagent"
|
ENTRYPOINT "/app/mizuagent"
|
||||||
|
|||||||
64
Makefile
64
Makefile
@@ -8,7 +8,7 @@ SHELL=/bin/bash
|
|||||||
# HELP
|
# HELP
|
||||||
# This will output the help for each task
|
# This will output the help for each task
|
||||||
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||||
.PHONY: help ui api cli tap docker
|
.PHONY: help ui agent cli tap docker
|
||||||
|
|
||||||
help: ## This help.
|
help: ## This help.
|
||||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
@@ -19,34 +19,38 @@ help: ## This help.
|
|||||||
TS_SUFFIX="$(shell date '+%s')"
|
TS_SUFFIX="$(shell date '+%s')"
|
||||||
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
||||||
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
||||||
|
export SEM_VER?=0.0.0
|
||||||
|
|
||||||
ui: ## build UI
|
ui: ## Build UI.
|
||||||
@(cd ui; npm i ; npm run build; )
|
@(cd ui; npm i ; npm run build; )
|
||||||
@ls -l ui/build
|
@ls -l ui/build
|
||||||
|
|
||||||
cli: # build CLI
|
cli: ## Build CLI.
|
||||||
@echo "building cli"; cd cli && $(MAKE) build
|
@echo "building cli"; cd cli && $(MAKE) build
|
||||||
|
|
||||||
api: ## build API server
|
build-cli-ci: ## Build CLI for CI.
|
||||||
@(echo "building API server .." )
|
@echo "building cli for ci"; cd cli && $(MAKE) build GIT_BRANCH=ci SUFFIX=ci
|
||||||
@(cd api; go build -o build/apiserver main.go)
|
|
||||||
@ls -l api/build
|
|
||||||
|
|
||||||
#tap: ## build tap binary
|
agent: ## Build agent.
|
||||||
# @(cd tap; go build -o build/tap ./src)
|
@(echo "building mizu agent .." )
|
||||||
# @ls -l tap/build
|
@(cd agent; go build -o build/mizuagent main.go)
|
||||||
|
${MAKE} extensions
|
||||||
|
@ls -l agent/build
|
||||||
|
|
||||||
docker: ## build Docker image
|
docker: ## Build and publish agent docker image.
|
||||||
@(echo "building docker image" )
|
$(MAKE) push-docker
|
||||||
./build-push-featurebranch.sh
|
|
||||||
|
|
||||||
push: push-docker push-cli ## build and publish Mizu docker image & CLI
|
push: push-docker push-cli ## Build and publish agent docker image & CLI.
|
||||||
|
|
||||||
push-docker:
|
push-docker: ## Build and publish agent docker image.
|
||||||
@echo "publishing Docker image .. "
|
@echo "publishing Docker image .. "
|
||||||
./build-push-featurebranch.sh
|
devops/build-push-featurebranch.sh
|
||||||
|
|
||||||
push-cli:
|
build-docker-ci: ## Build agent docker image for CI.
|
||||||
|
@echo "building docker image for ci"
|
||||||
|
devops/build-agent-ci.sh
|
||||||
|
|
||||||
|
push-cli: ## Build and publish CLI.
|
||||||
@echo "publishing CLI .. "
|
@echo "publishing CLI .. "
|
||||||
@cd cli; $(MAKE) build-all
|
@cd cli; $(MAKE) build-all
|
||||||
@echo "publishing file ${OUTPUT_FILE} .."
|
@echo "publishing file ${OUTPUT_FILE} .."
|
||||||
@@ -54,18 +58,28 @@ push-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-api clean-cli clean-docker ## Clean all build artifacts
|
clean-ui: ## Clean UI.
|
||||||
|
|
||||||
clean-ui:
|
|
||||||
@(rm -rf ui/build ; echo "UI cleanup done" )
|
@(rm -rf ui/build ; echo "UI cleanup done" )
|
||||||
|
|
||||||
clean-api:
|
clean-agent: ## Clean agent.
|
||||||
@(rm -rf api/build ; echo "api cleanup done" )
|
@(rm -rf agent/build ; echo "agent cleanup done" )
|
||||||
|
|
||||||
clean-cli:
|
clean-cli: ## Clean CLI.
|
||||||
@(cd cli; make clean ; echo "CLI cleanup done" )
|
@(cd cli; make clean ; echo "CLI cleanup done" )
|
||||||
|
|
||||||
clean-docker:
|
clean-docker:
|
||||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||||
|
|
||||||
|
extensions:
|
||||||
|
devops/build_extensions.sh
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
188
README.md
188
README.md
@@ -1,24 +1,184 @@
|
|||||||
# 水 mizu
|

|
||||||
standalone web app traffic viewer for Kubernetes
|
|
||||||
|
# The API Traffic Viewer for Kubernetes
|
||||||
|
|
||||||
|
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
|
||||||
|
|
||||||
|
Think TCPDump and Chrome Dev Tools combined.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Simple and powerful CLI
|
||||||
|
- Real-time view of all HTTP requests, REST and gRPC API calls
|
||||||
|
- No installation or code instrumentation
|
||||||
|
- Works completely on premises
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
A Kubernetes server version of 1.16.0 or higher is required.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
Download `mizu` for your platform and operating system
|
Download Mizu for your platform and operating system
|
||||||
|
|
||||||
### Latest stable release
|
### Latest Stable Release
|
||||||
|
|
||||||
* for MacOS - `curl -o mizu https://github.com/up9inc/mizu/releases/download/latest/mizu_darwin_amd64 && chmod 755 mizu`
|
* for MacOS - Intel
|
||||||
* for Linux - `curl -o mizu https://github.com/up9inc/mizu/releases/download/latest/mizu_linux_amd64 && chmod 755 mizu`
|
```
|
||||||
|
curl -Lo mizu \
|
||||||
|
https://github.com/up9inc/mizu/releases/latest/download/mizu_darwin_amd64 \
|
||||||
|
&& chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
* for Linux - Intel 64bit
|
||||||
|
```
|
||||||
|
curl -Lo mizu \
|
||||||
|
https://github.com/up9inc/mizu/releases/latest/download/mizu_linux_amd64 \
|
||||||
|
&& chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
### Development (unstable) build
|
SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/releases) page
|
||||||
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page.
|
|
||||||
|
|
||||||
## How to run
|
### Development (unstable) Build
|
||||||
|
Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page
|
||||||
|
|
||||||
1. Find pod you'd like to tap to in your Kubernetes cluster
|
## Kubeconfig & Permissions
|
||||||
2. Run `mizu PODNAME` or `mizu REGEX`
|
While `mizu`most often works out of the box, you can influence its behavior:
|
||||||
3. Open browser on `http://localhost:8899` as instructed ..
|
|
||||||
4. Watch the WebAPI traffic flowing ..
|
1. [OPTIONAL] Set `KUBECONFIG` environment variable to your Kubernetes configuration. If this is not set, Mizu assumes that configuration is at `${HOME}/.kube/config`
|
||||||
|
2. `mizu` assumes user running the command has permissions to create resources (such as pods, services, namespaces) on your Kubernetes cluster (no worries - `mizu` resources are cleaned up upon termination)
|
||||||
|
|
||||||
|
For detailed list of k8s permissions see [PERMISSIONS](docs/PERMISSIONS.md) document
|
||||||
|
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
1. Find pods you'd like to tap to in your Kubernetes cluster
|
||||||
|
2. Run `mizu tap` or `mizu tap PODNAME`
|
||||||
|
3. Open browser on `http://localhost:8899/mizu` **or** as instructed in the CLI
|
||||||
|
4. Watch the API traffic flowing
|
||||||
|
5. Type ^C to stop
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
TBD
|
|
||||||
|
Run `mizu help` for usage options
|
||||||
|
|
||||||
|
To tap all pods in current namespace -
|
||||||
|
```
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
||||||
|
catalogue-5f4cb7cf5-7zrmn 2/2 Running 0 20m
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 20m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap
|
||||||
|
+carts-66c77f5fbb-fq65r
|
||||||
|
+catalogue-5f4cb7cf5-7zrmn
|
||||||
|
+front-end-649fc5fd6-kqbtn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To tap specific pod -
|
||||||
|
```bash
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 7m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap front-end-649fc5fd6-kqbtn
|
||||||
|
+front-end-649fc5fd6-kqbtn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
To tap multiple pods using regex -
|
||||||
|
```bash
|
||||||
|
$ kubectl get pods
|
||||||
|
NAME READY STATUS RESTARTS AGE
|
||||||
|
carts-66c77f5fbb-fq65r 2/2 Running 0 20m
|
||||||
|
catalogue-5f4cb7cf5-7zrmn 2/2 Running 0 20m
|
||||||
|
front-end-649fc5fd6-kqbtn 2/2 Running 0 20m
|
||||||
|
..
|
||||||
|
|
||||||
|
$ mizu tap "^ca.*"
|
||||||
|
+carts-66c77f5fbb-fq65r
|
||||||
|
+catalogue-5f4cb7cf5-7zrmn
|
||||||
|
Web interface is now available at http://localhost:8899
|
||||||
|
^C
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Mizu can work with config file which should be stored in ${HOME}/.mizu/config.yaml (macOS: ~/.mizu/config.yaml) <br />
|
||||||
|
In case no config file found, defaults will be used <br />
|
||||||
|
In case of partial configuration defined, all other fields will be used with defaults <br />
|
||||||
|
You can always override the defaults or config file with CLI flags
|
||||||
|
|
||||||
|
To get the default config params run `mizu config` <br />
|
||||||
|
To generate a new config file with default values use `mizu config -r`
|
||||||
|
|
||||||
|
### Telemetry
|
||||||
|
|
||||||
|
By default, mizu reports usage telemetry. It can be disabled by adding a line of `telemetry: false` in the `${HOME}/.mizu/config.yaml` file
|
||||||
|
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Namespace-Restricted Mode
|
||||||
|
|
||||||
|
Some users have permission to only manage resources in one particular namespace assigned to them
|
||||||
|
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
|
||||||
|
|
||||||
|
If `mizu-resources-namespace` is set to a value other than the default `mizu`, Mizu will operate in a
|
||||||
|
Namespace-Restricted mode. It will only tap pods in `mizu-resources-namespace`. This way Mizu only requires permissions
|
||||||
|
to the namespace set by `mizu-resources-namespace`. The user must set the tapped namespace to the same namespace by
|
||||||
|
using the `--namespace` flag or by setting `tap.namespaces` in the config file
|
||||||
|
|
||||||
|
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 tap.ignored-user-agents=kube-probe --set tap.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
|
||||||
|
|
||||||
|
### Traffic validation rules
|
||||||
|
|
||||||
|
This feature allows you to define set of simple rules, and test the traffic against them.
|
||||||
|
Such validation may test response for specific JSON fields, headers, etc.
|
||||||
|
|
||||||
|
Please see [TRAFFIC RULES](docs/POLICY_RULES.md) page for more details and syntax.
|
||||||
|
|
||||||
|
### OpenAPI Specification (OAS) Contract Monitoring
|
||||||
|
|
||||||
|
An OAS/Swagger file can contain schemas under `parameters` and `responses` fields. With `--contract catalogue.yaml`
|
||||||
|
CLI option, you can pass your API description to Mizu and the traffic will automatically be validated
|
||||||
|
against the contracts.
|
||||||
|
|
||||||
|
Please see [CONTRACT MONITORING](docs/CONTRACT_MONITORING.md) page for more details and syntax.
|
||||||
|
|
||||||
|
## How to Run local UI
|
||||||
|
|
||||||
|
- run from mizu/agent `go run main.go --hars-read --hars-dir <folder>`
|
||||||
|
|
||||||
|
- copy Har files into the folder from last command
|
||||||
|
|
||||||
|
- change `MizuWebsocketURL` and `apiURL` in `api.js` file
|
||||||
|
|
||||||
|
- run from mizu/ui - `npm run start`
|
||||||
|
|
||||||
|
- open browser on `localhost:3000`
|
||||||
|
|||||||
2
acceptanceTests/Makefile
Normal file
2
acceptanceTests/Makefile
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
test: ## Run acceptance tests.
|
||||||
|
@go test ./... -timeout 1h
|
||||||
283
acceptanceTests/config_test.go
Normal file
283
acceptanceTests/config_test.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tapConfig struct {
|
||||||
|
GuiPort uint16 `yaml:"gui-port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type configStruct struct {
|
||||||
|
Tap tapConfig `yaml:"tap"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRegenerate(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configCmdArgs := getDefaultConfigCommandArgs()
|
||||||
|
|
||||||
|
configCmdArgs = append(configCmdArgs, "-r")
|
||||||
|
|
||||||
|
configCmd := exec.Command(cliPath, configCmdArgs...)
|
||||||
|
t.Logf("running command: %v", configCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := configCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start config command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := configCmd.Wait(); err != nil {
|
||||||
|
t.Errorf("failed to wait config command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, readFileErr := ioutil.ReadFile(configPath)
|
||||||
|
if readFileErr != nil {
|
||||||
|
t.Errorf("failed to read config file, err: %v", readFileErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []uint16{8898}
|
||||||
|
|
||||||
|
for _, guiPort := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigSetGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ConfigFileGuiPort uint16
|
||||||
|
SetGuiPort uint16
|
||||||
|
}{
|
||||||
|
{ConfigFileGuiPort: 8898, SetGuiPort: 8897},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, guiPortStruct := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPortStruct.SetGuiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--set", fmt.Sprintf("tap.gui-port=%v", guiPortStruct.SetGuiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPortStruct.SetGuiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFlagGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
ConfigFileGuiPort uint16
|
||||||
|
FlagGuiPort uint16
|
||||||
|
}{
|
||||||
|
{ConfigFileGuiPort: 8898, FlagGuiPort: 8896},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, guiPortStruct := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPortStruct.FlagGuiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath, configPathErr := getConfigPath()
|
||||||
|
if configPathErr != nil {
|
||||||
|
t.Errorf("failed to get config path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configStruct{}
|
||||||
|
config.Tap.GuiPort = guiPortStruct.ConfigFileGuiPort
|
||||||
|
|
||||||
|
configBytes, marshalErr := yaml.Marshal(config)
|
||||||
|
if marshalErr != nil {
|
||||||
|
t.Errorf("failed to marshal config, err: %v", marshalErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeErr := ioutil.WriteFile(configPath, configBytes, 0644); writeErr != nil {
|
||||||
|
t.Errorf("failed to write config to file, err: %v", writeErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%v", guiPortStruct.FlagGuiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
t.Logf("failed to delete config file, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPortStruct.FlagGuiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
10
acceptanceTests/go.mod
Normal file
10
acceptanceTests/go.mod
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/up9inc/mizu/tests
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/up9inc/mizu/shared v0.0.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
||||||
8
acceptanceTests/go.sum
Normal file
8
acceptanceTests/go.sum
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
196
acceptanceTests/logs_test.go
Normal file
196
acceptanceTests/logs_test.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogs(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logsCmdArgs := getDefaultLogsCommandArgs()
|
||||||
|
|
||||||
|
logsCmd := exec.Command(cliPath, logsCmdArgs...)
|
||||||
|
t.Logf("running command: %v", logsCmd.String())
|
||||||
|
|
||||||
|
if err := logsCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start logs command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logsCmd.Wait(); err != nil {
|
||||||
|
t.Errorf("failed to wait logs command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logsPath, logsPathErr := getLogsPath()
|
||||||
|
if logsPathErr != nil {
|
||||||
|
t.Errorf("failed to get logs path, err: %v", logsPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zipReader, zipError := zip.OpenReader(logsPath)
|
||||||
|
if zipError != nil {
|
||||||
|
t.Errorf("failed to get zip reader, err: %v", zipError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := zipReader.Close(); err != nil {
|
||||||
|
t.Logf("failed to close zip reader, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var logsFileNames []string
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
logsFileNames = append(logsFileNames, file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu.mizu-api-server.log") {
|
||||||
|
t.Errorf("api server logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_cli.log") {
|
||||||
|
t.Errorf("cli logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_events.log") {
|
||||||
|
t.Errorf("events logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsPartOfValue(logsFileNames, "mizu.mizu-tapper-daemon-set") {
|
||||||
|
t.Errorf("tapper logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogsPath(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logsCmdArgs := getDefaultLogsCommandArgs()
|
||||||
|
|
||||||
|
logsPath := "../logs.zip"
|
||||||
|
logsCmdArgs = append(logsCmdArgs, "-f", logsPath)
|
||||||
|
|
||||||
|
logsCmd := exec.Command(cliPath, logsCmdArgs...)
|
||||||
|
t.Logf("running command: %v", logsCmd.String())
|
||||||
|
|
||||||
|
if err := logsCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start logs command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := logsCmd.Wait(); err != nil {
|
||||||
|
t.Errorf("failed to wait logs command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zipReader, zipError := zip.OpenReader(logsPath)
|
||||||
|
if zipError != nil {
|
||||||
|
t.Errorf("failed to get zip reader, err: %v", zipError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := zipReader.Close(); err != nil {
|
||||||
|
t.Logf("failed to close zip reader, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var logsFileNames []string
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
logsFileNames = append(logsFileNames, file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu.mizu-api-server.log") {
|
||||||
|
t.Errorf("api server logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_cli.log") {
|
||||||
|
t.Errorf("cli logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_events.log") {
|
||||||
|
t.Errorf("events logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsPartOfValue(logsFileNames, "mizu.mizu-tapper-daemon-set") {
|
||||||
|
t.Errorf("tapper logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
55
acceptanceTests/setup.sh
Normal file
55
acceptanceTests/setup.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PREFIX=$HOME/local/bin
|
||||||
|
VERSION=v1.22.0
|
||||||
|
|
||||||
|
echo "Attempting to install minikube and assorted tools to $PREFIX"
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v kubectl)" ]; then
|
||||||
|
echo "Installing kubectl version $VERSION"
|
||||||
|
curl -LO "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl"
|
||||||
|
chmod +x kubectl
|
||||||
|
mv kubectl "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "kubetcl is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v minikube)" ]; then
|
||||||
|
echo "Installing minikube version $VERSION"
|
||||||
|
curl -Lo minikube https://storage.googleapis.com/minikube/releases/$VERSION/minikube-linux-amd64
|
||||||
|
chmod +x minikube
|
||||||
|
mv minikube "$PREFIX"
|
||||||
|
else
|
||||||
|
echo "minikube is already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting minikube..."
|
||||||
|
minikube start
|
||||||
|
|
||||||
|
echo "Creating mizu tests namespaces"
|
||||||
|
kubectl create namespace mizu-tests
|
||||||
|
kubectl create namespace mizu-tests2
|
||||||
|
|
||||||
|
echo "Creating httpbin deployments"
|
||||||
|
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests
|
||||||
|
kubectl create deployment httpbin2 --image=kennethreitz/httpbin -n mizu-tests
|
||||||
|
|
||||||
|
kubectl create deployment httpbin --image=kennethreitz/httpbin -n mizu-tests2
|
||||||
|
|
||||||
|
echo "Creating httpbin services"
|
||||||
|
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests
|
||||||
|
kubectl expose deployment httpbin2 --type=NodePort --port=80 -n mizu-tests
|
||||||
|
|
||||||
|
kubectl expose deployment httpbin --type=NodePort --port=80 -n mizu-tests2
|
||||||
|
|
||||||
|
echo "Starting proxy"
|
||||||
|
kubectl proxy --port=8080 &
|
||||||
|
|
||||||
|
echo "Setting minikube docker env"
|
||||||
|
eval $(minikube docker-env)
|
||||||
|
|
||||||
|
echo "Build agent image"
|
||||||
|
make build-docker-ci
|
||||||
|
|
||||||
|
echo "Build cli"
|
||||||
|
make build-cli-ci
|
||||||
975
acceptanceTests/tap_test.go
Normal file
975
acceptanceTests/tap_test.go
Normal file
@@ -0,0 +1,975 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTap(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []int{50}
|
||||||
|
|
||||||
|
for _, entriesCount := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", entriesCount), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
for i := 0; i < entriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpGetRequest(fmt.Sprintf("%v/get", proxyUrl)); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, entriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, entry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestResult == nil {
|
||||||
|
return fmt.Errorf("unexpected nil entry result")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, entriesCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapGuiPort(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []uint16{8898}
|
||||||
|
|
||||||
|
for _, guiPort := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d", guiPort), func(t *testing.T) {
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-p", fmt.Sprintf("%d", guiPort))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(guiPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapAllNamespaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-A")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/status/tap", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapMultipleNamespaces(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin2", Namespace: "mizu-tests"},
|
||||||
|
{Name: "httpbin", Namespace: "mizu-tests2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
var namespacesCmd []string
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
namespacesCmd = append(namespacesCmd, "-n", expectedPod.Namespace)
|
||||||
|
}
|
||||||
|
tapCmdArgs = append(tapCmdArgs, namespacesCmd...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/status/tap", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expectedPods) != len(pods) {
|
||||||
|
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRegex(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
regexPodName := "httpbin2"
|
||||||
|
expectedPods := []struct{
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
}{
|
||||||
|
{Name: regexPodName, Namespace: "mizu-tests"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgsWithRegex(regexPodName)
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
podsUrl := fmt.Sprintf("%v/status/tap", apiServerUrl)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(podsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
t.Errorf("failed to get tap status, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pods, err := getPods(requestResult)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get pods, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expectedPods) != len(pods) {
|
||||||
|
t.Errorf("unexpected result - expected pods length: %v, actual pods length: %v", len(expectedPods), len(pods))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expectedPod := range expectedPods {
|
||||||
|
podFound := false
|
||||||
|
|
||||||
|
for _, pod := range pods {
|
||||||
|
podNamespace := pod["namespace"].(string)
|
||||||
|
podName := pod["name"].(string)
|
||||||
|
|
||||||
|
if expectedPod.Namespace == podNamespace && strings.Contains(podName, expectedPod.Name) {
|
||||||
|
podFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !podFound {
|
||||||
|
t.Errorf("unexpected result - expected pod not found, pod namespace: %v, pod name: %v", expectedPod.Namespace, expectedPod.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapDryRun(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--dry-run")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChannel := make(chan string, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := tapCmd.Wait(); err != nil {
|
||||||
|
resultChannel <- "fail"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resultChannel <- "success"
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(shortRetriesCount * time.Second)
|
||||||
|
resultChannel <- "fail"
|
||||||
|
}()
|
||||||
|
|
||||||
|
testResult := <- resultChannel
|
||||||
|
if testResult != "success" {
|
||||||
|
t.Errorf("unexpected result - dry run cmd not done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRedact(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
requestBody := map[string]string{"User": "Mizu"}
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||||
|
entryJson := data["entry"].(string)
|
||||||
|
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal([]byte(entryJson), &entry); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse entry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
entryPayload := entryRequest["payload"].(map[string]interface{})
|
||||||
|
entryDetails := entryPayload["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers := entryDetails["headers"].([]interface{})
|
||||||
|
for _, headerInterface := range headers {
|
||||||
|
header := headerInterface.(map[string]interface{})
|
||||||
|
if header["name"].(string) != "User-Agent" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := header["value"].(string)
|
||||||
|
if userAgent != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user agent is not redacted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postData := entryDetails["postData"].(map[string]interface{})
|
||||||
|
textDataStr := postData["text"].(string)
|
||||||
|
|
||||||
|
var textData map[string]string
|
||||||
|
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if textData["User"] != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user in body is not redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapNoRedact(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--no-redact")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
requestBody := map[string]string{"User": "Mizu"}
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpPostRequest(fmt.Sprintf("%v/post", proxyUrl), requestBody); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||||
|
entryJson := data["entry"].(string)
|
||||||
|
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal([]byte(entryJson), &entry); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse entry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
entryPayload := entryRequest["payload"].(map[string]interface{})
|
||||||
|
entryDetails := entryPayload["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers := entryDetails["headers"].([]interface{})
|
||||||
|
for _, headerInterface := range headers {
|
||||||
|
header := headerInterface.(map[string]interface{})
|
||||||
|
if header["name"].(string) != "User-Agent" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userAgent := header["value"].(string)
|
||||||
|
if userAgent == "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user agent is redacted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postData := entryDetails["postData"].(map[string]interface{})
|
||||||
|
textDataStr := postData["text"].(string)
|
||||||
|
|
||||||
|
var textData map[string]string
|
||||||
|
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if textData["User"] == "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - user in body is redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapRegexMasking(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "-r", "Mizu")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
response, requestErr := http.Post(fmt.Sprintf("%v/post", proxyUrl), "text/plain", bytes.NewBufferString("Mizu"))
|
||||||
|
if _, requestErr = executeHttpRequest(response, requestErr); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redactCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
firstEntry := entries[0].(map[string]interface{})
|
||||||
|
|
||||||
|
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||||
|
entryJson := data["entry"].(string)
|
||||||
|
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal([]byte(entryJson), &entry); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse entry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
entryPayload := entryRequest["payload"].(map[string]interface{})
|
||||||
|
entryDetails := entryPayload["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
postData := entryDetails["postData"].(map[string]interface{})
|
||||||
|
textData := postData["text"].(string)
|
||||||
|
|
||||||
|
if textData != "[REDACTED]" {
|
||||||
|
return fmt.Errorf("unexpected result - body is not redacted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapIgnoredUserAgents(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
ignoredUserAgentValue := "ignore"
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--set", fmt.Sprintf("tap.ignored-user-agents=%v", ignoredUserAgentValue))
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Logf("failed to cleanup tap command, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyUrl := getProxyUrl(defaultNamespaceName, defaultServiceName)
|
||||||
|
|
||||||
|
ignoredUserAgentCustomHeader := "Ignored-User-Agent"
|
||||||
|
headers := map[string]string {"User-Agent": ignoredUserAgentValue, ignoredUserAgentCustomHeader: ""}
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpGetRequestWithHeaders(fmt.Sprintf("%v/get", proxyUrl), headers); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < defaultEntriesCount; i++ {
|
||||||
|
if _, requestErr := executeHttpGetRequest(fmt.Sprintf("%v/get", proxyUrl)); requestErr != nil {
|
||||||
|
t.Errorf("failed to send proxy request, err: %v", requestErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoredUserAgentsCheckFunc := func() error {
|
||||||
|
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
entriesUrl := fmt.Sprintf("%v/entries?limit=%v&operator=lt×tamp=%v", apiServerUrl, defaultEntriesCount * 2, timestamp)
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(entriesUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entries, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := requestResult.([]interface{})
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return fmt.Errorf("unexpected entries result - Expected more than 0 entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entryInterface := range entries {
|
||||||
|
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, entryInterface.(map[string]interface{})["id"])
|
||||||
|
requestResult, requestErr = executeHttpGetRequest(entryUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
||||||
|
entryJson := data["entry"].(string)
|
||||||
|
|
||||||
|
var entry map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal([]byte(entryJson), &entry); parseErr != nil {
|
||||||
|
return fmt.Errorf("failed to parse entry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryRequest := entry["request"].(map[string]interface{})
|
||||||
|
entryPayload := entryRequest["payload"].(map[string]interface{})
|
||||||
|
entryDetails := entryPayload["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
entryHeaders := entryDetails["headers"].([]interface{})
|
||||||
|
for _, headerInterface := range entryHeaders {
|
||||||
|
header := headerInterface.(map[string]interface{})
|
||||||
|
if header["name"].(string) != ignoredUserAgentCustomHeader {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unexpected result - user agent is not ignored")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := retriesExecute(shortRetriesCount, ignoredUserAgentsCheckFunc); err != nil {
|
||||||
|
t.Errorf("%v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTapDumpLogs(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("ignored acceptance test")
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath, cliPathErr := getCliPath()
|
||||||
|
if cliPathErr != nil {
|
||||||
|
t.Errorf("failed to get cli path, err: %v", cliPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tapCmdArgs := getDefaultTapCommandArgs()
|
||||||
|
|
||||||
|
tapNamespace := getDefaultTapNamespace()
|
||||||
|
tapCmdArgs = append(tapCmdArgs, tapNamespace...)
|
||||||
|
|
||||||
|
tapCmdArgs = append(tapCmdArgs, "--set", "dump-logs=true")
|
||||||
|
|
||||||
|
tapCmd := exec.Command(cliPath, tapCmdArgs...)
|
||||||
|
t.Logf("running command: %v", tapCmd.String())
|
||||||
|
|
||||||
|
if err := tapCmd.Start(); err != nil {
|
||||||
|
t.Errorf("failed to start tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiServerUrl := getApiServerUrl(defaultApiServerPort)
|
||||||
|
|
||||||
|
if err := waitTapPodsReady(apiServerUrl); err != nil {
|
||||||
|
t.Errorf("failed to start tap pods on time, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cleanupCommand(tapCmd); err != nil {
|
||||||
|
t.Errorf("failed to cleanup tap command, err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mizuFolderPath, mizuPathErr := getMizuFolderPath()
|
||||||
|
if mizuPathErr != nil {
|
||||||
|
t.Errorf("failed to get mizu folder path, err: %v", mizuPathErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files, readErr := ioutil.ReadDir(mizuFolderPath)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Errorf("failed to read mizu folder files, err: %v", readErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var dumpsLogsPath string
|
||||||
|
for _, file := range files {
|
||||||
|
fileName := file.Name()
|
||||||
|
if strings.Contains(fileName, "mizu_logs") {
|
||||||
|
dumpsLogsPath = path.Join(mizuFolderPath, fileName)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dumpsLogsPath == "" {
|
||||||
|
t.Errorf("dump logs file not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zipReader, zipError := zip.OpenReader(dumpsLogsPath)
|
||||||
|
if zipError != nil {
|
||||||
|
t.Errorf("failed to get zip reader, err: %v", zipError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := zipReader.Close(); err != nil {
|
||||||
|
t.Logf("failed to close zip reader, err: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var logsFileNames []string
|
||||||
|
for _, file := range zipReader.File {
|
||||||
|
logsFileNames = append(logsFileNames, file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu.mizu-api-server.log") {
|
||||||
|
t.Errorf("api server logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_cli.log") {
|
||||||
|
t.Errorf("cli logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Contains(logsFileNames, "mizu_events.log") {
|
||||||
|
t.Errorf("events logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsPartOfValue(logsFileNames, "mizu.mizu-tapper-daemon-set") {
|
||||||
|
t.Errorf("tapper logs not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
259
acceptanceTests/testsUtils.go
Normal file
259
acceptanceTests/testsUtils.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package acceptanceTests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
longRetriesCount = 100
|
||||||
|
shortRetriesCount = 10
|
||||||
|
defaultApiServerPort = shared.DefaultApiServerPort
|
||||||
|
defaultNamespaceName = "mizu-tests"
|
||||||
|
defaultServiceName = "httpbin"
|
||||||
|
defaultEntriesCount = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCliPath() (string, error) {
|
||||||
|
dir, filePathErr := os.Getwd()
|
||||||
|
if filePathErr != nil {
|
||||||
|
return "", filePathErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPath := path.Join(dir, "../cli/bin/mizu_ci")
|
||||||
|
return cliPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMizuFolderPath() (string, error) {
|
||||||
|
home, homeDirErr := os.UserHomeDir()
|
||||||
|
if homeDirErr != nil {
|
||||||
|
return "", homeDirErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(home, ".mizu"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfigPath() (string, error) {
|
||||||
|
mizuFolderPath, mizuPathError := getMizuFolderPath()
|
||||||
|
if mizuPathError != nil {
|
||||||
|
return "", mizuPathError
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(mizuFolderPath, "config.yaml"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyUrl(namespace string, service string) string {
|
||||||
|
return fmt.Sprintf("http://localhost:8080/api/v1/namespaces/%v/services/%v/proxy", namespace, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getApiServerUrl(port uint16) string {
|
||||||
|
return fmt.Sprintf("http://localhost:%v/mizu", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultCommandArgs() []string {
|
||||||
|
setFlag := "--set"
|
||||||
|
telemetry := "telemetry=false"
|
||||||
|
agentImage := "agent-image=gcr.io/up9-docker-hub/mizu/ci:0.0.0"
|
||||||
|
imagePullPolicy := "image-pull-policy=Never"
|
||||||
|
|
||||||
|
return []string{setFlag, telemetry, setFlag, agentImage, setFlag, imagePullPolicy}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapCommandArgs() []string {
|
||||||
|
tapCommand := "tap"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapCommandArgsWithRegex(regex string) []string {
|
||||||
|
tapCommand := "tap"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{tapCommand, regex}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultLogsCommandArgs() []string {
|
||||||
|
logsCommand := "logs"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{logsCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTapNamespace() []string {
|
||||||
|
return []string{"-n", "mizu-tests"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultConfigCommandArgs() []string {
|
||||||
|
configCommand := "config"
|
||||||
|
defaultCmdArgs := getDefaultCommandArgs()
|
||||||
|
|
||||||
|
return append([]string{configCommand}, defaultCmdArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func retriesExecute(retriesCount int, executeFunc func() error) error {
|
||||||
|
var lastError interface{}
|
||||||
|
|
||||||
|
for i := 0; i < retriesCount; i++ {
|
||||||
|
if err := tryExecuteFunc(executeFunc); err != nil {
|
||||||
|
lastError = err
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("reached max retries count, retries count: %v, last err: %v", retriesCount, lastError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryExecuteFunc(executeFunc func() error) (err interface{}) {
|
||||||
|
defer func() {
|
||||||
|
if panicErr := recover(); panicErr != nil {
|
||||||
|
err = panicErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return executeFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitTapPodsReady(apiServerUrl string) error {
|
||||||
|
resolvingUrl := fmt.Sprintf("%v/status/tappersCount", apiServerUrl)
|
||||||
|
tapPodsReadyFunc := func() error {
|
||||||
|
requestResult, requestErr := executeHttpGetRequest(resolvingUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return requestErr
|
||||||
|
}
|
||||||
|
|
||||||
|
tappersCount := requestResult.(float64)
|
||||||
|
if tappersCount == 0 {
|
||||||
|
return fmt.Errorf("no tappers running")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return retriesExecute(longRetriesCount, tapPodsReadyFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonBytesToInterface(jsonBytes []byte) (interface{}, error) {
|
||||||
|
var result interface{}
|
||||||
|
if parseErr := json.Unmarshal(jsonBytes, &result); parseErr != nil {
|
||||||
|
return nil, parseErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpRequest(response *http.Response, requestErr error) (interface{}, error) {
|
||||||
|
if requestErr != nil {
|
||||||
|
return nil, requestErr
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("invalid status code %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { response.Body.Close() }()
|
||||||
|
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonBytesToInterface(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpGetRequestWithHeaders(url string, headers map[string]string) (interface{}, error) {
|
||||||
|
request, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for headerKey, headerValue := range headers {
|
||||||
|
request.Header.Add(headerKey, headerValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
response, requestErr := client.Do(request)
|
||||||
|
return executeHttpRequest(response, requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpGetRequest(url string) (interface{}, error) {
|
||||||
|
response, requestErr := http.Get(url)
|
||||||
|
return executeHttpRequest(response, requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeHttpPostRequest(url string, body interface{}) (interface{}, error) {
|
||||||
|
requestBody, jsonErr := json.Marshal(body)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return nil, jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
response, requestErr := http.Post(url, "application/json", bytes.NewBuffer(requestBody))
|
||||||
|
return executeHttpRequest(response, requestErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupCommand(cmd *exec.Cmd) error {
|
||||||
|
if err := cmd.Process.Signal(syscall.SIGQUIT); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPods(tapStatusInterface interface{}) ([]map[string]interface{}, error) {
|
||||||
|
tapStatus := tapStatusInterface.(map[string]interface{})
|
||||||
|
podsInterface := tapStatus["pods"].([]interface{})
|
||||||
|
|
||||||
|
var pods []map[string]interface{}
|
||||||
|
for _, podInterface := range podsInterface {
|
||||||
|
pods = append(pods, podInterface.(map[string]interface{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogsPath() (string, error) {
|
||||||
|
dir, filePathErr := os.Getwd()
|
||||||
|
if filePathErr != nil {
|
||||||
|
return "", filePathErr
|
||||||
|
}
|
||||||
|
|
||||||
|
logsPath := path.Join(dir, "mizu_logs.zip")
|
||||||
|
return logsPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Contains(slice []string, containsValue string) bool {
|
||||||
|
for _, sliceValue := range slice {
|
||||||
|
if sliceValue == containsValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContainsPartOfValue(slice []string, containsValue string) bool {
|
||||||
|
for _, sliceValue := range slice {
|
||||||
|
if strings.Contains(sliceValue, containsValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
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
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# mizu API server
|
# mizu agent
|
||||||
API server for MIZU
|
Agent for MIZU (API server and tapper)
|
||||||
Basic APIs:
|
Basic APIs:
|
||||||
* /fetch - retrieve traffic data
|
|
||||||
* /stats - retrieve statistics of collected data
|
* /stats - retrieve statistics of collected data
|
||||||
* /viewer - web ui
|
* /viewer - web ui
|
||||||
|
|
||||||
@@ -13,8 +12,9 @@ Basic APIs:
|
|||||||
`docker build . -t gcr.io/up9-docker-hub/mizu/debug:latest -f debug.Dockerfile && docker push gcr.io/up9-docker-hub/mizu/debug:latest`
|
`docker build . -t gcr.io/up9-docker-hub/mizu/debug:latest -f debug.Dockerfile && docker push gcr.io/up9-docker-hub/mizu/debug:latest`
|
||||||
|
|
||||||
### Connecting
|
### Connecting
|
||||||
1. Start mizu using the cli with the debug image `mizu tap --mizu-image gcr.io/up9-docker-hub/mizu/debug:latest {tapped_pod_name}`
|
1. Start mizu using the cli with the debug
|
||||||
2. Forward the debug port using `kubectl port-forward -n default mizu-collector 2345:2345`
|
image `mizu tap --set agent-image=gcr.io/up9-docker-hub/mizu/debug:latest {tapped_pod_name}`
|
||||||
|
2. Forward the debug port using `kubectl port-forward -n default mizu-api-server 2345:2345`
|
||||||
3. Run the run/debug configuration you've created earlier in Intellij.
|
3. Run the run/debug configuration you've created earlier in Intellij.
|
||||||
|
|
||||||
<small>Do note that dlv won't start the api until a debugger connects to it.</small>
|
<small>Do note that dlv won't start the api until a debugger connects to it.</small>
|
||||||
@@ -3,23 +3,24 @@ module mizuserver
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a
|
|
||||||
github.com/beevik/etree v1.1.0
|
|
||||||
github.com/djherbis/atime v1.0.0
|
github.com/djherbis/atime v1.0.0
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1 // indirect
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
|
github.com/getkin/kin-openapi v0.76.0
|
||||||
|
github.com/gin-contrib/static v0.0.1
|
||||||
|
github.com/gin-gonic/gin v1.7.2
|
||||||
github.com/go-playground/locales v0.13.0
|
github.com/go-playground/locales v0.13.0
|
||||||
github.com/go-playground/universal-translator v0.17.0
|
github.com/go-playground/universal-translator v0.17.0
|
||||||
github.com/go-playground/validator/v10 v10.5.0
|
github.com/go-playground/validator/v10 v10.5.0
|
||||||
github.com/gofiber/fiber/v2 v2.8.0
|
|
||||||
github.com/google/gopacket v1.1.19
|
|
||||||
github.com/google/martian v2.1.0+incompatible
|
github.com/google/martian v2.1.0+incompatible
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/up9inc/mizu/shared v0.0.0
|
github.com/up9inc/mizu/shared v0.0.0
|
||||||
go.mongodb.org/mongo-driver v1.5.1
|
github.com/up9inc/mizu/tap v0.0.0
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758
|
github.com/up9inc/mizu/tap/api v0.0.0
|
||||||
|
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0
|
||||||
|
go.mongodb.org/mongo-driver v1.7.1
|
||||||
gorm.io/driver/sqlite v1.1.4
|
gorm.io/driver/sqlite v1.1.4
|
||||||
gorm.io/gorm v1.21.8
|
gorm.io/gorm v1.21.8
|
||||||
k8s.io/api v0.21.0
|
k8s.io/api v0.21.0
|
||||||
@@ -28,3 +29,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
replace github.com/up9inc/mizu/shared v0.0.0 => ../shared
|
||||||
|
|
||||||
|
replace github.com/up9inc/mizu/tap v0.0.0 => ../tap
|
||||||
|
|
||||||
|
replace github.com/up9inc/mizu/tap/api v0.0.0 => ../tap/api
|
||||||
@@ -41,15 +41,9 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
|
|||||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
|
|
||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a h1:76llBleIE3fkdqaJFDzdirtiYhQPdIQem8H8r2iwA1Q=
|
|
||||||
github.com/antoniodipinto/ikisocket v0.0.0-20210417133349-f1502512d69a/go.mod h1:QvDfsDQDmGxUsvEeWabVZ5pp2FMXpOkwQV0L6SE6cp0=
|
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M=
|
||||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM=
|
||||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
@@ -61,18 +55,32 @@ 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/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg=
|
github.com/djherbis/atime v1.0.0 h1:ySLvBAM0EvOGaX7TI4dAM5lWj+RdJUCKtGSEHN8SGBg=
|
||||||
github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8=
|
github.com/djherbis/atime v1.0.0/go.mod h1:5W+KBIuTwVGcqjIfaTwt+KSYX1o6uep8dtevevQP/f8=
|
||||||
|
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||||
|
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||||
github.com/fasthttp/websocket v1.4.2/go.mod h1:smsv/h4PBEBaU0XDTY5UwJTpZv69fQ0FfcLJr21mA6Y=
|
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1 h1:stc4P2aoxYKsdmbe1AJ5mAm73Fxc1NOgrZpPftvZIXQ=
|
|
||||||
github.com/fasthttp/websocket v1.4.3-beta.1/go.mod h1:JGrgLaT02bL9NuJkZbHN8mVV2tkCJZQh7yJ5/XCXO2g=
|
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/getkin/kin-openapi v0.76.0 h1:j77zg3Ec+k+r+GA3d8hBoXpAc6KX9TbBPrwQGBIy2sY=
|
||||||
|
github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
|
||||||
|
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
|
||||||
|
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||||
|
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
||||||
|
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
|
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
||||||
|
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
@@ -81,10 +89,13 @@ github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
|
|||||||
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
|
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
|
||||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||||
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
|
||||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
|
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
|
||||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
@@ -92,9 +103,10 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
|
|||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-playground/validator/v10 v10.5.0 h1:X9rflw/KmpACwT8zdrm1upefpvdy6ur8d1kWyq6sg3E=
|
github.com/go-playground/validator/v10 v10.5.0 h1:X9rflw/KmpACwT8zdrm1upefpvdy6ur8d1kWyq6sg3E=
|
||||||
github.com/go-playground/validator/v10 v10.5.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
|
github.com/go-playground/validator/v10 v10.5.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
|
||||||
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
|
||||||
@@ -120,14 +132,10 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
|
|||||||
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
|
||||||
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
|
||||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||||
github.com/gofiber/fiber/v2 v2.1.3/go.mod h1:MMiSv1HrDkN8Pv7NeVDYK+T/lwXOEKAvPBbLvJPCEfA=
|
|
||||||
github.com/gofiber/fiber/v2 v2.7.1/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0=
|
|
||||||
github.com/gofiber/fiber/v2 v2.8.0 h1:BdWvZmg/WY/Vjtjm38aXOp1Lks1BhuyS2b7lSWSPAzk=
|
|
||||||
github.com/gofiber/fiber/v2 v2.8.0/go.mod h1:Ah3IJikrKNRepl/HuVawppS25X7FWohwfCSRn7kJG28=
|
|
||||||
github.com/gofiber/websocket/v2 v2.0.3 h1:nqPGHB4LQhxKX5KJUjayOd2xiiENieS/dn6TPfCL8uk=
|
|
||||||
github.com/gofiber/websocket/v2 v2.0.3/go.mod h1:/OTEImCxORKE5unw0dWqJYovid6vZF+wB1W0aaMKs2M=
|
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -179,6 +187,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
|||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
|
github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
|
||||||
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
@@ -186,7 +196,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
|||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
|
|
||||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -194,10 +203,9 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
|
||||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
|
||||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
@@ -206,13 +214,7 @@ github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaR
|
|||||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
|
||||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
|
|
||||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
|
||||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -222,13 +224,15 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
|
||||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
|
||||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
|
||||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||||
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
|
||||||
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
@@ -249,6 +253,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
|
|||||||
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 h1:fa50YL1pzKW+1SsBnJDOHppJN9stOEwS+CRWyUtyYGU=
|
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 h1:fa50YL1pzKW+1SsBnJDOHppJN9stOEwS+CRWyUtyYGU=
|
||||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
@@ -264,9 +270,6 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:
|
|||||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/savsgio/gotils v0.0.0-20200117113501-90175b0fbe3f/go.mod h1:lHhJedqxCoHN+zMtwGNTXWmF0u9Jt363FYRhV6g0CdY=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c h1:KKqhycXW1WVNkX7r4ekTV2gFkbhdyihlWD8c0/FiWmk=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20200616100644-13ff1fd2c28c/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8=
|
|
||||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
@@ -278,33 +281,28 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
|
||||||
github.com/valyala/fasthttp v1.18.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A=
|
|
||||||
github.com/valyala/fasthttp v1.23.0 h1:0ufwSD9BhWa6f8HWdmdq4FHQ23peRo3Ng/Qs8m5NcFs=
|
|
||||||
github.com/valyala/fasthttp v1.23.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
|
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||||
|
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/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI=
|
go.mongodb.org/mongo-driver v1.7.1 h1:jwqTeEM3x6L9xDXrCxN0Hbg7vdGfPBOTIkr0+/LYZDA=
|
||||||
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
|
go.mongodb.org/mongo-driver v1.7.1/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
@@ -372,13 +370,9 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM=
|
||||||
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY=
|
|
||||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -410,24 +404,22 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe h1:WdX7u8s3yOigWAhHEaDl8r9G+4XwFQEQFtBMYyN+kXQ=
|
|
||||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||||
@@ -438,9 +430,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
|
|||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
@@ -553,10 +544,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
|||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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.3.0/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=
|
||||||
315
agent/main.go
Normal file
315
agent/main.go
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"mizuserver/pkg/api"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/routes"
|
||||||
|
"mizuserver/pkg/up9"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/static"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/op/go-logging"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tapperMode = flag.Bool("tap", false, "Run in tapper mode without API")
|
||||||
|
var apiServerMode = flag.Bool("api-server", false, "Run in API server mode with API")
|
||||||
|
var standaloneMode = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
|
||||||
|
var apiServerAddress = flag.String("api-server-address", "", "Address of mizu API server")
|
||||||
|
var namespace = flag.String("namespace", "", "Resolve IPs if they belong to resources in this namespace (default is all)")
|
||||||
|
var harsReaderMode = flag.Bool("hars-read", false, "Run in hars-read mode")
|
||||||
|
var harsDir = flag.String("hars-dir", "", "Directory to read hars from")
|
||||||
|
|
||||||
|
var extensions []*tapApi.Extension // global
|
||||||
|
var extensionsMap map[string]*tapApi.Extension // global
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logLevel := determineLogLevel()
|
||||||
|
logger.InitLoggerStderrOnly(logLevel)
|
||||||
|
flag.Parse()
|
||||||
|
loadExtensions()
|
||||||
|
|
||||||
|
if !*tapperMode && !*apiServerMode && !*standaloneMode && !*harsReaderMode {
|
||||||
|
panic("One of the flags --tap, --api or --standalone or --hars-read must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *standaloneMode {
|
||||||
|
api.StartResolving(*namespace)
|
||||||
|
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
filteringOptions := getTrafficFilteringOptions()
|
||||||
|
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
||||||
|
tapOpts := &tap.TapOpts{HostMode: hostMode}
|
||||||
|
tap.StartPassiveTapper(tapOpts, outputItemsChannel, extensions, filteringOptions)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredOutputItemsChannel)
|
||||||
|
go api.StartReadingEntries(filteredOutputItemsChannel, nil, extensionsMap)
|
||||||
|
|
||||||
|
hostApi(nil)
|
||||||
|
} else if *tapperMode {
|
||||||
|
logger.Log.Infof("Starting tapper, websocket address: %s", *apiServerAddress)
|
||||||
|
if *apiServerAddress == "" {
|
||||||
|
panic("API server address must be provided with --api-server-address when using --tap")
|
||||||
|
}
|
||||||
|
|
||||||
|
tapTargets := getTapTargets()
|
||||||
|
if tapTargets != nil {
|
||||||
|
tap.SetFilterAuthorities(tapTargets)
|
||||||
|
logger.Log.Infof("Filtering for the following authorities: %v", tap.GetFilterIPs())
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
filteringOptions := getTrafficFilteringOptions()
|
||||||
|
hostMode := os.Getenv(shared.HostModeEnvVar) == "1"
|
||||||
|
tapOpts := &tap.TapOpts{HostMode: hostMode}
|
||||||
|
tap.StartPassiveTapper(tapOpts, filteredOutputItemsChannel, extensions, filteringOptions)
|
||||||
|
socketConnection, _, err := websocket.DefaultDialer.Dial(*apiServerAddress, nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("Error connecting to socket server at %s %v", *apiServerAddress, err))
|
||||||
|
}
|
||||||
|
logger.Log.Infof("Connected successfully to websocket %s", *apiServerAddress)
|
||||||
|
|
||||||
|
go pipeTapChannelToSocket(socketConnection, filteredOutputItemsChannel)
|
||||||
|
} else if *apiServerMode {
|
||||||
|
api.StartResolving(*namespace)
|
||||||
|
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
filteredOutputItemsChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredOutputItemsChannel)
|
||||||
|
go api.StartReadingEntries(filteredOutputItemsChannel, nil, extensionsMap)
|
||||||
|
|
||||||
|
syncEntriesConfig := getSyncEntriesConfig()
|
||||||
|
if syncEntriesConfig != nil {
|
||||||
|
if err := up9.SyncEntries(syncEntriesConfig); err != nil {
|
||||||
|
panic(fmt.Sprintf("Error syncing entries, err: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostApi(outputItemsChannel)
|
||||||
|
} else if *harsReaderMode {
|
||||||
|
outputItemsChannel := make(chan *tapApi.OutputChannelItem, 1000)
|
||||||
|
filteredHarChannel := make(chan *tapApi.OutputChannelItem)
|
||||||
|
|
||||||
|
go filterItems(outputItemsChannel, filteredHarChannel)
|
||||||
|
go api.StartReadingEntries(filteredHarChannel, harsDir, extensionsMap)
|
||||||
|
hostApi(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signalChan, os.Interrupt)
|
||||||
|
<-signalChan
|
||||||
|
|
||||||
|
logger.Log.Info("Exiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadExtensions() {
|
||||||
|
dir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
extensionsDir := path.Join(dir, "./extensions/")
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(extensionsDir)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Fatal(err)
|
||||||
|
}
|
||||||
|
extensions = make([]*tapApi.Extension, len(files))
|
||||||
|
extensionsMap = make(map[string]*tapApi.Extension)
|
||||||
|
for i, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
logger.Log.Infof("Loading extension: %s\n", filename)
|
||||||
|
extension := &tapApi.Extension{
|
||||||
|
Path: path.Join(extensionsDir, filename),
|
||||||
|
}
|
||||||
|
plug, _ := plugin.Open(extension.Path)
|
||||||
|
extension.Plug = plug
|
||||||
|
symDissector, err := plug.Lookup("Dissector")
|
||||||
|
|
||||||
|
var dissector tapApi.Dissector
|
||||||
|
var ok bool
|
||||||
|
dissector, ok = symDissector.(tapApi.Dissector)
|
||||||
|
if err != nil || !ok {
|
||||||
|
panic(fmt.Sprintf("Failed to load the extension: %s\n", extension.Path))
|
||||||
|
}
|
||||||
|
dissector.Register(extension)
|
||||||
|
extension.Dissector = dissector
|
||||||
|
extensions[i] = extension
|
||||||
|
extensionsMap[extension.Protocol.Name] = extension
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(extensions, func(i, j int) bool {
|
||||||
|
return extensions[i].Protocol.Priority < extensions[j].Protocol.Priority
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, extension := range extensions {
|
||||||
|
logger.Log.Infof("Extension Properties: %+v\n", extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
controllers.InitExtensionsMap(extensionsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
||||||
|
app := gin.Default()
|
||||||
|
|
||||||
|
app.GET("/echo", func(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "Here is Mizu agent")
|
||||||
|
})
|
||||||
|
|
||||||
|
eventHandlers := api.RoutesEventHandlers{
|
||||||
|
SocketOutChannel: socketHarOutputChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Use(DisableRootStaticCache())
|
||||||
|
app.Use(static.ServeRoot("/", "./site"))
|
||||||
|
app.Use(CORSMiddleware()) // This has to be called after the static middleware, does not work if its called before
|
||||||
|
|
||||||
|
api.WebSocketRoutes(app, &eventHandlers)
|
||||||
|
routes.EntriesRoutes(app)
|
||||||
|
routes.MetadataRoutes(app)
|
||||||
|
routes.StatusRoutes(app)
|
||||||
|
routes.NotFoundRoute(app)
|
||||||
|
|
||||||
|
utils.StartServer(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableRootStaticCache() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.RequestURI == "/" {
|
||||||
|
// Disable cache only for the main static route
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-store")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CORSMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
||||||
|
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvVar(env string) map[string][]string {
|
||||||
|
var mapOfList map[string][]string
|
||||||
|
|
||||||
|
val, present := os.LookupEnv(env)
|
||||||
|
|
||||||
|
if !present {
|
||||||
|
return mapOfList
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(val), &mapOfList)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("env var %s's value of %s is invalid! must be map[string][]string %v", env, mapOfList, err))
|
||||||
|
}
|
||||||
|
return mapOfList
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTapTargets() []string {
|
||||||
|
nodeName := os.Getenv(shared.NodeNameEnvVar)
|
||||||
|
tappedAddressesPerNodeDict := parseEnvVar(shared.TappedAddressesPerNodeDictEnvVar)
|
||||||
|
return tappedAddressesPerNodeDict[nodeName]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTrafficFilteringOptions() *tapApi.TrafficFilteringOptions {
|
||||||
|
filteringOptionsJson := os.Getenv(shared.MizuFilteringOptionsEnvVar)
|
||||||
|
if filteringOptionsJson == "" {
|
||||||
|
return &tapApi.TrafficFilteringOptions{
|
||||||
|
IgnoredUserAgents: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var filteringOptions tapApi.TrafficFilteringOptions
|
||||||
|
err := json.Unmarshal([]byte(filteringOptionsJson), &filteringOptions)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("env var %s's value of %s is invalid! json must match the api.TrafficFilteringOptions struct %v", shared.MizuFilteringOptionsEnvVar, filteringOptionsJson, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filteringOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterItems(inChannel <-chan *tapApi.OutputChannelItem, outChannel chan *tapApi.OutputChannelItem) {
|
||||||
|
for message := range inChannel {
|
||||||
|
if message.ConnectionInfo.IsOutgoing && api.CheckIsServiceIP(message.ConnectionInfo.ServerIP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outChannel <- message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pipeTapChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tapApi.OutputChannelItem) {
|
||||||
|
if connection == nil {
|
||||||
|
panic("Websocket connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageDataChannel == nil {
|
||||||
|
panic("Channel of captured messages is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
for messageData := range messageDataChannel {
|
||||||
|
marshaledData, err := models.CreateWebsocketTappedEntryMessage(messageData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("error converting message to json %v, err: %s, (%v,%+v)", messageData, err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is where the `*tapApi.OutputChannelItem` leaves the code
|
||||||
|
// and goes into the intermediate WebSocket.
|
||||||
|
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("error sending message through socket server %v, err: %s, (%v,%+v)", messageData, err, err, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSyncEntriesConfig() *shared.SyncEntriesConfig {
|
||||||
|
syncEntriesConfigJson := os.Getenv(shared.SyncEntriesConfigEnvVar)
|
||||||
|
if syncEntriesConfigJson == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncEntriesConfig = &shared.SyncEntriesConfig{}
|
||||||
|
err := json.Unmarshal([]byte(syncEntriesConfigJson), syncEntriesConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("env var %s's value of %s is invalid! json must match the shared.SyncEntriesConfig struct, err: %v", shared.SyncEntriesConfigEnvVar, syncEntriesConfigJson, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncEntriesConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func determineLogLevel() (logLevel logging.Level) {
|
||||||
|
logLevel = logging.INFO
|
||||||
|
if os.Getenv(shared.DebugModeEnvVar) == "1" {
|
||||||
|
logLevel = logging.DEBUG
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
110
agent/pkg/api/contract_validation.go
Normal file
110
agent/pkg/api/contract_validation.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/getkin/kin-openapi/openapi3filter"
|
||||||
|
"github.com/getkin/kin-openapi/routers"
|
||||||
|
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContractNotApplicable api.ContractStatus = 0
|
||||||
|
ContractPassed api.ContractStatus = 1
|
||||||
|
ContractFailed api.ContractStatus = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadOAS(ctx context.Context) (doc *openapi3.T, contractContent string, router routers.Router, err error) {
|
||||||
|
path := fmt.Sprintf("%s/%s", shared.RulePolicyPath, shared.ContractFileName)
|
||||||
|
bytes, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contractContent = string(bytes)
|
||||||
|
loader := &openapi3.Loader{Context: ctx}
|
||||||
|
doc, _ = loader.LoadFromData(bytes)
|
||||||
|
err = doc.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
router, _ = legacyrouter.NewRouter(doc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOAS(ctx context.Context, doc *openapi3.T, router routers.Router, req *http.Request, res *http.Response) (isValid bool, reqErr error, resErr error) {
|
||||||
|
isValid = true
|
||||||
|
reqErr = nil
|
||||||
|
resErr = nil
|
||||||
|
|
||||||
|
// Find route
|
||||||
|
route, pathParams, err := router.FindRoute(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
requestValidationInput := &openapi3filter.RequestValidationInput{
|
||||||
|
Request: req,
|
||||||
|
PathParams: pathParams,
|
||||||
|
Route: route,
|
||||||
|
}
|
||||||
|
if reqErr = openapi3filter.ValidateRequest(ctx, requestValidationInput); reqErr != nil {
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
responseValidationInput := &openapi3filter.ResponseValidationInput{
|
||||||
|
RequestValidationInput: requestValidationInput,
|
||||||
|
Status: res.StatusCode,
|
||||||
|
Header: res.Header,
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Body != nil {
|
||||||
|
body, _ := ioutil.ReadAll(res.Body)
|
||||||
|
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
responseValidationInput.SetBodyBytes(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate response.
|
||||||
|
if resErr = openapi3filter.ValidateResponse(ctx, responseValidationInput); resErr != nil {
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleOAS(ctx context.Context, doc *openapi3.T, router routers.Router, req *http.Request, res *http.Response, contractContent string) (contract api.Contract) {
|
||||||
|
contract = api.Contract{
|
||||||
|
Content: contractContent,
|
||||||
|
Status: ContractNotApplicable,
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid, reqErr, resErr := validateOAS(ctx, doc, router, req, res)
|
||||||
|
if isValid {
|
||||||
|
contract.Status = ContractPassed
|
||||||
|
} else {
|
||||||
|
contract.Status = ContractFailed
|
||||||
|
if reqErr != nil {
|
||||||
|
contract.RequestReason = reqErr.Error()
|
||||||
|
} else {
|
||||||
|
contract.RequestReason = ""
|
||||||
|
}
|
||||||
|
if resErr != nil {
|
||||||
|
contract.ResponseReason = resErr.Error()
|
||||||
|
} else {
|
||||||
|
contract.ResponseReason = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
191
agent/pkg/api/main.go
Normal file
191
agent/pkg/api/main.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/holder"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/resolver"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var k8sResolver *resolver.Resolver
|
||||||
|
|
||||||
|
func StartResolving(namespace string) {
|
||||||
|
errOut := make(chan error, 100)
|
||||||
|
res, err := resolver.NewFromInCluster(errOut, namespace)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("error creating k8s resolver %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
res.Start(ctx)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-errOut:
|
||||||
|
logger.Log.Infof("name resolving error %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
k8sResolver = res
|
||||||
|
holder.SetResolver(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartReadingEntries(harChannel <-chan *tapApi.OutputChannelItem, workingDir *string, extensionsMap map[string]*tapApi.Extension) {
|
||||||
|
if workingDir != nil && *workingDir != "" {
|
||||||
|
startReadingFiles(*workingDir)
|
||||||
|
} else {
|
||||||
|
startReadingChannel(harChannel, extensionsMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReadingFiles(workingDir string) {
|
||||||
|
if err := os.MkdirAll(workingDir, os.ModePerm); err != nil {
|
||||||
|
logger.Log.Errorf("Failed to make dir: %s, err: %v", workingDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for true {
|
||||||
|
dir, _ := os.Open(workingDir)
|
||||||
|
dirFiles, _ := dir.Readdir(-1)
|
||||||
|
|
||||||
|
var harFiles []os.FileInfo
|
||||||
|
for _, fileInfo := range dirFiles {
|
||||||
|
if strings.HasSuffix(fileInfo.Name(), ".har") {
|
||||||
|
harFiles = append(harFiles, fileInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(utils.ByModTime(harFiles))
|
||||||
|
|
||||||
|
if len(harFiles) == 0 {
|
||||||
|
logger.Log.Infof("Waiting for new files\n")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fileInfo := harFiles[0]
|
||||||
|
inputFilePath := path.Join(workingDir, fileInfo.Name())
|
||||||
|
file, err := os.Open(inputFilePath)
|
||||||
|
utils.CheckErr(err)
|
||||||
|
|
||||||
|
var inputHar har.HAR
|
||||||
|
decErr := json.NewDecoder(bufio.NewReader(file)).Decode(&inputHar)
|
||||||
|
utils.CheckErr(decErr)
|
||||||
|
|
||||||
|
rmErr := os.Remove(inputFilePath)
|
||||||
|
utils.CheckErr(rmErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extensionsMap map[string]*tapApi.Extension) {
|
||||||
|
if outputItems == nil {
|
||||||
|
panic("Channel of captured messages is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
disableOASValidation := false
|
||||||
|
ctx := context.Background()
|
||||||
|
doc, contractContent, router, err := loadOAS(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("Disabled OAS validation: %s\n", err.Error())
|
||||||
|
disableOASValidation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for item := range outputItems {
|
||||||
|
providers.EntryAdded()
|
||||||
|
|
||||||
|
extension := extensionsMap[item.Protocol.Name]
|
||||||
|
resolvedSource, resolvedDestionation := resolveIP(item.ConnectionInfo)
|
||||||
|
mizuEntry := extension.Dissector.Analyze(item, primitive.NewObjectID().Hex(), resolvedSource, resolvedDestionation)
|
||||||
|
baseEntry := extension.Dissector.Summarize(mizuEntry)
|
||||||
|
mizuEntry.EstimatedSizeBytes = getEstimatedEntrySizeBytes(mizuEntry)
|
||||||
|
if extension.Protocol.Name == "http" {
|
||||||
|
if !disableOASValidation {
|
||||||
|
var httpPair tapApi.HTTPRequestResponsePair
|
||||||
|
json.Unmarshal([]byte(mizuEntry.Entry), &httpPair)
|
||||||
|
|
||||||
|
contract := handleOAS(ctx, doc, router, httpPair.Request.Payload.RawRequest, httpPair.Response.Payload.RawResponse, contractContent)
|
||||||
|
baseEntry.ContractStatus = contract.Status
|
||||||
|
mizuEntry.ContractStatus = contract.Status
|
||||||
|
mizuEntry.ContractRequestReason = contract.RequestReason
|
||||||
|
mizuEntry.ContractResponseReason = contract.ResponseReason
|
||||||
|
mizuEntry.ContractContent = contract.Content
|
||||||
|
}
|
||||||
|
|
||||||
|
var pair tapApi.RequestResponsePair
|
||||||
|
json.Unmarshal([]byte(mizuEntry.Entry), &pair)
|
||||||
|
harEntry, err := utils.NewEntry(&pair)
|
||||||
|
if err == nil {
|
||||||
|
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Service)
|
||||||
|
baseEntry.Rules = rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
database.CreateEntry(mizuEntry)
|
||||||
|
|
||||||
|
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(baseEntry)
|
||||||
|
BroadcastToBrowserClients(baseEntryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, resolvedDestination string) {
|
||||||
|
if k8sResolver != nil {
|
||||||
|
unresolvedSource := connectionInfo.ClientIP
|
||||||
|
resolvedSource = k8sResolver.Resolve(unresolvedSource)
|
||||||
|
if resolvedSource == "" {
|
||||||
|
logger.Log.Debugf("Cannot find resolved name to source: %s\n", unresolvedSource)
|
||||||
|
if os.Getenv("SKIP_NOT_RESOLVED_SOURCE") == "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort)
|
||||||
|
resolvedDestination = k8sResolver.Resolve(unresolvedDestination)
|
||||||
|
if resolvedDestination == "" {
|
||||||
|
logger.Log.Debugf("Cannot find resolved name to dest: %s\n", unresolvedDestination)
|
||||||
|
if os.Getenv("SKIP_NOT_RESOLVED_DEST") == "1" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedSource, resolvedDestination
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIsServiceIP(address string) bool {
|
||||||
|
if k8sResolver == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return k8sResolver.CheckIsServiceIP(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// gives a rough estimate of the size this will take up in the db, good enough for maintaining db size limit accurately
|
||||||
|
func getEstimatedEntrySizeBytes(mizuEntry *tapApi.MizuEntry) int {
|
||||||
|
sizeBytes := len(mizuEntry.Entry)
|
||||||
|
sizeBytes += len(mizuEntry.EntryId)
|
||||||
|
sizeBytes += len(mizuEntry.Service)
|
||||||
|
sizeBytes += len(mizuEntry.Url)
|
||||||
|
sizeBytes += len(mizuEntry.Method)
|
||||||
|
sizeBytes += len(mizuEntry.RequestSenderIp)
|
||||||
|
sizeBytes += len(mizuEntry.ResolvedDestination)
|
||||||
|
sizeBytes += len(mizuEntry.ResolvedSource)
|
||||||
|
sizeBytes += 8 // Status bytes (sqlite integer is always 8 bytes)
|
||||||
|
sizeBytes += 8 // Timestamp bytes
|
||||||
|
sizeBytes += 8 // SizeBytes bytes
|
||||||
|
sizeBytes += 1 // IsOutgoing bytes
|
||||||
|
|
||||||
|
return sizeBytes
|
||||||
|
}
|
||||||
119
agent/pkg/api/socket_routes.go
Normal file
119
agent/pkg/api/socket_routes.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/up9inc/mizu/shared/debounce"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventHandlers interface {
|
||||||
|
WebSocketConnect(socketId int, isTapper bool)
|
||||||
|
WebSocketDisconnect(socketId int, isTapper bool)
|
||||||
|
WebSocketMessage(socketId int, message []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocketConnection struct {
|
||||||
|
connection *websocket.Conn
|
||||||
|
lock *sync.Mutex
|
||||||
|
eventHandlers EventHandlers
|
||||||
|
isTapper bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var websocketUpgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
}
|
||||||
|
|
||||||
|
var websocketIdsLock = sync.Mutex{}
|
||||||
|
var connectedWebsockets map[int]*SocketConnection
|
||||||
|
var connectedWebsocketIdCounter = 0
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
websocketUpgrader.CheckOrigin = func(r *http.Request) bool { return true } // like cors for web socket
|
||||||
|
connectedWebsockets = make(map[int]*SocketConnection, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
|
||||||
|
app.GET("/ws", func(c *gin.Context) {
|
||||||
|
websocketHandler(c.Writer, c.Request, eventHandlers, false)
|
||||||
|
})
|
||||||
|
app.GET("/wsTapper", func(c *gin.Context) {
|
||||||
|
websocketHandler(c.Writer, c.Request, eventHandlers, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers EventHandlers, isTapper bool) {
|
||||||
|
conn, err := websocketUpgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed to set websocket upgrade: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketIdsLock.Lock()
|
||||||
|
|
||||||
|
connectedWebsocketIdCounter++
|
||||||
|
socketId := connectedWebsocketIdCounter
|
||||||
|
connectedWebsockets[socketId] = &SocketConnection{connection: conn, lock: &sync.Mutex{}, eventHandlers: eventHandlers, isTapper: isTapper}
|
||||||
|
|
||||||
|
websocketIdsLock.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
socketCleanup(socketId, connectedWebsockets[socketId])
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventHandlers.WebSocketConnect(socketId, isTapper)
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error reading message, socket id: %d, error: %v", socketId, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
eventHandlers.WebSocketMessage(socketId, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func socketCleanup(socketId int, socketConnection *SocketConnection) {
|
||||||
|
err := socketConnection.connection.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error closing socket connection for socket id %d: %v\n", socketId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
websocketIdsLock.Lock()
|
||||||
|
connectedWebsockets[socketId] = nil
|
||||||
|
websocketIdsLock.Unlock()
|
||||||
|
|
||||||
|
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = debounce.NewDebouncer(time.Second*5, func() {
|
||||||
|
logger.Log.Error("Successfully sent to socket")
|
||||||
|
})
|
||||||
|
|
||||||
|
func SendToSocket(socketId int, message []byte) error {
|
||||||
|
socketObj := connectedWebsockets[socketId]
|
||||||
|
if socketObj == nil {
|
||||||
|
return errors.New("Socket is disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sent = false
|
||||||
|
time.AfterFunc(time.Second*5, func() {
|
||||||
|
if !sent {
|
||||||
|
logger.Log.Error("Socket timed out")
|
||||||
|
socketCleanup(socketId, socketObj)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socketObj.lock.Lock() // gorilla socket panics from concurrent writes to a single socket
|
||||||
|
err := socketObj.connection.WriteMessage(1, message)
|
||||||
|
socketObj.lock.Unlock()
|
||||||
|
|
||||||
|
sent = true
|
||||||
|
return err
|
||||||
|
}
|
||||||
134
agent/pkg/api/socket_server_handlers.go
Normal file
134
agent/pkg/api/socket_server_handlers.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"mizuserver/pkg/up9"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var browserClientSocketUUIDs = make([]int, 0)
|
||||||
|
var socketListLock = sync.Mutex{}
|
||||||
|
|
||||||
|
type RoutesEventHandlers struct {
|
||||||
|
EventHandlers
|
||||||
|
SocketOutChannel chan<- *tapApi.OutputChannelItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
|
||||||
|
if isTapper {
|
||||||
|
logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
|
||||||
|
providers.TapperAdded()
|
||||||
|
} else {
|
||||||
|
logger.Log.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
|
||||||
|
socketListLock.Lock()
|
||||||
|
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId)
|
||||||
|
socketListLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
|
||||||
|
if isTapper {
|
||||||
|
logger.Log.Infof("Websocket event - Tapper disconnected, socket ID: %d", socketId)
|
||||||
|
providers.TapperRemoved()
|
||||||
|
} else {
|
||||||
|
logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
|
||||||
|
socketListLock.Lock()
|
||||||
|
removeSocketUUIDFromBrowserSlice(socketId)
|
||||||
|
socketListLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastToBrowserClients(message []byte) {
|
||||||
|
for _, socketId := range browserClientSocketUUIDs {
|
||||||
|
go func(socketId int) {
|
||||||
|
err := SendToSocket(socketId, message)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("error sending message to socket ID %d: %v", socketId, err)
|
||||||
|
}
|
||||||
|
}(socketId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
|
||||||
|
var socketMessageBase shared.WebSocketMessageMetadata
|
||||||
|
err := json.Unmarshal(message, &socketMessageBase)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("Could not unmarshal websocket message %v\n", err)
|
||||||
|
} else {
|
||||||
|
switch socketMessageBase.MessageType {
|
||||||
|
case shared.WebSocketMessageTypeTappedEntry:
|
||||||
|
var tappedEntryMessage models.WebSocketTappedEntryMessage
|
||||||
|
err := json.Unmarshal(message, &tappedEntryMessage)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
// NOTE: This is where the message comes back from the intermediate WebSocket to code.
|
||||||
|
h.SocketOutChannel <- tappedEntryMessage.Data
|
||||||
|
}
|
||||||
|
case shared.WebSocketMessageTypeUpdateStatus:
|
||||||
|
var statusMessage shared.WebSocketStatusMessage
|
||||||
|
err := json.Unmarshal(message, &statusMessage)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
providers.TapStatus.Pods = statusMessage.TappingStatus.Pods
|
||||||
|
BroadcastToBrowserClients(message)
|
||||||
|
}
|
||||||
|
case shared.WebsocketMessageTypeOutboundLink:
|
||||||
|
var outboundLinkMessage models.WebsocketOutboundLinkMessage
|
||||||
|
err := json.Unmarshal(message, &outboundLinkMessage)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
||||||
|
} else {
|
||||||
|
handleTLSLink(outboundLinkMessage)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.Log.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTLSLink(outboundLinkMessage models.WebsocketOutboundLinkMessage) {
|
||||||
|
resolvedName := k8sResolver.Resolve(outboundLinkMessage.Data.DstIP)
|
||||||
|
if resolvedName != "" {
|
||||||
|
outboundLinkMessage.Data.DstIP = resolvedName
|
||||||
|
} else if outboundLinkMessage.Data.SuggestedResolvedName != "" {
|
||||||
|
outboundLinkMessage.Data.DstIP = outboundLinkMessage.Data.SuggestedResolvedName
|
||||||
|
}
|
||||||
|
cacheKey := fmt.Sprintf("%s -> %s:%d", outboundLinkMessage.Data.Src, outboundLinkMessage.Data.DstIP, outboundLinkMessage.Data.DstPort)
|
||||||
|
_, isInCache := providers.RecentTLSLinks.Get(cacheKey)
|
||||||
|
if isInCache {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
providers.RecentTLSLinks.SetDefault(cacheKey, outboundLinkMessage.Data)
|
||||||
|
}
|
||||||
|
marshaledMessage, err := json.Marshal(outboundLinkMessage)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error marshaling outbound link message for broadcasting: %v", err)
|
||||||
|
} else {
|
||||||
|
logger.Log.Errorf("Broadcasting outboundlink message %s", string(marshaledMessage))
|
||||||
|
BroadcastToBrowserClients(marshaledMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeSocketUUIDFromBrowserSlice(uuidToRemove int) {
|
||||||
|
newUUIDSlice := make([]int, 0, len(browserClientSocketUUIDs))
|
||||||
|
for _, uuid := range browserClientSocketUUIDs {
|
||||||
|
if uuid != uuidToRemove {
|
||||||
|
newUUIDSlice = append(newUUIDSlice, uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
browserClientSocketUUIDs = newUUIDSlice
|
||||||
|
}
|
||||||
96
agent/pkg/controllers/entries_controller.go
Normal file
96
agent/pkg/controllers/entries_controller.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"mizuserver/pkg/validation"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var extensionsMap map[string]*tapApi.Extension // global
|
||||||
|
|
||||||
|
func InitExtensionsMap(ref map[string]*tapApi.Extension) {
|
||||||
|
extensionsMap = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntries(c *gin.Context) {
|
||||||
|
entriesFilter := &models.EntriesFilter{}
|
||||||
|
|
||||||
|
if err := c.BindQuery(entriesFilter); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
err := validation.Validate(entriesFilter)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
order := database.OperatorToOrderMapping[entriesFilter.Operator]
|
||||||
|
operatorSymbol := database.OperatorToSymbolMapping[entriesFilter.Operator]
|
||||||
|
var entries []tapApi.MizuEntry
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Order(fmt.Sprintf("timestamp %s", order)).
|
||||||
|
Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
||||||
|
Limit(entriesFilter.Limit).
|
||||||
|
Find(&entries)
|
||||||
|
|
||||||
|
if len(entries) > 0 && order == database.OrderDesc {
|
||||||
|
// the entries always order from oldest to newest - we should reverse
|
||||||
|
utils.ReverseSlice(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseEntries := make([]tapApi.BaseEntryDetails, 0)
|
||||||
|
for _, entry := range entries {
|
||||||
|
baseEntryDetails := tapApi.BaseEntryDetails{}
|
||||||
|
if err := models.GetEntry(&entry, &baseEntryDetails); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var pair tapApi.RequestResponsePair
|
||||||
|
json.Unmarshal([]byte(entry.Entry), &pair)
|
||||||
|
harEntry, err := utils.NewEntry(&pair)
|
||||||
|
if err == nil {
|
||||||
|
rules, _, _ := models.RunValidationRulesState(*harEntry, entry.Service)
|
||||||
|
baseEntryDetails.Rules = rules
|
||||||
|
}
|
||||||
|
|
||||||
|
baseEntries = append(baseEntries, baseEntryDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, baseEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntry(c *gin.Context) {
|
||||||
|
var entryData tapApi.MizuEntry
|
||||||
|
database.GetEntriesTable().
|
||||||
|
Where(map[string]string{"entryId": c.Param("entryId")}).
|
||||||
|
First(&entryData)
|
||||||
|
|
||||||
|
extension := extensionsMap[entryData.ProtocolName]
|
||||||
|
protocol, representation, bodySize, _ := extension.Dissector.Represent(&entryData)
|
||||||
|
|
||||||
|
var rules []map[string]interface{}
|
||||||
|
var isRulesEnabled bool
|
||||||
|
if entryData.ProtocolName == "http" {
|
||||||
|
var pair tapApi.RequestResponsePair
|
||||||
|
json.Unmarshal([]byte(entryData.Entry), &pair)
|
||||||
|
harEntry, _ := utils.NewEntry(&pair)
|
||||||
|
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entryData.Service)
|
||||||
|
isRulesEnabled = _isRulesEnabled
|
||||||
|
inrec, _ := json.Marshal(rulesMatched)
|
||||||
|
json.Unmarshal(inrec, &rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, tapApi.MizuEntryWrapper{
|
||||||
|
Protocol: protocol,
|
||||||
|
Representation: string(representation),
|
||||||
|
BodySize: bodySize,
|
||||||
|
Data: entryData,
|
||||||
|
Rules: rules,
|
||||||
|
IsRulesEnabled: isRulesEnabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
13
agent/pkg/controllers/metadata_controller.go
Normal file
13
agent/pkg/controllers/metadata_controller.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"mizuserver/pkg/version"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetVersion(c *gin.Context) {
|
||||||
|
resp := shared.VersionResponse{SemVer: version.SemVer}
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
69
agent/pkg/controllers/status_controller.go
Normal file
69
agent/pkg/controllers/status_controller.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"mizuserver/pkg/api"
|
||||||
|
"mizuserver/pkg/holder"
|
||||||
|
"mizuserver/pkg/providers"
|
||||||
|
"mizuserver/pkg/up9"
|
||||||
|
"mizuserver/pkg/validation"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PostTappedPods(c *gin.Context) {
|
||||||
|
tapStatus := &shared.TapStatus{}
|
||||||
|
if err := c.Bind(tapStatus); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := validation.Validate(tapStatus); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log.Infof("[Status] POST request: %d tapped pods", len(tapStatus.Pods))
|
||||||
|
providers.TapStatus.Pods = tapStatus.Pods
|
||||||
|
message := shared.CreateWebSocketStatusMessage(*tapStatus)
|
||||||
|
if jsonBytes, err := json.Marshal(message); err != nil {
|
||||||
|
logger.Log.Errorf("Could not Marshal message %v\n", err)
|
||||||
|
} else {
|
||||||
|
api.BroadcastToBrowserClients(jsonBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTappersCount(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.TappersCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAuthStatus(c *gin.Context) {
|
||||||
|
authStatus, err := providers.GetAuthStatus()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, authStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTappingStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.TapStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnalyzeInformation(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, up9.GetAnalyzeInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGeneralStats(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.GetGeneralStats())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRecentTLSLinks(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, providers.GetAllRecentTLSAddresses())
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentResolvingInformation(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, holder.GetResolver().GetMap())
|
||||||
|
}
|
||||||
79
agent/pkg/database/main.go
Normal file
79
agent/pkg/database/main.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DBPath = "./entries.db"
|
||||||
|
OrderDesc = "desc"
|
||||||
|
OrderAsc = "asc"
|
||||||
|
LT = "lt"
|
||||||
|
GT = "gt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DB *gorm.DB
|
||||||
|
IsDBLocked = false
|
||||||
|
OperatorToSymbolMapping = map[string]string{
|
||||||
|
LT: "<",
|
||||||
|
GT: ">",
|
||||||
|
}
|
||||||
|
OperatorToOrderMapping = map[string]string{
|
||||||
|
LT: OrderDesc,
|
||||||
|
GT: OrderAsc,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
DB = initDataBase(DBPath)
|
||||||
|
go StartEnforcingDatabaseSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntriesTable() *gorm.DB {
|
||||||
|
return DB.Table("mizu_entries")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateEntry(entry *tapApi.MizuEntry) {
|
||||||
|
if IsDBLocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GetEntriesTable().Create(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDataBase(databasePath string) *gorm.DB {
|
||||||
|
temp, _ := gorm.Open(sqlite.Open(databasePath), &gorm.Config{
|
||||||
|
Logger: &utils.TruncatingLogger{LogLevel: logger.Warn, SlowThreshold: 500 * time.Millisecond},
|
||||||
|
})
|
||||||
|
_ = temp.AutoMigrate(&tapApi.MizuEntry{}) // this will ensure table is created
|
||||||
|
return temp
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEntriesFromDb(timestampFrom int64, timestampTo int64, protocolName *string) []tapApi.MizuEntry {
|
||||||
|
order := OrderDesc
|
||||||
|
protocolNameCondition := "1 = 1"
|
||||||
|
if protocolName != nil {
|
||||||
|
protocolNameCondition = fmt.Sprintf("protocolName = '%s'", *protocolName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []tapApi.MizuEntry
|
||||||
|
GetEntriesTable().
|
||||||
|
Where(protocolNameCondition).
|
||||||
|
Where(fmt.Sprintf("timestamp BETWEEN %v AND %v", timestampFrom, timestampTo)).
|
||||||
|
Order(fmt.Sprintf("timestamp %s", order)).
|
||||||
|
Find(&entries)
|
||||||
|
|
||||||
|
if len(entries) > 0 {
|
||||||
|
// the entries always order from oldest to newest so we should revers
|
||||||
|
utils.ReverseSlice(entries)
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
121
agent/pkg/database/size_enforcer.go
Normal file
121
agent/pkg/database/size_enforcer.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/debounce"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"github.com/up9inc/mizu/shared/units"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const percentageOfMaxSizeBytesToPrune = 15
|
||||||
|
const defaultMaxDatabaseSizeBytes int64 = 200 * 1000 * 1000
|
||||||
|
|
||||||
|
func StartEnforcingDatabaseSize() {
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Fatalf("Error creating filesystem watcher for db size enforcement: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxEntriesDBByteSize, err := getMaxEntriesDBByteSize()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Fatalf("Error parsing max db size: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFileSizeDebouncer := debounce.NewDebouncer(5*time.Second, func() {
|
||||||
|
checkFileSize(maxEntriesDBByteSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return // closed channel
|
||||||
|
}
|
||||||
|
if event.Op == fsnotify.Write {
|
||||||
|
checkFileSizeDebouncer.SetOn()
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return // closed channel
|
||||||
|
}
|
||||||
|
logger.Log.Errorf("filesystem watcher encountered error:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = watcher.Add(DBPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Fatalf("Error adding %s to filesystem watcher for db size enforcement: %v\n", DBPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMaxEntriesDBByteSize() (int64, error) {
|
||||||
|
maxEntriesDBByteSize := defaultMaxDatabaseSizeBytes
|
||||||
|
var err error
|
||||||
|
|
||||||
|
maxEntriesDBSizeByteSEnvVarValue := os.Getenv(shared.MaxEntriesDBSizeBytesEnvVar)
|
||||||
|
if maxEntriesDBSizeByteSEnvVarValue != "" {
|
||||||
|
maxEntriesDBByteSize, err = strconv.ParseInt(maxEntriesDBSizeByteSEnvVarValue, 10, 64)
|
||||||
|
}
|
||||||
|
return maxEntriesDBByteSize, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFileSize(maxSizeBytes int64) {
|
||||||
|
fileStat, err := os.Stat(DBPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error checking %s file size: %v", DBPath, err)
|
||||||
|
} else {
|
||||||
|
if fileStat.Size() > maxSizeBytes {
|
||||||
|
pruneOldEntries(fileStat.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneOldEntries(currentFileSize int64) {
|
||||||
|
// sqlite locks the database while delete or VACUUM are running and sqlite is terrible at handling its own db lock while a lot of inserts are attempted, we prevent a significant bottleneck by handling the db lock ourselves here
|
||||||
|
IsDBLocked = true
|
||||||
|
defer func() { IsDBLocked = false }()
|
||||||
|
|
||||||
|
amountOfBytesToTrim := currentFileSize / (100 / percentageOfMaxSizeBytesToPrune)
|
||||||
|
|
||||||
|
rows, err := GetEntriesTable().Limit(10000).Order("id").Rows()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error getting 10000 first db rows: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIdsToRemove := make([]uint, 0)
|
||||||
|
bytesToBeRemoved := int64(0)
|
||||||
|
for rows.Next() {
|
||||||
|
if bytesToBeRemoved >= amountOfBytesToTrim {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var entry tapApi.MizuEntry
|
||||||
|
err = DB.ScanRows(rows, &entry)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Error scanning db row: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entryIdsToRemove = append(entryIdsToRemove, entry.ID)
|
||||||
|
bytesToBeRemoved += int64(entry.EstimatedSizeBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entryIdsToRemove) > 0 {
|
||||||
|
GetEntriesTable().Where(entryIdsToRemove).Delete(tapApi.MizuEntry{})
|
||||||
|
// VACUUM causes sqlite to shrink the db file after rows have been deleted, the db file will not shrink without this
|
||||||
|
DB.Exec("VACUUM")
|
||||||
|
logger.Log.Errorf("Removed %d rows and cleared %s", len(entryIdsToRemove), units.BytesToHumanReadable(bytesToBeRemoved))
|
||||||
|
} else {
|
||||||
|
logger.Log.Error("Found no rows to remove when pruning")
|
||||||
|
}
|
||||||
|
}
|
||||||
14
agent/pkg/holder/main.go
Normal file
14
agent/pkg/holder/main.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package holder
|
||||||
|
|
||||||
|
import "mizuserver/pkg/resolver"
|
||||||
|
|
||||||
|
var k8sResolver *resolver.Resolver
|
||||||
|
|
||||||
|
func SetResolver(param *resolver.Resolver) {
|
||||||
|
k8sResolver = param
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetResolver() *resolver.Resolver {
|
||||||
|
return k8sResolver
|
||||||
|
}
|
||||||
|
|
||||||
99
agent/pkg/models/models.go
Normal file
99
agent/pkg/models/models.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
|
||||||
|
"mizuserver/pkg/rules"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetEntry(r *tapApi.MizuEntry, v tapApi.DataUnmarshaler) error {
|
||||||
|
return v.UnmarshalData(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EntriesFilter struct {
|
||||||
|
Limit int `form:"limit" validate:"required,min=1,max=200"`
|
||||||
|
Operator string `form:"operator" validate:"required,oneof='lt' 'gt'"`
|
||||||
|
Timestamp int64 `form:"timestamp" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketEntryMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tapApi.BaseEntryDetails `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketTappedEntryMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tapApi.OutputChannelItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsocketOutboundLinkMessage struct {
|
||||||
|
*shared.WebSocketMessageMetadata
|
||||||
|
Data *tap.OutboundLink
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthStatus struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateBaseEntryWebSocketMessage(base *tapApi.BaseEntryDetails) ([]byte, error) {
|
||||||
|
message := &WebSocketEntryMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebSocketMessageTypeEntry,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWebsocketTappedEntryMessage(base *tapApi.OutputChannelItem) ([]byte, error) {
|
||||||
|
message := &WebSocketTappedEntryMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebSocketMessageTypeTappedEntry,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWebsocketOutboundLinkMessage(base *tap.OutboundLink) ([]byte, error) {
|
||||||
|
message := &WebsocketOutboundLinkMessage{
|
||||||
|
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
||||||
|
MessageType: shared.WebsocketMessageTypeOutboundLink,
|
||||||
|
},
|
||||||
|
Data: base,
|
||||||
|
}
|
||||||
|
return json.Marshal(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedHAR is the top level object of a HAR log.
|
||||||
|
type ExtendedHAR struct {
|
||||||
|
Log *ExtendedLog `json:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedLog is the HAR HTTP request and response log.
|
||||||
|
type ExtendedLog struct {
|
||||||
|
// Version number of the HAR format.
|
||||||
|
Version string `json:"version"`
|
||||||
|
// Creator holds information about the log creator application.
|
||||||
|
Creator *ExtendedCreator `json:"creator"`
|
||||||
|
// Entries is a list containing requests and responses.
|
||||||
|
Entries []*har.Entry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExtendedCreator struct {
|
||||||
|
*har.Creator
|
||||||
|
Source *string `json:"_source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunValidationRulesState(harEntry har.Entry, service string) (tapApi.ApplicableRules, []rules.RulesMatched, bool) {
|
||||||
|
resultPolicyToSend, isEnabled := rules.MatchRequestPolicy(harEntry, service)
|
||||||
|
statusPolicyToSend, latency, numberOfRules := rules.PassedValidationRules(resultPolicyToSend)
|
||||||
|
return tapApi.ApplicableRules{Status: statusPolicyToSend, Latency: latency, NumberOfRules: numberOfRules}, resultPolicyToSend, isEnabled
|
||||||
|
}
|
||||||
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
82
agent/pkg/providers/status_provider.go
Normal file
82
agent/pkg/providers/status_provider.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/tap"
|
||||||
|
"mizuserver/pkg/models"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tlsLinkRetainmentTime = time.Minute * 15
|
||||||
|
|
||||||
|
var (
|
||||||
|
TappersCount int
|
||||||
|
TapStatus shared.TapStatus
|
||||||
|
authStatus *models.AuthStatus
|
||||||
|
RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime)
|
||||||
|
|
||||||
|
tappersCountLock = sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAuthStatus() (*models.AuthStatus, error) {
|
||||||
|
if authStatus == nil {
|
||||||
|
syncEntriesConfigJson := os.Getenv(shared.SyncEntriesConfigEnvVar)
|
||||||
|
if syncEntriesConfigJson == "" {
|
||||||
|
authStatus = &models.AuthStatus{}
|
||||||
|
return authStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
syncEntriesConfig := &shared.SyncEntriesConfig{}
|
||||||
|
err := json.Unmarshal([]byte(syncEntriesConfigJson), syncEntriesConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal sync entries config, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if syncEntriesConfig.Token == "" {
|
||||||
|
authStatus = &models.AuthStatus{}
|
||||||
|
return authStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenEmail, err := shared.GetTokenEmail(syncEntriesConfig.Token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get token email, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authStatus = &models.AuthStatus{
|
||||||
|
Email: tokenEmail,
|
||||||
|
Model: syncEntriesConfig.Workspace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllRecentTLSAddresses() []string {
|
||||||
|
recentTLSLinks := make([]string, 0)
|
||||||
|
|
||||||
|
for _, outboundLinkItem := range RecentTLSLinks.Items() {
|
||||||
|
outboundLink, castOk := outboundLinkItem.Object.(*tap.OutboundLink)
|
||||||
|
if castOk {
|
||||||
|
recentTLSLinks = append(recentTLSLinks, outboundLink.DstIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentTLSLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
func TapperAdded() {
|
||||||
|
tappersCountLock.Lock()
|
||||||
|
TappersCount++
|
||||||
|
tappersCountLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TapperRemoved() {
|
||||||
|
tappersCountLock.Lock()
|
||||||
|
TappersCount--
|
||||||
|
tappersCountLock.Unlock()
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ Now you will be able to import `github.com/up9inc/mizu/resolver` in any `.go` fi
|
|||||||
errOut := make(chan error, 100)
|
errOut := make(chan error, 100)
|
||||||
k8sResolver, err := resolver.NewFromOutOfCluster("", errOut)
|
k8sResolver, err := resolver.NewFromOutOfCluster("", errOut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error creating k8s resolver %s", err)
|
logger.Log.Errorf("error creating k8s resolver %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -40,15 +40,15 @@ k8sResolver.Start(ctx)
|
|||||||
|
|
||||||
resolvedName := k8sResolver.Resolve("10.107.251.91") // will always return `nil` in real scenarios as the internal map takes a moment to populate after `Start` is called
|
resolvedName := k8sResolver.Resolve("10.107.251.91") // will always return `nil` in real scenarios as the internal map takes a moment to populate after `Start` is called
|
||||||
if resolvedName != nil {
|
if resolvedName != nil {
|
||||||
fmt.Printf("resolved 10.107.251.91=%s", *resolvedName)
|
logger.Log.Errorf("resolved 10.107.251.91=%s", *resolvedName)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Could not find a resolved name for 10.107.251.91")
|
logger.Log.Error("Could not find a resolved name for 10.107.251.91")
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case err := <- errOut:
|
case err := <- errOut:
|
||||||
fmt.Printf("name resolving error %s", err)
|
logger.Log.Errorf("name resolving error %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
23
agent/pkg/resolver/loader.go
Normal file
23
agent/pkg/resolver/loader.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||||
|
_ "k8s.io/client-go/plugin/pkg/client/auth/openstack"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFromInCluster(errOut chan error, namesapce string) (*Resolver, error) {
|
||||||
|
config, err := restclient.InClusterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientset, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Resolver{clientConfig: config, clientSet: clientset, nameMap: cmap.New(), serviceMap: cmap.New(), errOut: errOut, namespace: namesapce}, nil
|
||||||
|
}
|
||||||
200
agent/pkg/resolver/resolver.go
Normal file
200
agent/pkg/resolver/resolver.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
restclient "k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kubClientNullString = "None"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver struct {
|
||||||
|
clientConfig *restclient.Config
|
||||||
|
clientSet *kubernetes.Clientset
|
||||||
|
nameMap cmap.ConcurrentMap
|
||||||
|
serviceMap cmap.ConcurrentMap
|
||||||
|
isStarted bool
|
||||||
|
errOut chan error
|
||||||
|
namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) Start(ctx context.Context) {
|
||||||
|
if !resolver.isStarted {
|
||||||
|
resolver.isStarted = true
|
||||||
|
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchServices)
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchEndpoints)
|
||||||
|
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchPods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) Resolve(name string) string {
|
||||||
|
resolvedName, isFound := resolver.nameMap.Get(name)
|
||||||
|
if !isFound {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resolvedName.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) GetMap() cmap.ConcurrentMap {
|
||||||
|
return resolver.nameMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) CheckIsServiceIP(address string) bool {
|
||||||
|
_, isFound := resolver.serviceMap.Get(address)
|
||||||
|
return isFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchPods(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Pods(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl pod watch")
|
||||||
|
}
|
||||||
|
if event.Type == watch.Deleted {
|
||||||
|
pod := event.Object.(*corev1.Pod)
|
||||||
|
resolver.saveResolvedName(pod.Status.PodIP, "", event.Type)
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchEndpoints(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Endpoints(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl endpoint watch")
|
||||||
|
}
|
||||||
|
endpoint := event.Object.(*corev1.Endpoints)
|
||||||
|
serviceHostname := fmt.Sprintf("%s.%s", endpoint.Name, endpoint.Namespace)
|
||||||
|
if endpoint.Subsets != nil {
|
||||||
|
for _, subset := range endpoint.Subsets {
|
||||||
|
var ports []int32
|
||||||
|
if subset.Ports != nil {
|
||||||
|
for _, portMapping := range subset.Ports {
|
||||||
|
if portMapping.Port > 0 {
|
||||||
|
ports = append(ports, portMapping.Port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subset.Addresses != nil {
|
||||||
|
for _, address := range subset.Addresses {
|
||||||
|
resolver.saveResolvedName(address.IP, serviceHostname, event.Type)
|
||||||
|
for _, port := range ports {
|
||||||
|
ipWithPort := fmt.Sprintf("%s:%d", address.IP, port)
|
||||||
|
resolver.saveResolvedName(ipWithPort, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) watchServices(ctx context.Context) error {
|
||||||
|
// empty namespace makes the client watch all namespaces
|
||||||
|
watcher, err := resolver.clientSet.CoreV1().Services(resolver.namespace).Watch(ctx, metav1.ListOptions{Watch: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-watcher.ResultChan():
|
||||||
|
if event.Object == nil {
|
||||||
|
return errors.New("error in kubectl service watch")
|
||||||
|
}
|
||||||
|
|
||||||
|
service := event.Object.(*corev1.Service)
|
||||||
|
serviceHostname := fmt.Sprintf("%s.%s", service.Name, service.Namespace)
|
||||||
|
if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != kubClientNullString {
|
||||||
|
resolver.saveResolvedName(service.Spec.ClusterIP, serviceHostname, event.Type)
|
||||||
|
if service.Spec.Ports != nil {
|
||||||
|
for _, port := range service.Spec.Ports {
|
||||||
|
if port.Port > 0 {
|
||||||
|
resolver.saveResolvedName(fmt.Sprintf("%s:%d", service.Spec.ClusterIP, port.Port), serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolver.saveServiceIP(service.Spec.ClusterIP, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
if service.Status.LoadBalancer.Ingress != nil {
|
||||||
|
for _, ingress := range service.Status.LoadBalancer.Ingress {
|
||||||
|
resolver.saveResolvedName(ingress.IP, serviceHostname, event.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
watcher.Stop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) saveResolvedName(key string, resolved string, eventType watch.EventType) {
|
||||||
|
if eventType == watch.Deleted {
|
||||||
|
resolver.nameMap.Remove(key)
|
||||||
|
logger.Log.Infof("setting %s=nil\n", key)
|
||||||
|
} else {
|
||||||
|
resolver.nameMap.Set(key, resolved)
|
||||||
|
logger.Log.Infof("setting %s=%s\n", key, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) saveServiceIP(key string, resolved string, eventType watch.EventType) {
|
||||||
|
if eventType == watch.Deleted {
|
||||||
|
resolver.serviceMap.Remove(key)
|
||||||
|
} else {
|
||||||
|
resolver.serviceMap.Set(key, resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resolver *Resolver) infiniteErrorHandleRetryFunc(ctx context.Context, fun func(ctx context.Context) error) {
|
||||||
|
for {
|
||||||
|
err := fun(ctx)
|
||||||
|
if err != nil {
|
||||||
|
resolver.errOut <- err
|
||||||
|
|
||||||
|
var statusError *k8serrors.StatusError
|
||||||
|
if errors.As(err, &statusError) {
|
||||||
|
if statusError.ErrStatus.Reason == metav1.StatusReasonForbidden {
|
||||||
|
logger.Log.Infof("Resolver loop encountered permission error, aborting event listening - %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil { // context was cancelled or errored
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
agent/pkg/routes/entries_routes.go
Normal file
14
agent/pkg/routes/entries_routes.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EntriesRoutes defines the group of har entries routes.
|
||||||
|
func EntriesRoutes(ginApp *gin.Engine) {
|
||||||
|
routeGroup := ginApp.Group("/entries")
|
||||||
|
|
||||||
|
routeGroup.GET("/", controllers.GetEntries) // get entries (base/thin entries)
|
||||||
|
routeGroup.GET("/:entryId", controllers.GetEntry) // get single (full) entry
|
||||||
|
}
|
||||||
13
agent/pkg/routes/metadata_routes.go
Normal file
13
agent/pkg/routes/metadata_routes.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetadataRoutes defines the group of metadata routes.
|
||||||
|
func MetadataRoutes(app *gin.Engine) {
|
||||||
|
routeGroup := app.Group("/metadata")
|
||||||
|
|
||||||
|
routeGroup.GET("/version", controllers.GetVersion)
|
||||||
|
}
|
||||||
18
agent/pkg/routes/not_found_route.go
Normal file
18
agent/pkg/routes/not_found_route.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotFoundRoute defines the 404 Error route.
|
||||||
|
func NotFoundRoute(app *gin.Engine) {
|
||||||
|
app.Use(
|
||||||
|
func(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusNotFound, map[string]interface{}{
|
||||||
|
"error": true,
|
||||||
|
"msg": "sorry, endpoint is not found",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
24
agent/pkg/routes/status_routes.go
Normal file
24
agent/pkg/routes/status_routes.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"mizuserver/pkg/controllers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StatusRoutes(ginApp *gin.Engine) {
|
||||||
|
routeGroup := ginApp.Group("/status")
|
||||||
|
|
||||||
|
routeGroup.POST("/tappedPods", controllers.PostTappedPods)
|
||||||
|
routeGroup.GET("/tappersCount", controllers.GetTappersCount)
|
||||||
|
routeGroup.GET("/tap", controllers.GetTappingStatus)
|
||||||
|
|
||||||
|
routeGroup.GET("/auth", controllers.GetAuthStatus)
|
||||||
|
|
||||||
|
routeGroup.GET("/analyze", controllers.AnalyzeInformation)
|
||||||
|
|
||||||
|
routeGroup.GET("/general", controllers.GetGeneralStats) // get general stats about entries in DB
|
||||||
|
|
||||||
|
routeGroup.GET("/recentTLSLinks", controllers.GetRecentTLSLinks)
|
||||||
|
|
||||||
|
routeGroup.GET("/resolving", controllers.GetCurrentResolvingInformation)
|
||||||
|
}
|
||||||
123
agent/pkg/rules/rulesHTTP.go
Normal file
123
agent/pkg/rules/rulesHTTP.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
jsonpath "github.com/yalp/jsonpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RulesMatched struct {
|
||||||
|
Matched bool `json:"matched"`
|
||||||
|
Rule shared.RulePolicy `json:"rule"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendRulesMatched(rulesMatched []RulesMatched, matched bool, rule shared.RulePolicy) []RulesMatched {
|
||||||
|
return append(rulesMatched, RulesMatched{Matched: matched, Rule: rule})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidatePath(URLFromRule string, URL string) bool {
|
||||||
|
if URLFromRule != "" {
|
||||||
|
matchPath, err := regexp.MatchString(URLFromRule, URL)
|
||||||
|
if err != nil || !matchPath {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateService(serviceFromRule string, service string) bool {
|
||||||
|
if serviceFromRule != "" {
|
||||||
|
matchService, err := regexp.MatchString(serviceFromRule, service)
|
||||||
|
if err != nil || !matchService {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchRequestPolicy(harEntry har.Entry, service string) (resultPolicyToSend []RulesMatched, isEnabled bool) {
|
||||||
|
enforcePolicy, err := shared.DecodeEnforcePolicy(fmt.Sprintf("%s/%s", shared.RulePolicyPath, shared.RulePolicyFileName))
|
||||||
|
if err == nil && len(enforcePolicy.Rules) > 0 {
|
||||||
|
isEnabled = true
|
||||||
|
}
|
||||||
|
for _, rule := range enforcePolicy.Rules {
|
||||||
|
if !ValidatePath(rule.Path, harEntry.Request.URL) || !ValidateService(rule.Service, service) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rule.Type == "json" {
|
||||||
|
var bodyJsonMap interface{}
|
||||||
|
contentTextDecoded, _ := base64.StdEncoding.DecodeString(string(harEntry.Response.Content.Text))
|
||||||
|
if err := json.Unmarshal(contentTextDecoded, &bodyJsonMap); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, err := jsonpath.Read(bodyJsonMap, rule.Key)
|
||||||
|
if err != nil || out == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var matchValue bool
|
||||||
|
if reflect.TypeOf(out).Kind() == reflect.String {
|
||||||
|
matchValue, err = regexp.MatchString(rule.Value, out.(string))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Log.Info(matchValue, rule.Value)
|
||||||
|
} else {
|
||||||
|
val := fmt.Sprint(out)
|
||||||
|
matchValue, err = regexp.MatchString(rule.Value, val)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule)
|
||||||
|
} else if rule.Type == "header" {
|
||||||
|
for j := range harEntry.Response.Headers {
|
||||||
|
matchKey, err := regexp.MatchString(rule.Key, harEntry.Response.Headers[j].Name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchKey {
|
||||||
|
matchValue, err := regexp.MatchString(rule.Value, harEntry.Response.Headers[j].Value)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultPolicyToSend = appendRulesMatched(resultPolicyToSend, true, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func PassedValidationRules(rulesMatched []RulesMatched) (bool, int64, int) {
|
||||||
|
var numberOfRulesMatched = len(rulesMatched)
|
||||||
|
var responseTime int64 = -1
|
||||||
|
|
||||||
|
if numberOfRulesMatched == 0 {
|
||||||
|
return false, 0, numberOfRulesMatched
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range rulesMatched {
|
||||||
|
if rule.Matched == false {
|
||||||
|
return false, responseTime, numberOfRulesMatched
|
||||||
|
} else {
|
||||||
|
if strings.ToLower(rule.Rule.Type) == "slo" {
|
||||||
|
if rule.Rule.ResponseTime < responseTime || responseTime == -1 {
|
||||||
|
responseTime = rule.Rule.ResponseTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, responseTime, numberOfRulesMatched
|
||||||
|
}
|
||||||
302
agent/pkg/up9/main.go
Normal file
302
agent/pkg/up9/main.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package up9
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/zlib"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"mizuserver/pkg/database"
|
||||||
|
"mizuserver/pkg/utils"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
tapApi "github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AnalyzeCheckSleepTime = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type GuestToken struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelStatus struct {
|
||||||
|
LastMajorGeneration float64 `json:"lastMajorGeneration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRemoteUrl(analyzeDestination string, analyzeModel string, analyzeToken string, guestMode bool) string {
|
||||||
|
if guestMode {
|
||||||
|
return fmt.Sprintf("https://%s/share/%s", analyzeDestination, analyzeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("https://%s/app/workspaces/%s", analyzeDestination, analyzeModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckIfModelReady(analyzeDestination string, analyzeModel string, analyzeToken string, guestMode bool) bool {
|
||||||
|
statusUrl, _ := url.Parse(fmt.Sprintf("https://trcc.%s/models/%s/status", analyzeDestination, analyzeModel))
|
||||||
|
|
||||||
|
authHeader := getAuthHeader(guestMode)
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: statusUrl,
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Content-Type": {"application/json"},
|
||||||
|
authHeader: {analyzeToken},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statusResp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer statusResp.Body.Close()
|
||||||
|
|
||||||
|
target := &ModelStatus{}
|
||||||
|
_ = json.NewDecoder(statusResp.Body).Decode(&target)
|
||||||
|
|
||||||
|
return target.LastMajorGeneration > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthHeader(guestMode bool) string {
|
||||||
|
if guestMode {
|
||||||
|
return "Guest-Auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Authorization"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrafficDumpUrl(analyzeDestination string, analyzeModel string) *url.URL {
|
||||||
|
strUrl := fmt.Sprintf("https://traffic.%s/dumpTrafficBulk/%s", analyzeDestination, analyzeModel)
|
||||||
|
postUrl, _ := url.Parse(strUrl)
|
||||||
|
return postUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnalyzeInformation struct {
|
||||||
|
IsAnalyzing bool
|
||||||
|
GuestMode bool
|
||||||
|
SentCount int
|
||||||
|
AnalyzedModel string
|
||||||
|
AnalyzeToken string
|
||||||
|
AnalyzeDestination string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *AnalyzeInformation) Reset() {
|
||||||
|
info.IsAnalyzing = false
|
||||||
|
info.GuestMode = true
|
||||||
|
info.AnalyzedModel = ""
|
||||||
|
info.AnalyzeToken = ""
|
||||||
|
info.AnalyzeDestination = ""
|
||||||
|
info.SentCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var analyzeInformation = &AnalyzeInformation{}
|
||||||
|
|
||||||
|
func GetAnalyzeInfo() *shared.AnalyzeStatus {
|
||||||
|
return &shared.AnalyzeStatus{
|
||||||
|
IsAnalyzing: analyzeInformation.IsAnalyzing,
|
||||||
|
RemoteUrl: GetRemoteUrl(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzedModel, analyzeInformation.AnalyzeToken, analyzeInformation.GuestMode),
|
||||||
|
IsRemoteReady: CheckIfModelReady(analyzeInformation.AnalyzeDestination, analyzeInformation.AnalyzedModel, analyzeInformation.AnalyzeToken, analyzeInformation.GuestMode),
|
||||||
|
SentCount: analyzeInformation.SentCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncEntries(syncEntriesConfig *shared.SyncEntriesConfig) error {
|
||||||
|
logger.Log.Infof("Sync entries - started\n")
|
||||||
|
|
||||||
|
var (
|
||||||
|
token, model string
|
||||||
|
guestMode bool
|
||||||
|
)
|
||||||
|
if syncEntriesConfig.Token == "" {
|
||||||
|
logger.Log.Infof("Sync entries - creating anonymous token. env %s\n", syncEntriesConfig.Env)
|
||||||
|
guestToken, err := createAnonymousToken(syncEntriesConfig.Env)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed creating anonymous token, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token = guestToken.Token
|
||||||
|
model = guestToken.Model
|
||||||
|
guestMode = true
|
||||||
|
} else {
|
||||||
|
token = fmt.Sprintf("bearer %s", syncEntriesConfig.Token)
|
||||||
|
model = syncEntriesConfig.Workspace
|
||||||
|
guestMode = false
|
||||||
|
|
||||||
|
logger.Log.Infof("Sync entries - upserting model. env %s, model %s\n", syncEntriesConfig.Env, model)
|
||||||
|
if err := upsertModel(token, model, syncEntriesConfig.Env); err != nil {
|
||||||
|
return fmt.Errorf("failed upserting model, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modelRegex, _ := regexp.Compile("[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]+$")
|
||||||
|
if len(model) > 63 || !modelRegex.MatchString(model) {
|
||||||
|
return fmt.Errorf("invalid model name, model name: %s", model)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("Sync entries - syncing. token: %s, model: %s, guest mode: %v\n", token, model, guestMode)
|
||||||
|
go syncEntriesImpl(token, model, syncEntriesConfig.Env, syncEntriesConfig.UploadIntervalSec, guestMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertModel(token string, model string, envPrefix string) error {
|
||||||
|
upsertModelUrl, _ := url.Parse(fmt.Sprintf("https://trcc.%s/models/%s", envPrefix, model))
|
||||||
|
|
||||||
|
authHeader := getAuthHeader(false)
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: upsertModelUrl,
|
||||||
|
Header: map[string][]string{
|
||||||
|
authHeader: {token},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed request to upsert model, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case the model is not created (not 201) and doesn't exists (not 409)
|
||||||
|
if response.StatusCode != 201 && response.StatusCode != 409 {
|
||||||
|
return fmt.Errorf("failed request to upsert model, status code: %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAnonymousToken(envPrefix string) (*GuestToken, error) {
|
||||||
|
tokenUrl := fmt.Sprintf("https://trcc.%s/anonymous/token", envPrefix)
|
||||||
|
if strings.HasPrefix(envPrefix, "http") {
|
||||||
|
tokenUrl = fmt.Sprintf("%s/api/token", envPrefix)
|
||||||
|
}
|
||||||
|
token := &GuestToken{}
|
||||||
|
if err := getGuestToken(tokenUrl, token); err != nil {
|
||||||
|
logger.Log.Infof("Failed to get token, %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGuestToken(url string, target *GuestToken) error {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
logger.Log.Infof("Got token from the server, starting to json decode... status code: %v", resp.StatusCode)
|
||||||
|
return json.NewDecoder(resp.Body).Decode(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncEntriesImpl(token string, model string, envPrefix string, uploadIntervalSec int, guestMode bool) {
|
||||||
|
analyzeInformation.IsAnalyzing = true
|
||||||
|
analyzeInformation.GuestMode = guestMode
|
||||||
|
analyzeInformation.AnalyzedModel = model
|
||||||
|
analyzeInformation.AnalyzeToken = token
|
||||||
|
analyzeInformation.AnalyzeDestination = envPrefix
|
||||||
|
analyzeInformation.SentCount = 0
|
||||||
|
|
||||||
|
sleepTime := time.Second * time.Duration(uploadIntervalSec)
|
||||||
|
|
||||||
|
var timestampFrom int64 = 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
timestampTo := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
logger.Log.Infof("Getting entries from %v, to %v\n", timestampFrom, timestampTo)
|
||||||
|
protocolFilter := "http"
|
||||||
|
entriesArray := database.GetEntriesFromDb(timestampFrom, timestampTo, &protocolFilter)
|
||||||
|
|
||||||
|
if len(entriesArray) > 0 {
|
||||||
|
result := make([]har.Entry, 0)
|
||||||
|
for _, data := range entriesArray {
|
||||||
|
var pair tapApi.RequestResponsePair
|
||||||
|
if err := json.Unmarshal([]byte(data.Entry), &pair); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
harEntry, err := utils.NewEntry(&pair)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if data.ResolvedSource != "" {
|
||||||
|
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-source", Value: data.ResolvedSource})
|
||||||
|
}
|
||||||
|
if data.ResolvedDestination != "" {
|
||||||
|
harEntry.Request.Headers = append(harEntry.Request.Headers, har.Header{Name: "x-mizu-destination", Value: data.ResolvedDestination})
|
||||||
|
harEntry.Request.URL = utils.SetHostname(harEntry.Request.URL, data.ResolvedDestination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// go's default marshal behavior is to encode []byte fields to base64, python's default unmarshal behavior is to not decode []byte fields from base64
|
||||||
|
if harEntry.Response.Content.Text, err = base64.StdEncoding.DecodeString(string(harEntry.Response.Content.Text)); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, *harEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("About to upload %v entries\n", len(result))
|
||||||
|
|
||||||
|
body, jMarshalErr := json.Marshal(result)
|
||||||
|
if jMarshalErr != nil {
|
||||||
|
analyzeInformation.Reset()
|
||||||
|
logger.Log.Infof("Stopping sync entries")
|
||||||
|
logger.Log.Fatal(jMarshalErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var in bytes.Buffer
|
||||||
|
w := zlib.NewWriter(&in)
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
_ = w.Close()
|
||||||
|
reqBody := ioutil.NopCloser(bytes.NewReader(in.Bytes()))
|
||||||
|
|
||||||
|
authHeader := getAuthHeader(guestMode)
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
URL: GetTrafficDumpUrl(envPrefix, model),
|
||||||
|
Header: map[string][]string{
|
||||||
|
"Content-Encoding": {"deflate"},
|
||||||
|
"Content-Type": {"application/octet-stream"},
|
||||||
|
authHeader: {token},
|
||||||
|
},
|
||||||
|
Body: reqBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, postErr := http.DefaultClient.Do(req); postErr != nil {
|
||||||
|
analyzeInformation.Reset()
|
||||||
|
logger.Log.Info("Stopping sync entries")
|
||||||
|
logger.Log.Fatal(postErr)
|
||||||
|
}
|
||||||
|
analyzeInformation.SentCount += len(entriesArray)
|
||||||
|
logger.Log.Infof("Finish uploading %v entries to %s\n", len(entriesArray), GetTrafficDumpUrl(envPrefix, model))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
logger.Log.Infof("Nothing to upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("Sleeping for %v...\n", sleepTime)
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
timestampFrom = timestampTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAnalyzeStatus(callback func(data []byte)) {
|
||||||
|
for {
|
||||||
|
if !analyzeInformation.IsAnalyzing {
|
||||||
|
time.Sleep(AnalyzeCheckSleepTime)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
analyzeStatus := GetAnalyzeInfo()
|
||||||
|
socketMessage := shared.CreateWebSocketMessageTypeAnalyzeStatus(*analyzeStatus)
|
||||||
|
|
||||||
|
jsonMessage, _ := json.Marshal(socketMessage)
|
||||||
|
callback(jsonMessage)
|
||||||
|
time.Sleep(AnalyzeCheckSleepTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
257
agent/pkg/utils/har.go
Normal file
257
agent/pkg/utils/har.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/martian/har"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"github.com/up9inc/mizu/tap/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keep it because we might want cookies in the future
|
||||||
|
//func BuildCookies(rawCookies []interface{}) []har.Cookie {
|
||||||
|
// cookies := make([]har.Cookie, 0, len(rawCookies))
|
||||||
|
//
|
||||||
|
// for _, cookie := range rawCookies {
|
||||||
|
// c := cookie.(map[string]interface{})
|
||||||
|
// expiresStr := ""
|
||||||
|
// if c["expires"] != nil {
|
||||||
|
// expiresStr = c["expires"].(string)
|
||||||
|
// }
|
||||||
|
// expires, _ := time.Parse(time.RFC3339, expiresStr)
|
||||||
|
// httpOnly := false
|
||||||
|
// if c["httponly"] != nil {
|
||||||
|
// httpOnly, _ = strconv.ParseBool(c["httponly"].(string))
|
||||||
|
// }
|
||||||
|
// secure := false
|
||||||
|
// if c["secure"] != nil {
|
||||||
|
// secure, _ = strconv.ParseBool(c["secure"].(string))
|
||||||
|
// }
|
||||||
|
// path := ""
|
||||||
|
// if c["path"] != nil {
|
||||||
|
// path = c["path"].(string)
|
||||||
|
// }
|
||||||
|
// domain := ""
|
||||||
|
// if c["domain"] != nil {
|
||||||
|
// domain = c["domain"].(string)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// cookies = append(cookies, har.Cookie{
|
||||||
|
// Name: c["name"].(string),
|
||||||
|
// Value: c["value"].(string),
|
||||||
|
// Path: path,
|
||||||
|
// Domain: domain,
|
||||||
|
// HTTPOnly: httpOnly,
|
||||||
|
// Secure: secure,
|
||||||
|
// Expires: expires,
|
||||||
|
// Expires8601: expiresStr,
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return cookies
|
||||||
|
//}
|
||||||
|
|
||||||
|
func BuildHeaders(rawHeaders []interface{}) ([]har.Header, string, string, string, string, string) {
|
||||||
|
var host, scheme, authority, path, status string
|
||||||
|
headers := make([]har.Header, 0, len(rawHeaders))
|
||||||
|
|
||||||
|
for _, header := range rawHeaders {
|
||||||
|
h := header.(map[string]interface{})
|
||||||
|
|
||||||
|
headers = append(headers, har.Header{
|
||||||
|
Name: h["name"].(string),
|
||||||
|
Value: h["value"].(string),
|
||||||
|
})
|
||||||
|
|
||||||
|
if h["name"] == "Host" {
|
||||||
|
host = h["value"].(string)
|
||||||
|
}
|
||||||
|
if h["name"] == ":authority" {
|
||||||
|
authority = h["value"].(string)
|
||||||
|
}
|
||||||
|
if h["name"] == ":scheme" {
|
||||||
|
scheme = h["value"].(string)
|
||||||
|
}
|
||||||
|
if h["name"] == ":path" {
|
||||||
|
path = h["value"].(string)
|
||||||
|
}
|
||||||
|
if h["name"] == ":status" {
|
||||||
|
status = h["value"].(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers, host, scheme, authority, path, status
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildPostParams(rawParams []interface{}) []har.Param {
|
||||||
|
params := make([]har.Param, 0, len(rawParams))
|
||||||
|
for _, param := range rawParams {
|
||||||
|
p := param.(map[string]interface{})
|
||||||
|
name := ""
|
||||||
|
if p["name"] != nil {
|
||||||
|
name = p["name"].(string)
|
||||||
|
}
|
||||||
|
value := ""
|
||||||
|
if p["value"] != nil {
|
||||||
|
value = p["value"].(string)
|
||||||
|
}
|
||||||
|
fileName := ""
|
||||||
|
if p["fileName"] != nil {
|
||||||
|
fileName = p["fileName"].(string)
|
||||||
|
}
|
||||||
|
contentType := ""
|
||||||
|
if p["contentType"] != nil {
|
||||||
|
contentType = p["contentType"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
params = append(params, har.Param{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Filename: fileName,
|
||||||
|
ContentType: contentType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest(request *api.GenericMessage) (harRequest *har.Request, err error) {
|
||||||
|
reqDetails := request.Payload.(map[string]interface{})["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers, host, scheme, authority, path, _ := BuildHeaders(reqDetails["headers"].([]interface{}))
|
||||||
|
cookies := make([]har.Cookie, 0) // BuildCookies(reqDetails["cookies"].([]interface{}))
|
||||||
|
|
||||||
|
postData, _ := reqDetails["postData"].(map[string]interface{})
|
||||||
|
mimeType, _ := postData["mimeType"]
|
||||||
|
if mimeType == nil || len(mimeType.(string)) == 0 {
|
||||||
|
mimeType = "text/html"
|
||||||
|
}
|
||||||
|
text, _ := postData["text"]
|
||||||
|
postDataText := ""
|
||||||
|
if text != nil {
|
||||||
|
postDataText = text.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryString := make([]har.QueryString, 0)
|
||||||
|
for _, _qs := range reqDetails["queryString"].([]interface{}) {
|
||||||
|
qs := _qs.(map[string]interface{})
|
||||||
|
queryString = append(queryString, har.QueryString{
|
||||||
|
Name: qs["name"].(string),
|
||||||
|
Value: qs["value"].(string),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s%s", host, reqDetails["url"].(string))
|
||||||
|
if strings.HasPrefix(mimeType.(string), "application/grpc") {
|
||||||
|
url = fmt.Sprintf("%s://%s%s", scheme, authority, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
harParams := make([]har.Param, 0)
|
||||||
|
if postData["params"] != nil {
|
||||||
|
harParams = BuildPostParams(postData["params"].([]interface{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
harRequest = &har.Request{
|
||||||
|
Method: reqDetails["method"].(string),
|
||||||
|
URL: url,
|
||||||
|
HTTPVersion: reqDetails["httpVersion"].(string),
|
||||||
|
HeadersSize: -1,
|
||||||
|
BodySize: int64(bytes.NewBufferString(postDataText).Len()),
|
||||||
|
QueryString: queryString,
|
||||||
|
Headers: headers,
|
||||||
|
Cookies: cookies,
|
||||||
|
PostData: &har.PostData{
|
||||||
|
MimeType: mimeType.(string),
|
||||||
|
Params: harParams,
|
||||||
|
Text: postDataText,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResponse(response *api.GenericMessage) (harResponse *har.Response, err error) {
|
||||||
|
resDetails := response.Payload.(map[string]interface{})["details"].(map[string]interface{})
|
||||||
|
|
||||||
|
headers, _, _, _, _, _status := BuildHeaders(resDetails["headers"].([]interface{}))
|
||||||
|
cookies := make([]har.Cookie, 0) // BuildCookies(resDetails["cookies"].([]interface{}))
|
||||||
|
|
||||||
|
content, _ := resDetails["content"].(map[string]interface{})
|
||||||
|
mimeType, _ := content["mimeType"]
|
||||||
|
if mimeType == nil || len(mimeType.(string)) == 0 {
|
||||||
|
mimeType = "text/html"
|
||||||
|
}
|
||||||
|
encoding, _ := content["encoding"]
|
||||||
|
text, _ := content["text"]
|
||||||
|
bodyText := ""
|
||||||
|
if text != nil {
|
||||||
|
bodyText = text.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
harContent := &har.Content{
|
||||||
|
Encoding: encoding.(string),
|
||||||
|
MimeType: mimeType.(string),
|
||||||
|
Text: []byte(bodyText),
|
||||||
|
Size: int64(len(bodyText)),
|
||||||
|
}
|
||||||
|
|
||||||
|
status := int(resDetails["status"].(float64))
|
||||||
|
if strings.HasPrefix(mimeType.(string), "application/grpc") {
|
||||||
|
status, err = strconv.Atoi(_status)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed converting status to int %s (%v,%+v)", err, err, err)
|
||||||
|
return nil, errors.New("failed converting response status to int for HAR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
harResponse = &har.Response{
|
||||||
|
HTTPVersion: resDetails["httpVersion"].(string),
|
||||||
|
Status: status,
|
||||||
|
StatusText: resDetails["statusText"].(string),
|
||||||
|
HeadersSize: -1,
|
||||||
|
BodySize: int64(bytes.NewBufferString(bodyText).Len()),
|
||||||
|
Headers: headers,
|
||||||
|
Cookies: cookies,
|
||||||
|
Content: harContent,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntry(pair *api.RequestResponsePair) (*har.Entry, error) {
|
||||||
|
harRequest, err := NewRequest(&pair.Request)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed converting request to HAR %s (%v,%+v)", err, err, err)
|
||||||
|
return nil, errors.New("failed converting request to HAR")
|
||||||
|
}
|
||||||
|
|
||||||
|
harResponse, err := NewResponse(&pair.Response)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed converting response to HAR %s (%v,%+v)", err, err, err)
|
||||||
|
return nil, errors.New("failed converting response to HAR")
|
||||||
|
}
|
||||||
|
|
||||||
|
totalTime := pair.Response.CaptureTime.Sub(pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds()
|
||||||
|
if totalTime < 1 {
|
||||||
|
totalTime = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
harEntry := har.Entry{
|
||||||
|
StartedDateTime: pair.Request.CaptureTime,
|
||||||
|
Time: totalTime,
|
||||||
|
Request: harRequest,
|
||||||
|
Response: harResponse,
|
||||||
|
Cache: &har.Cache{},
|
||||||
|
Timings: &har.Timings{
|
||||||
|
Send: -1,
|
||||||
|
Wait: -1,
|
||||||
|
Receive: totalTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &harEntry, nil
|
||||||
|
}
|
||||||
60
agent/pkg/utils/truncating_logger.go
Normal file
60
agent/pkg/utils/truncating_logger.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
loggerShared "github.com/up9inc/mizu/shared/logger"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TruncatingLogger implements the gorm logger.Interface interface. Its purpose is to act as gorm's logger while truncating logs to a max of 50 characters to minimise the performance impact
|
||||||
|
type TruncatingLogger struct {
|
||||||
|
LogLevel logger.LogLevel
|
||||||
|
SlowThreshold time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) LogMode(logLevel logger.LogLevel) logger.Interface {
|
||||||
|
truncatingLogger.LogLevel = logLevel
|
||||||
|
return truncatingLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Info(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Info {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loggerShared.Log.Errorf("gorm info: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Warn(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Warn {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loggerShared.Log.Errorf("gorm warning: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Error(_ context.Context, message string, __ ...interface{}) {
|
||||||
|
if truncatingLogger.LogLevel < logger.Error {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loggerShared.Log.Errorf("gorm error: %.150s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (truncatingLogger *TruncatingLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
|
if truncatingLogger.LogLevel == logger.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
if err != nil {
|
||||||
|
sql, rows := fc() // copied into every condition as this is a potentially heavy operation best done only when necessary
|
||||||
|
truncatingLogger.Error(ctx, fmt.Sprintf("Error in %s: %v - elapsed: %fs affected rows: %d, sql: %s", utils.FileWithLineNum(), err, elapsed.Seconds(), rows, sql))
|
||||||
|
} else if truncatingLogger.LogLevel >= logger.Warn && elapsed > truncatingLogger.SlowThreshold {
|
||||||
|
sql, rows := fc()
|
||||||
|
truncatingLogger.Warn(ctx, fmt.Sprintf("Slow sql query - elapse: %fs rows: %d, sql: %s", elapsed.Seconds(), rows, sql))
|
||||||
|
} else if truncatingLogger.LogLevel >= logger.Info {
|
||||||
|
sql, rows := fc()
|
||||||
|
truncatingLogger.Info(ctx, fmt.Sprintf("Sql query - elapse: %fs rows: %d, sql: %s", elapsed.Seconds(), rows, sql))
|
||||||
|
}
|
||||||
|
}
|
||||||
73
agent/pkg/utils/utils.go
Normal file
73
agent/pkg/utils/utils.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartServer starts the server with a graceful shutdown
|
||||||
|
func StartServer(app *gin.Engine) {
|
||||||
|
signals := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(signals,
|
||||||
|
os.Interrupt, // this catch ctrl + c
|
||||||
|
syscall.SIGTSTP, // this catch ctrl + z
|
||||||
|
)
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
Handler: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = <-signals
|
||||||
|
logger.Log.Infof("Shutting down...")
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
_ = srv.Shutdown(ctx)
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Run server.
|
||||||
|
logger.Log.Infof("Starting the server...")
|
||||||
|
if err := app.Run(fmt.Sprintf(":%d", shared.DefaultApiServerPort)); err != nil {
|
||||||
|
logger.Log.Errorf("Server is not running! Reason: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReverseSlice(data interface{}) {
|
||||||
|
value := reflect.ValueOf(data)
|
||||||
|
valueLen := value.Len()
|
||||||
|
for i := 0; i <= int((valueLen-1)/2); i++ {
|
||||||
|
reverseIndex := valueLen - 1 - i
|
||||||
|
tmp := value.Index(reverseIndex).Interface()
|
||||||
|
value.Index(reverseIndex).Set(value.Index(i))
|
||||||
|
value.Index(i).Set(reflect.ValueOf(tmp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckErr(e error) {
|
||||||
|
if e != nil {
|
||||||
|
logger.Log.Errorf("%v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetHostname(address, newHostname string) string {
|
||||||
|
replacedUrl, err := url.Parse(address)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("error replacing hostname to %s in address %s, returning original %v", newHostname, address, err)
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
replacedUrl.Host = newHostname
|
||||||
|
return replacedUrl.String()
|
||||||
|
|
||||||
|
}
|
||||||
8
agent/pkg/version/consts.go
Normal file
8
agent/pkg/version/consts.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
var (
|
||||||
|
SemVer = "0.0.1"
|
||||||
|
Branch = "develop"
|
||||||
|
GitCommitHash = "" // this var is overridden using ldflags in makefile when building
|
||||||
|
BuildTimestamp = "" // this var is overridden using ldflags in makefile when building
|
||||||
|
)
|
||||||
147
api/main.go
147
api/main.go
@@ -1,147 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/api"
|
|
||||||
"mizuserver/pkg/middleware"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/routes"
|
|
||||||
"mizuserver/pkg/sensitiveDataFiltering"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
)
|
|
||||||
|
|
||||||
var shouldTap = flag.Bool("tap", false, "Run in tapper mode without API")
|
|
||||||
var aggregator = flag.Bool("aggregator", false, "Run in aggregator mode with API")
|
|
||||||
var standalone = flag.Bool("standalone", false, "Run in standalone tapper and API mode")
|
|
||||||
var aggregatorAddress = flag.String("aggregator-address", "", "Address of mizu collector for tapping")
|
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if !*shouldTap && !*aggregator && !*standalone{
|
|
||||||
panic("One of the flags --tap, --api or --standalone must be provided")
|
|
||||||
}
|
|
||||||
|
|
||||||
if *standalone {
|
|
||||||
harOutputChannel := tap.StartPassiveTapper()
|
|
||||||
filteredHarChannel := make(chan *tap.OutputChannelItem)
|
|
||||||
go filterHarHeaders(harOutputChannel, filteredHarChannel, getTrafficFilteringOptions())
|
|
||||||
go api.StartReadingEntries(filteredHarChannel, nil)
|
|
||||||
hostApi(nil)
|
|
||||||
} else if *shouldTap {
|
|
||||||
if *aggregatorAddress == "" {
|
|
||||||
panic("Aggregator address must be provided with --aggregator-address when using --tap")
|
|
||||||
}
|
|
||||||
|
|
||||||
tapTargets := getTapTargets()
|
|
||||||
if tapTargets != nil {
|
|
||||||
tap.HostAppAddresses = tapTargets
|
|
||||||
fmt.Println("Filtering for the following addresses:", tap.HostAppAddresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
harOutputChannel := tap.StartPassiveTapper()
|
|
||||||
socketConnection, err := shared.ConnectToSocketServer(*aggregatorAddress, shared.DEFAULT_SOCKET_RETRIES, shared.DEFAULT_SOCKET_RETRY_SLEEP_TIME, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Error connecting to socket server at %s %v", *aggregatorAddress, err))
|
|
||||||
}
|
|
||||||
go pipeChannelToSocket(socketConnection, harOutputChannel)
|
|
||||||
} else if *aggregator {
|
|
||||||
socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000)
|
|
||||||
filteredHarChannel := make(chan *tap.OutputChannelItem)
|
|
||||||
go api.StartReadingEntries(filteredHarChannel, nil)
|
|
||||||
go filterHarHeaders(socketHarOutChannel, filteredHarChannel, getTrafficFilteringOptions())
|
|
||||||
hostApi(socketHarOutChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
signalChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signalChan, os.Interrupt)
|
|
||||||
<-signalChan
|
|
||||||
|
|
||||||
fmt.Println("Exiting")
|
|
||||||
}
|
|
||||||
|
|
||||||
func hostApi(socketHarOutputChannel chan<- *tap.OutputChannelItem) {
|
|
||||||
app := fiber.New()
|
|
||||||
|
|
||||||
|
|
||||||
middleware.FiberMiddleware(app) // Register Fiber's middleware for app.
|
|
||||||
app.Static("/", "./site")
|
|
||||||
|
|
||||||
//Simple route to know server is running
|
|
||||||
app.Get("/echo", func(c *fiber.Ctx) error {
|
|
||||||
return c.SendString("Hello, World 👋!")
|
|
||||||
})
|
|
||||||
eventHandlers := api.RoutesEventHandlers{
|
|
||||||
SocketHarOutChannel: socketHarOutputChannel,
|
|
||||||
}
|
|
||||||
routes.WebSocketRoutes(app, &eventHandlers)
|
|
||||||
routes.EntriesRoutes(app)
|
|
||||||
routes.NotFoundRoute(app)
|
|
||||||
|
|
||||||
utils.StartServer(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func getTapTargets() []string {
|
|
||||||
nodeName := os.Getenv(shared.NodeNameEnvVar)
|
|
||||||
var tappedAddressesPerNodeDict map[string][]string
|
|
||||||
err := json.Unmarshal([]byte(os.Getenv(shared.TappedAddressesPerNodeDictEnvVar)), &tappedAddressesPerNodeDict)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("env var %s's value of %s is invalid! must be map[string][]string %v", shared.TappedAddressesPerNodeDictEnvVar, tappedAddressesPerNodeDict, err))
|
|
||||||
}
|
|
||||||
return tappedAddressesPerNodeDict[nodeName]
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTrafficFilteringOptions() *shared.TrafficFilteringOptions {
|
|
||||||
filteringOptionsJson := os.Getenv(shared.MizuFilteringOptionsEnvVar)
|
|
||||||
if filteringOptionsJson == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var filteringOptions shared.TrafficFilteringOptions
|
|
||||||
err := json.Unmarshal([]byte(filteringOptionsJson), &filteringOptions)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("env var %s's value of %s is invalid! json must match the shared.TrafficFilteringOptions struct %v", shared.MizuFilteringOptionsEnvVar, filteringOptionsJson, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &filteringOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHarHeaders(inChannel <- chan *tap.OutputChannelItem, outChannel chan *tap.OutputChannelItem, filterOptions *shared.TrafficFilteringOptions) {
|
|
||||||
for message := range inChannel {
|
|
||||||
sensitiveDataFiltering.FilterSensitiveInfoFromHarRequest(message, filterOptions)
|
|
||||||
outChannel <- message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) {
|
|
||||||
if connection == nil {
|
|
||||||
panic("Websocket connection is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if messageDataChannel == nil {
|
|
||||||
panic("Channel of captured messages is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
for messageData := range messageDataChannel {
|
|
||||||
marshaledData, err := models.CreateWebsocketTappedEntryMessage(messageData)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error converting message to json %s, (%v,%+v)\n", err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = connection.WriteMessage(websocket.TextMessage, marshaledData)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error sending message through socket server %s, (%v,%+v)\n", err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
||||||
"mizuserver/pkg/database"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/resolver"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var k8sResolver *resolver.Resolver
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
errOut := make(chan error, 100)
|
|
||||||
res, err := resolver.NewFromInCluster(errOut)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error creating k8s resolver %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
res.Start(ctx)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case err := <-errOut:
|
|
||||||
fmt.Printf("name resolving error %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
k8sResolver = res
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartReadingEntries(harChannel <-chan *tap.OutputChannelItem, workingDir *string) {
|
|
||||||
if workingDir != nil && *workingDir != "" {
|
|
||||||
startReadingFiles(*workingDir)
|
|
||||||
} else {
|
|
||||||
startReadingChannel(harChannel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startReadingFiles(workingDir string) {
|
|
||||||
err := os.MkdirAll(workingDir, os.ModePerm)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
|
|
||||||
for true {
|
|
||||||
dir, _ := os.Open(workingDir)
|
|
||||||
dirFiles, _ := dir.Readdir(-1)
|
|
||||||
sort.Sort(utils.ByModTime(dirFiles))
|
|
||||||
|
|
||||||
if len(dirFiles) == 0 {
|
|
||||||
fmt.Printf("Waiting for new files\n")
|
|
||||||
time.Sleep(3 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fileInfo := dirFiles[0]
|
|
||||||
inputFilePath := path.Join(workingDir, fileInfo.Name())
|
|
||||||
file, err := os.Open(inputFilePath)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
|
|
||||||
var inputHar har.HAR
|
|
||||||
decErr := json.NewDecoder(bufio.NewReader(file)).Decode(&inputHar)
|
|
||||||
utils.CheckErr(decErr)
|
|
||||||
|
|
||||||
for _, entry := range inputHar.Log.Entries {
|
|
||||||
time.Sleep(time.Millisecond * 250)
|
|
||||||
saveHarToDb(entry, fileInfo.Name())
|
|
||||||
}
|
|
||||||
rmErr := os.Remove(inputFilePath)
|
|
||||||
utils.CheckErr(rmErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startReadingChannel(outputItems <-chan *tap.OutputChannelItem) {
|
|
||||||
if outputItems == nil {
|
|
||||||
panic("Channel of captured messages is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
for item := range outputItems {
|
|
||||||
saveHarToDb(item.HarEntry, item.RequestSenderIp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveHarToDb(entry *har.Entry, sender string) {
|
|
||||||
entryBytes, _ := json.Marshal(entry)
|
|
||||||
serviceName, urlPath, serviceHostName := getServiceNameFromUrl(entry.Request.URL)
|
|
||||||
entryId := primitive.NewObjectID().Hex()
|
|
||||||
var (
|
|
||||||
resolvedSource string
|
|
||||||
resolvedDestination string
|
|
||||||
)
|
|
||||||
if k8sResolver != nil {
|
|
||||||
resolvedSource = k8sResolver.Resolve(sender)
|
|
||||||
resolvedDestination = k8sResolver.Resolve(serviceHostName)
|
|
||||||
}
|
|
||||||
mizuEntry := models.MizuEntry{
|
|
||||||
EntryId: entryId,
|
|
||||||
Entry: string(entryBytes), // simple way to store it and not convert to bytes
|
|
||||||
Service: serviceName,
|
|
||||||
Url: entry.Request.URL,
|
|
||||||
Path: urlPath,
|
|
||||||
Method: entry.Request.Method,
|
|
||||||
Status: entry.Response.Status,
|
|
||||||
RequestSenderIp: sender,
|
|
||||||
Timestamp: entry.StartedDateTime.UnixNano() / int64(time.Millisecond),
|
|
||||||
ResolvedSource: resolvedSource,
|
|
||||||
ResolvedDestination: resolvedDestination,
|
|
||||||
}
|
|
||||||
database.GetEntriesTable().Create(&mizuEntry)
|
|
||||||
|
|
||||||
baseEntry := utils.GetResolvedBaseEntry(mizuEntry)
|
|
||||||
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(&baseEntry)
|
|
||||||
broadcastToBrowserClients(baseEntryBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServiceNameFromUrl(inputUrl string) (string, string, string) {
|
|
||||||
parsed, err := url.Parse(inputUrl)
|
|
||||||
utils.CheckErr(err)
|
|
||||||
return fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host), parsed.Path, parsed.Host
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/antoniodipinto/ikisocket"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/controllers"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/routes"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
)
|
|
||||||
|
|
||||||
var browserClientSocketUUIDs = make([]string, 0)
|
|
||||||
|
|
||||||
type RoutesEventHandlers struct {
|
|
||||||
routes.EventHandlers
|
|
||||||
SocketHarOutChannel chan<- *tap.OutputChannelItem
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketConnect(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Connection event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Connection event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
browserClientSocketUUIDs = append(browserClientSocketUUIDs, ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketDisconnect(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Disconnection event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Disconnection event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
removeSocketUUIDFromBrowserSlice(ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastToBrowserClients(message []byte) {
|
|
||||||
ikisocket.EmitToList(browserClientSocketUUIDs, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketClose(ep *ikisocket.EventPayload) {
|
|
||||||
if ep.Kws.GetAttribute("is_tapper") == true {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Close event - Tapper connected: %s", ep.SocketUUID))
|
|
||||||
} else {
|
|
||||||
fmt.Println(fmt.Sprintf("Websocket Close event - Browser socket connected: %s", ep.SocketUUID))
|
|
||||||
removeSocketUUIDFromBrowserSlice(ep.SocketUUID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketError(ep *ikisocket.EventPayload) {
|
|
||||||
fmt.Println(fmt.Sprintf("Socket error - Socket uuid : %s %v", ep.SocketUUID, ep.Error))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RoutesEventHandlers) WebSocketMessage(ep *ikisocket.EventPayload) {
|
|
||||||
var socketMessageBase shared.WebSocketMessageMetadata
|
|
||||||
err := json.Unmarshal(ep.Data, &socketMessageBase)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal websocket message %v\n", err)
|
|
||||||
} else {
|
|
||||||
switch socketMessageBase.MessageType {
|
|
||||||
case shared.WebSocketMessageTypeTappedEntry:
|
|
||||||
var tappedEntryMessage models.WebSocketTappedEntryMessage
|
|
||||||
err := json.Unmarshal(ep.Data, &tappedEntryMessage)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
|
||||||
} else {
|
|
||||||
h.SocketHarOutChannel <- tappedEntryMessage.Data
|
|
||||||
}
|
|
||||||
case shared.WebSocketMessageTypeUpdateStatus:
|
|
||||||
var statusMessage shared.WebSocketStatusMessage
|
|
||||||
err := json.Unmarshal(ep.Data, &statusMessage)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err)
|
|
||||||
} else {
|
|
||||||
controllers.TapStatus = statusMessage.TappingStatus
|
|
||||||
broadcastToBrowserClients(ep.Data)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
fmt.Printf("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func removeSocketUUIDFromBrowserSlice(uuidToRemove string) {
|
|
||||||
newUUIDSlice := make([]string, 0, len(browserClientSocketUUIDs))
|
|
||||||
for _, uuid := range browserClientSocketUUIDs {
|
|
||||||
if uuid != uuidToRemove {
|
|
||||||
newUUIDSlice = append(newUUIDSlice, uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
browserClientSocketUUIDs = newUUIDSlice
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"mizuserver/pkg/database"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"mizuserver/pkg/utils"
|
|
||||||
"mizuserver/pkg/validation"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
OrderDesc = "desc"
|
|
||||||
OrderAsc = "asc"
|
|
||||||
LT = "lt"
|
|
||||||
GT = "gt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
operatorToSymbolMapping = map[string]string{
|
|
||||||
LT: "<",
|
|
||||||
GT: ">",
|
|
||||||
}
|
|
||||||
operatorToOrderMapping = map[string]string{
|
|
||||||
LT: OrderDesc,
|
|
||||||
GT: OrderAsc,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetEntries(c *fiber.Ctx) error {
|
|
||||||
entriesFilter := &models.EntriesFilter{}
|
|
||||||
|
|
||||||
if err := c.QueryParser(entriesFilter); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
err := validation.Validate(entriesFilter)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
order := operatorToOrderMapping[entriesFilter.Operator]
|
|
||||||
operatorSymbol := operatorToSymbolMapping[entriesFilter.Operator]
|
|
||||||
var entries []models.MizuEntry
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Order(fmt.Sprintf("timestamp %s", order)).
|
|
||||||
Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
|
||||||
Omit("entry"). // remove the "big" entry field
|
|
||||||
Limit(entriesFilter.Limit).
|
|
||||||
Find(&entries)
|
|
||||||
|
|
||||||
if len(entries) > 0 && order == OrderDesc {
|
|
||||||
// the entries always order from oldest to newest so we should revers
|
|
||||||
utils.ReverseSlice(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to base entries
|
|
||||||
baseEntries := make([]models.BaseEntryDetails, 0, entriesFilter.Limit)
|
|
||||||
for _, entry := range entries {
|
|
||||||
baseEntries = append(baseEntries, utils.GetResolvedBaseEntry(entry))
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(baseEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetHARs(c *fiber.Ctx) error {
|
|
||||||
entriesFilter := &models.HarFetchRequestBody{}
|
|
||||||
order := OrderDesc
|
|
||||||
if err := c.QueryParser(entriesFilter); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
err := validation.Validate(entriesFilter)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var entries []models.MizuEntry
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Order(fmt.Sprintf("timestamp %s", order)).
|
|
||||||
// Where(fmt.Sprintf("timestamp %s %v", operatorSymbol, entriesFilter.Timestamp)).
|
|
||||||
Limit(1000).
|
|
||||||
Find(&entries)
|
|
||||||
|
|
||||||
if len(entries) > 0 {
|
|
||||||
// the entries always order from oldest to newest so we should revers
|
|
||||||
utils.ReverseSlice(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
harsObject := map[string]*models.ExtendedHAR{}
|
|
||||||
|
|
||||||
for _, entryData := range entries {
|
|
||||||
var harEntry har.Entry
|
|
||||||
_ = json.Unmarshal([]byte(entryData.Entry), &harEntry)
|
|
||||||
|
|
||||||
sourceOfEntry := entryData.ResolvedSource
|
|
||||||
fileName := fmt.Sprintf("%s.har", sourceOfEntry)
|
|
||||||
if harOfSource, ok := harsObject[fileName]; ok {
|
|
||||||
harOfSource.Log.Entries = append(harOfSource.Log.Entries, &harEntry)
|
|
||||||
} else {
|
|
||||||
var entriesHar []*har.Entry
|
|
||||||
entriesHar = append(entriesHar, &harEntry)
|
|
||||||
harsObject[fileName] = &models.ExtendedHAR{
|
|
||||||
Log: &models.ExtendedLog{
|
|
||||||
Version: "1.2",
|
|
||||||
Creator: &models.ExtendedCreator{
|
|
||||||
Creator: &har.Creator{
|
|
||||||
Name: "mizu",
|
|
||||||
Version: "0.0.2",
|
|
||||||
},
|
|
||||||
Source: sourceOfEntry,
|
|
||||||
},
|
|
||||||
Entries: entriesHar,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retObj := map[string][]byte{}
|
|
||||||
for k, v := range harsObject {
|
|
||||||
bytesData, _ := json.Marshal(v)
|
|
||||||
retObj[k] = bytesData
|
|
||||||
}
|
|
||||||
buffer := utils.ZipData(retObj)
|
|
||||||
return c.Status(fiber.StatusOK).SendStream(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEntry(c *fiber.Ctx) error {
|
|
||||||
var entryData models.EntryData
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Select("entry", "resolvedDestination").
|
|
||||||
Where(map[string]string{"entryId": c.Params("entryId")}).
|
|
||||||
First(&entryData)
|
|
||||||
|
|
||||||
var fullEntry har.Entry
|
|
||||||
unmarshallErr := json.Unmarshal([]byte(entryData.Entry), &fullEntry)
|
|
||||||
utils.CheckErr(unmarshallErr)
|
|
||||||
|
|
||||||
if entryData.ResolvedDestination != "" {
|
|
||||||
fullEntry.Request.URL = utils.SetHostname(fullEntry.Request.URL, entryData.ResolvedDestination)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(fullEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteAllEntries(c *fiber.Ctx) error {
|
|
||||||
database.GetEntriesTable().
|
|
||||||
Where("1 = 1").
|
|
||||||
Delete(&models.MizuEntry{})
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
||||||
"msg": "Success",
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetGeneralStats(c *fiber.Ctx) error {
|
|
||||||
sqlQuery := "SELECT count(*) as count, min(timestamp) as min, max(timestamp) as max from mizu_entries"
|
|
||||||
var result struct {
|
|
||||||
Count int
|
|
||||||
Min int
|
|
||||||
Max int
|
|
||||||
}
|
|
||||||
database.GetEntriesTable().Raw(sqlQuery).Scan(&result)
|
|
||||||
return c.Status(fiber.StatusOK).JSON(&result)
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package controllers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
var TapStatus shared.TapStatus
|
|
||||||
|
|
||||||
func GetTappingStatus(c *fiber.Ctx) error {
|
|
||||||
return c.Status(fiber.StatusOK).JSON(TapStatus)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DBPath = "./entries.db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DB = initDataBase(DBPath)
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetEntriesTable() *gorm.DB {
|
|
||||||
return DB.Table("mizu_entries")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDataBase(databasePath string) *gorm.DB {
|
|
||||||
temp, _ := gorm.Open(sqlite.Open(databasePath), &gorm.Config{})
|
|
||||||
_ = temp.AutoMigrate(&models.MizuEntry{}) // this will ensure table is created
|
|
||||||
return temp
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FiberMiddleware provide Fiber's built-in middlewares.
|
|
||||||
// See: https://docs.gofiber.io/api/middleware
|
|
||||||
func FiberMiddleware(a *fiber.App) {
|
|
||||||
a.Use(
|
|
||||||
// Add CORS to each route.
|
|
||||||
cors.New(),
|
|
||||||
// Add simple logger.
|
|
||||||
logger.New(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MizuEntry struct {
|
|
||||||
ID uint `gorm:"primarykey"`
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
Entry string `json:"entry,omitempty" gorm:"column:entry"`
|
|
||||||
EntryId string `json:"entryId" gorm:"column:entryId"`
|
|
||||||
Url string `json:"url" gorm:"column:url"`
|
|
||||||
Method string `json:"method" gorm:"column:method"`
|
|
||||||
Status int `json:"status" gorm:"column:status"`
|
|
||||||
RequestSenderIp string `json:"requestSenderIp" gorm:"column:requestSenderIp"`
|
|
||||||
Service string `json:"service" gorm:"column:service"`
|
|
||||||
Timestamp int64 `json:"timestamp" gorm:"column:timestamp"`
|
|
||||||
Path string `json:"path" gorm:"column:path"`
|
|
||||||
ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"`
|
|
||||||
ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseEntryDetails struct {
|
|
||||||
Id string `json:"id,omitempty"`
|
|
||||||
Url string `json:"url,omitempty"`
|
|
||||||
RequestSenderIp string `json:"requestSenderIp,omitempty"`
|
|
||||||
Service string `json:"service,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
StatusCode int `json:"statusCode,omitempty"`
|
|
||||||
Method string `json:"method,omitempty"`
|
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntryData struct {
|
|
||||||
Entry string `json:"entry,omitempty"`
|
|
||||||
ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntriesFilter struct {
|
|
||||||
Limit int `query:"limit" validate:"required,min=1,max=200"`
|
|
||||||
Operator string `query:"operator" validate:"required,oneof='lt' 'gt'"`
|
|
||||||
Timestamp int64 `query:"timestamp" validate:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarFetchRequestBody struct {
|
|
||||||
Limit int `query:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebSocketEntryMessage struct {
|
|
||||||
*shared.WebSocketMessageMetadata
|
|
||||||
Data *BaseEntryDetails `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type WebSocketTappedEntryMessage struct {
|
|
||||||
*shared.WebSocketMessageMetadata
|
|
||||||
Data *tap.OutputChannelItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateBaseEntryWebSocketMessage(base *BaseEntryDetails) ([]byte, error) {
|
|
||||||
message := &WebSocketEntryMessage{
|
|
||||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
|
||||||
MessageType: shared.WebSocketMessageTypeEntry,
|
|
||||||
},
|
|
||||||
Data: base,
|
|
||||||
}
|
|
||||||
return json.Marshal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateWebsocketTappedEntryMessage(base *tap.OutputChannelItem) ([]byte, error) {
|
|
||||||
message := &WebSocketTappedEntryMessage{
|
|
||||||
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
|
|
||||||
MessageType: shared.WebSocketMessageTypeTappedEntry,
|
|
||||||
},
|
|
||||||
Data: base,
|
|
||||||
}
|
|
||||||
return json.Marshal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ExtendedHAR is the top level object of a HAR log.
|
|
||||||
type ExtendedHAR struct {
|
|
||||||
Log *ExtendedLog `json:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtendedLog is the HAR HTTP request and response log.
|
|
||||||
type ExtendedLog struct {
|
|
||||||
// Version number of the HAR format.
|
|
||||||
Version string `json:"version"`
|
|
||||||
// Creator holds information about the log creator application.
|
|
||||||
Creator *ExtendedCreator `json:"creator"`
|
|
||||||
// Entries is a list containing requests and responses.
|
|
||||||
Entries []*har.Entry `json:"entries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtendedCreator struct {
|
|
||||||
*har.Creator
|
|
||||||
Source string `json:"_source"`
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
|
||||||
_ "k8s.io/client-go/plugin/pkg/client/auth/openstack"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
|
||||||
"k8s.io/client-go/util/homedir"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewFromInCluster(errOut chan error) (*Resolver, error) {
|
|
||||||
config, err := restclient.InClusterConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientset, err := kubernetes.NewForConfig(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Resolver{clientConfig: config, clientSet: clientset, nameMap: make(map[string]string), errOut: errOut}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFromOutOfCluster(kubeConfigPath string, errOut chan error) (*Resolver, error) {
|
|
||||||
if kubeConfigPath == "" {
|
|
||||||
home := homedir.HomeDir()
|
|
||||||
kubeConfigPath = filepath.Join(home, ".kube", "config")
|
|
||||||
}
|
|
||||||
|
|
||||||
configPathList := filepath.SplitList(kubeConfigPath)
|
|
||||||
configLoadingRules := &clientcmd.ClientConfigLoadingRules{}
|
|
||||||
if len(configPathList) <= 1 {
|
|
||||||
configLoadingRules.ExplicitPath = kubeConfigPath
|
|
||||||
} else {
|
|
||||||
configLoadingRules.Precedence = configPathList
|
|
||||||
}
|
|
||||||
contextName := ""
|
|
||||||
clientConfigLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
|
||||||
configLoadingRules,
|
|
||||||
&clientcmd.ConfigOverrides{
|
|
||||||
CurrentContext: contextName,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
clientConfig, err := clientConfigLoader.ClientConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
clientset, err := kubernetes.NewForConfig(clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Resolver{clientConfig: clientConfig, clientSet: clientset, nameMap: make(map[string]string), errOut: errOut}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFromExisting(clientConfig *restclient.Config, clientSet *kubernetes.Clientset, errOut chan error) *Resolver {
|
|
||||||
return &Resolver{clientConfig: clientConfig, clientSet: clientSet, nameMap: make(map[string]string), errOut: errOut}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package resolver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
|
||||||
restclient "k8s.io/client-go/rest"
|
|
||||||
)
|
|
||||||
const (
|
|
||||||
kubClientNullString = "None"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Resolver struct {
|
|
||||||
clientConfig *restclient.Config
|
|
||||||
clientSet *kubernetes.Clientset
|
|
||||||
nameMap map[string]string
|
|
||||||
isStarted bool
|
|
||||||
errOut chan error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) Start(ctx context.Context) {
|
|
||||||
if !resolver.isStarted {
|
|
||||||
resolver.isStarted = true
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchServices)
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchEndpoints)
|
|
||||||
go resolver.infiniteErrorHandleRetryFunc(ctx, resolver.watchPods)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) Resolve(name string) string {
|
|
||||||
resolvedName, isFound := resolver.nameMap[name]
|
|
||||||
if !isFound {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return resolvedName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchPods(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Pods("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl pod watch")
|
|
||||||
}
|
|
||||||
if event.Type == watch.Deleted {
|
|
||||||
pod := event.Object.(*corev1.Pod)
|
|
||||||
resolver.saveResolvedName(pod.Status.PodIP, "", event.Type)
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchEndpoints(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Endpoints("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl endpoint watch")
|
|
||||||
}
|
|
||||||
endpoint := event.Object.(*corev1.Endpoints)
|
|
||||||
serviceHostname := fmt.Sprintf("%s.%s", endpoint.Name, endpoint.Namespace)
|
|
||||||
if endpoint.Subsets != nil {
|
|
||||||
for _, subset := range endpoint.Subsets {
|
|
||||||
var ports []int32
|
|
||||||
if subset.Ports != nil {
|
|
||||||
for _, portMapping := range subset.Ports {
|
|
||||||
if portMapping.Port > 0 {
|
|
||||||
ports = append(ports, portMapping.Port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if subset.Addresses != nil {
|
|
||||||
for _, address := range subset.Addresses {
|
|
||||||
resolver.saveResolvedName(address.IP, serviceHostname, event.Type)
|
|
||||||
for _, port := range ports {
|
|
||||||
ipWithPort := fmt.Sprintf("%s:%d", address.IP, port)
|
|
||||||
resolver.saveResolvedName(ipWithPort, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) watchServices(ctx context.Context) error {
|
|
||||||
// empty namespace makes the client watch all namespaces
|
|
||||||
watcher, err := resolver.clientSet.CoreV1().Services("").Watch(ctx, metav1.ListOptions{Watch: true})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event := <- watcher.ResultChan():
|
|
||||||
if event.Object == nil {
|
|
||||||
return errors.New("error in kubectl service watch")
|
|
||||||
}
|
|
||||||
|
|
||||||
service := event.Object.(*corev1.Service)
|
|
||||||
serviceHostname := fmt.Sprintf("%s.%s", service.Name, service.Namespace)
|
|
||||||
if service.Spec.ClusterIP != "" && service.Spec.ClusterIP != kubClientNullString {
|
|
||||||
resolver.saveResolvedName(service.Spec.ClusterIP, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
if service.Status.LoadBalancer.Ingress != nil {
|
|
||||||
for _, ingress := range service.Status.LoadBalancer.Ingress {
|
|
||||||
resolver.saveResolvedName(ingress.IP, serviceHostname, event.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <- ctx.Done():
|
|
||||||
watcher.Stop()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) saveResolvedName(key string, resolved string, eventType watch.EventType) {
|
|
||||||
if eventType == watch.Deleted {
|
|
||||||
delete(resolver.nameMap, key)
|
|
||||||
// fmt.Printf("setting %s=nil\n", key)
|
|
||||||
} else {
|
|
||||||
resolver.nameMap[key] = resolved
|
|
||||||
// fmt.Printf("setting %s=%s\n", key, resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (resolver *Resolver) infiniteErrorHandleRetryFunc(ctx context.Context, fun func(ctx context.Context) error) {
|
|
||||||
for {
|
|
||||||
err := fun(ctx)
|
|
||||||
if err != nil {
|
|
||||||
resolver.errOut <- err
|
|
||||||
|
|
||||||
var statusError *k8serrors.StatusError
|
|
||||||
if errors.As(err, &statusError) {
|
|
||||||
if statusError.ErrStatus.Reason == metav1.StatusReasonForbidden {
|
|
||||||
fmt.Printf("Resolver loop encountered permission error, aborting event listening - %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ctx.Err() != nil { // context was cancelled or errored
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import "github.com/gofiber/fiber/v2"
|
|
||||||
|
|
||||||
// NotFoundRoute func for describe 404 Error route.
|
|
||||||
func NotFoundRoute(fiberApp *fiber.App) {
|
|
||||||
fiberApp.Use(
|
|
||||||
func(c *fiber.Ctx) error {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
|
||||||
"error": true,
|
|
||||||
"msg": "sorry, endpoint is not found",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"mizuserver/pkg/controllers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EntriesRoutes func for describe group of public routes.
|
|
||||||
func EntriesRoutes(fiberApp *fiber.App) {
|
|
||||||
routeGroup := fiberApp.Group("/api")
|
|
||||||
|
|
||||||
routeGroup.Get("/entries", controllers.GetEntries) // get entries (base/thin entries)
|
|
||||||
routeGroup.Get("/entries/:entryId", controllers.GetEntry) // get single (full) entry
|
|
||||||
|
|
||||||
routeGroup.Get("/har", controllers.GetHARs)
|
|
||||||
routeGroup.Get("/resetDB", controllers.DeleteAllEntries) // get single (full) entry
|
|
||||||
routeGroup.Get("/generalStats", controllers.GetGeneralStats) // get general stats about entries in DB
|
|
||||||
|
|
||||||
routeGroup.Get("/tapStatus", controllers.GetTappingStatus) // get tapping status
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/antoniodipinto/ikisocket"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EventHandlers interface {
|
|
||||||
WebSocketConnect(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketDisconnect(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketClose(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketError(ep *ikisocket.EventPayload)
|
|
||||||
WebSocketMessage(ep *ikisocket.EventPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func WebSocketRoutes(app *fiber.App, eventHandlers EventHandlers) {
|
|
||||||
app.Get("/ws", ikisocket.New(func(kws *ikisocket.Websocket) {
|
|
||||||
kws.SetAttribute("is_tapper", false)
|
|
||||||
}))
|
|
||||||
|
|
||||||
app.Get("/wsTapper", ikisocket.New(func(kws *ikisocket.Websocket) {
|
|
||||||
// Tapper clients are handled differently, they don't need to receive new message broadcasts.
|
|
||||||
kws.SetAttribute("is_tapper", true)
|
|
||||||
}))
|
|
||||||
|
|
||||||
ikisocket.On(ikisocket.EventMessage, eventHandlers.WebSocketMessage)
|
|
||||||
ikisocket.On(ikisocket.EventConnect, eventHandlers.WebSocketConnect)
|
|
||||||
ikisocket.On(ikisocket.EventDisconnect, eventHandlers.WebSocketDisconnect)
|
|
||||||
ikisocket.On(ikisocket.EventClose, eventHandlers.WebSocketClose) // This event is called when the server disconnects the user actively with .Close() method
|
|
||||||
ikisocket.On(ikisocket.EventError, eventHandlers.WebSocketError) // On error event
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package sensitiveDataFiltering
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"mizuserver/pkg/tap"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/beevik/etree"
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FilterSensitiveInfoFromHarRequest(harOutputItem *tap.OutputChannelItem, options *shared.TrafficFilteringOptions) {
|
|
||||||
filterHarHeaders(harOutputItem.HarEntry.Request.Headers)
|
|
||||||
filterHarHeaders(harOutputItem.HarEntry.Response.Headers)
|
|
||||||
|
|
||||||
harOutputItem.HarEntry.Request.Cookies = make([]har.Cookie, 0, 0)
|
|
||||||
harOutputItem.HarEntry.Response.Cookies = make([]har.Cookie, 0, 0)
|
|
||||||
|
|
||||||
harOutputItem.HarEntry.Request.URL = filterUrl(harOutputItem.HarEntry.Request.URL)
|
|
||||||
for i, queryString := range harOutputItem.HarEntry.Request.QueryString {
|
|
||||||
if isFieldNameSensitive(queryString.Name) {
|
|
||||||
harOutputItem.HarEntry.Request.QueryString[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if harOutputItem.HarEntry.Request.PostData != nil {
|
|
||||||
requestContentType := getContentTypeHeaderValue(harOutputItem.HarEntry.Request.Headers)
|
|
||||||
filteredRequestBody, err := filterHttpBody([]byte(harOutputItem.HarEntry.Request.PostData.Text), requestContentType, options)
|
|
||||||
if err == nil {
|
|
||||||
harOutputItem.HarEntry.Request.PostData.Text = string(filteredRequestBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if harOutputItem.HarEntry.Response.Content != nil {
|
|
||||||
responseContentType := getContentTypeHeaderValue(harOutputItem.HarEntry.Response.Headers)
|
|
||||||
filteredResponseBody, err := filterHttpBody(harOutputItem.HarEntry.Response.Content.Text, responseContentType, options)
|
|
||||||
if err == nil {
|
|
||||||
harOutputItem.HarEntry.Response.Content.Text = filteredResponseBody
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHarHeaders(headers []har.Header) {
|
|
||||||
for i, header := range headers {
|
|
||||||
if isFieldNameSensitive(header.Name) {
|
|
||||||
headers[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContentTypeHeaderValue(headers []har.Header) string {
|
|
||||||
for _, header := range headers {
|
|
||||||
if strings.ToLower(header.Name) == "content-type" {
|
|
||||||
return header.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isFieldNameSensitive(fieldName string) bool {
|
|
||||||
name := strings.ToLower(fieldName)
|
|
||||||
name = strings.ReplaceAll(name, "_", "")
|
|
||||||
name = strings.ReplaceAll(name, "-", "")
|
|
||||||
name = strings.ReplaceAll(name, " ", "")
|
|
||||||
|
|
||||||
for _, sensitiveField := range personallyIdentifiableDataFields {
|
|
||||||
if strings.Contains(name, sensitiveField) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterHttpBody(bytes []byte, contentType string, options *shared.TrafficFilteringOptions) ([]byte, error) {
|
|
||||||
mimeType := strings.Split(contentType, ";")[0]
|
|
||||||
switch strings.ToLower(mimeType) {
|
|
||||||
case "application/json":
|
|
||||||
return filterJsonBody(bytes)
|
|
||||||
case "text/html":
|
|
||||||
fallthrough
|
|
||||||
case "application/xhtml+xml":
|
|
||||||
fallthrough
|
|
||||||
case "text/xml":
|
|
||||||
fallthrough
|
|
||||||
case "application/xml":
|
|
||||||
return filterXmlEtree(bytes)
|
|
||||||
case "text/plain":
|
|
||||||
if options != nil && options.PlainTextMaskingRegexes != nil {
|
|
||||||
return filterPlainText(bytes, options), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bytes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterPlainText(bytes []byte, options *shared.TrafficFilteringOptions) []byte {
|
|
||||||
for _, regex := range options.PlainTextMaskingRegexes {
|
|
||||||
bytes = regex.ReplaceAll(bytes, []byte(maskedFieldPlaceholderValue))
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterXmlEtree(bytes []byte) ([]byte, error) {
|
|
||||||
if !IsValidXML(bytes) {
|
|
||||||
return nil, errors.New("Invalid XML")
|
|
||||||
}
|
|
||||||
xmlDoc := etree.NewDocument()
|
|
||||||
err := xmlDoc.ReadFromBytes(bytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else {
|
|
||||||
filterXmlElement(xmlDoc.Root())
|
|
||||||
}
|
|
||||||
return xmlDoc.WriteToBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsValidXML(data []byte) bool {
|
|
||||||
return xml.Unmarshal(data, new(interface{})) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterXmlElement(element *etree.Element) {
|
|
||||||
for i, attribute := range element.Attr {
|
|
||||||
if isFieldNameSensitive(attribute.Key) {
|
|
||||||
element.Attr[i].Value = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if element.ChildElements() == nil || len(element.ChildElements()) == 0 {
|
|
||||||
if isFieldNameSensitive(element.Tag) {
|
|
||||||
element.SetText(maskedFieldPlaceholderValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, element := range element.ChildElements() {
|
|
||||||
filterXmlElement(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterJsonBody(bytes []byte) ([]byte, error) {
|
|
||||||
var bodyJsonMap map[string] interface{}
|
|
||||||
err := json.Unmarshal(bytes ,&bodyJsonMap)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
filterJsonMap(bodyJsonMap)
|
|
||||||
return json.Marshal(bodyJsonMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterJsonMap(jsonMap map[string] interface{}) {
|
|
||||||
for key, value := range jsonMap {
|
|
||||||
if value == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nestedMap, isNested := value.(map[string] interface{})
|
|
||||||
if isNested {
|
|
||||||
filterJsonMap(nestedMap)
|
|
||||||
} else {
|
|
||||||
if isFieldNameSensitive(key) {
|
|
||||||
jsonMap[key] = maskedFieldPlaceholderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// receives string representing url, returns string url without sensitive query param values (http://service/api?userId=bob&password=123&type=login -> http://service/api?userId=[REDACTED]&password=[REDACTED]&type=login)
|
|
||||||
func filterUrl(originalUrl string) string {
|
|
||||||
parsedUrl, err := url.Parse(originalUrl)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Sprintf("http://%s", maskedFieldPlaceholderValue)
|
|
||||||
} else {
|
|
||||||
if len(parsedUrl.RawQuery) > 0 {
|
|
||||||
newQueryArgs := make([]string, 0)
|
|
||||||
for urlQueryParamName, urlQueryParamValues := range parsedUrl.Query() {
|
|
||||||
newValues := urlQueryParamValues
|
|
||||||
if isFieldNameSensitive(urlQueryParamName) {
|
|
||||||
newValues = []string {maskedFieldPlaceholderValue}
|
|
||||||
}
|
|
||||||
for _, paramValue := range newValues {
|
|
||||||
newQueryArgs = append(newQueryArgs, fmt.Sprintf("%s=%s", urlQueryParamName, paramValue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedUrl.RawQuery = strings.Join(newQueryArgs, "&")
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedUrl.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,252 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/martian/har"
|
|
||||||
)
|
|
||||||
|
|
||||||
const readPermission = 0644
|
|
||||||
const tempFilenamePrefix = "har_writer"
|
|
||||||
|
|
||||||
type PairChanItem struct {
|
|
||||||
Request *http.Request
|
|
||||||
RequestTime time.Time
|
|
||||||
Response *http.Response
|
|
||||||
ResponseTime time.Time
|
|
||||||
RequestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
func openNewHarFile(filename string) *HarFile {
|
|
||||||
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, readPermission)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to open output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
harFile := HarFile{file: file, entryCount: 0}
|
|
||||||
harFile.writeHeader()
|
|
||||||
|
|
||||||
return &harFile
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarFile struct {
|
|
||||||
file *os.File
|
|
||||||
entryCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEntry(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time) (*har.Entry, error) {
|
|
||||||
harRequest, err := har.NewRequest(request, true)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-request-to-har", "Failed converting request to HAR %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting request to HAR")
|
|
||||||
}
|
|
||||||
|
|
||||||
harResponse, err := har.NewResponse(response, true)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-response-to-har", "Failed converting response to HAR %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting response to HAR")
|
|
||||||
}
|
|
||||||
|
|
||||||
if harRequest.PostData != nil && strings.HasPrefix(harRequest.PostData.MimeType, "application/grpc") {
|
|
||||||
// Force HTTP/2 gRPC into HAR template
|
|
||||||
|
|
||||||
harRequest.URL = fmt.Sprintf("%s://%s%s", request.Header.Get(":scheme"), request.Header.Get(":authority"), request.Header.Get(":path"))
|
|
||||||
|
|
||||||
status, err := strconv.Atoi(response.Header.Get(":status"))
|
|
||||||
if err != nil {
|
|
||||||
SilentError("convert-response-status-for-har", "Failed converting status to int %s (%v,%+v)\n", err, err, err)
|
|
||||||
return nil, errors.New("Failed converting response status to int for HAR")
|
|
||||||
}
|
|
||||||
harResponse.Status = status
|
|
||||||
} else {
|
|
||||||
// Martian copies http.Request.URL.String() to har.Request.URL, which usually contains the path.
|
|
||||||
// However, according to the HAR spec, the URL field needs to be the absolute URL.
|
|
||||||
var scheme string
|
|
||||||
if request.URL.Scheme != "" {
|
|
||||||
scheme = request.URL.Scheme
|
|
||||||
} else {
|
|
||||||
scheme = "http"
|
|
||||||
}
|
|
||||||
harRequest.URL = fmt.Sprintf("%s://%s%s", scheme, request.Host, request.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTime := responseTime.Sub(requestTime).Round(time.Millisecond).Milliseconds()
|
|
||||||
if totalTime < 1 {
|
|
||||||
totalTime = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
harEntry := har.Entry{
|
|
||||||
StartedDateTime: time.Now().UTC(),
|
|
||||||
Time: totalTime,
|
|
||||||
Request: harRequest,
|
|
||||||
Response: harResponse,
|
|
||||||
Cache: &har.Cache{},
|
|
||||||
Timings: &har.Timings{
|
|
||||||
Send: -1,
|
|
||||||
Wait: -1,
|
|
||||||
Receive: totalTime,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &harEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) WriteEntry(harEntry *har.Entry) {
|
|
||||||
harEntryJson, err := json.Marshal(harEntry)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("har-entry-marshal", "Failed converting har entry object to JSON%s (%v,%+v)\n", err, err, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var separator string
|
|
||||||
if f.GetEntryCount() > 0 {
|
|
||||||
separator = ","
|
|
||||||
} else {
|
|
||||||
separator = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
harEntryString := append([]byte(separator), harEntryJson...)
|
|
||||||
|
|
||||||
if _, err := f.file.Write(harEntryString); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
f.entryCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) GetEntryCount() int {
|
|
||||||
return f.entryCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *HarFile) Close() {
|
|
||||||
f.writeTrailer()
|
|
||||||
|
|
||||||
err := f.file.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to close output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f*HarFile) writeHeader() {
|
|
||||||
header := []byte(`{"log": {"version": "1.2", "creator": {"name": "Mizu", "version": "0.0.1"}, "entries": [`)
|
|
||||||
if _, err := f.file.Write(header); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write header to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f*HarFile) writeTrailer() {
|
|
||||||
trailer := []byte("]}}")
|
|
||||||
if _, err := f.file.Write(trailer); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to write trailer to output file: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHarWriter(outputDir string, maxEntries int) *HarWriter {
|
|
||||||
return &HarWriter{
|
|
||||||
OutputDirPath: outputDir,
|
|
||||||
MaxEntries: maxEntries,
|
|
||||||
PairChan: make(chan *PairChanItem),
|
|
||||||
OutChan: make(chan *OutputChannelItem, 1000),
|
|
||||||
currentFile: nil,
|
|
||||||
done: make(chan bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutputChannelItem struct {
|
|
||||||
HarEntry *har.Entry
|
|
||||||
RequestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HarWriter struct {
|
|
||||||
OutputDirPath string
|
|
||||||
MaxEntries int
|
|
||||||
PairChan chan *PairChanItem
|
|
||||||
OutChan chan *OutputChannelItem
|
|
||||||
currentFile *HarFile
|
|
||||||
done chan bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) WritePair(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time, requestSenderIp string) {
|
|
||||||
hw.PairChan <- &PairChanItem{
|
|
||||||
Request: request,
|
|
||||||
RequestTime: requestTime,
|
|
||||||
Response: response,
|
|
||||||
ResponseTime: responseTime,
|
|
||||||
RequestSenderIp: requestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) Start() {
|
|
||||||
if hw.OutputDirPath != "" {
|
|
||||||
if err := os.MkdirAll(hw.OutputDirPath, os.ModePerm); err != nil {
|
|
||||||
panic(fmt.Sprintf("Failed to create output directory: %s (%v,%+v)", err, err, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for pair := range hw.PairChan {
|
|
||||||
harEntry, err := NewEntry(pair.Request, pair.RequestTime, pair.Response, pair.ResponseTime)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if hw.OutputDirPath != "" {
|
|
||||||
if hw.currentFile == nil {
|
|
||||||
hw.openNewFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
hw.currentFile.WriteEntry(harEntry)
|
|
||||||
|
|
||||||
if hw.currentFile.GetEntryCount() >= hw.MaxEntries {
|
|
||||||
hw.closeFile()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hw.OutChan <- &OutputChannelItem{
|
|
||||||
HarEntry: harEntry,
|
|
||||||
RequestSenderIp: pair.RequestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hw.currentFile != nil {
|
|
||||||
hw.closeFile()
|
|
||||||
}
|
|
||||||
hw.done <- true
|
|
||||||
} ()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) Stop() {
|
|
||||||
close(hw.PairChan)
|
|
||||||
<-hw.done
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) openNewFile() {
|
|
||||||
filename := filepath.Join(os.TempDir(), fmt.Sprintf("%s_%d", tempFilenamePrefix, time.Now().UnixNano()))
|
|
||||||
hw.currentFile = openNewHarFile(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hw *HarWriter) closeFile() {
|
|
||||||
hw.currentFile.Close()
|
|
||||||
tmpFilename := hw.currentFile.file.Name()
|
|
||||||
hw.currentFile = nil
|
|
||||||
|
|
||||||
filename := buildFilename(hw.OutputDirPath, time.Now())
|
|
||||||
err := os.Rename(tmpFilename, filename)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("Rename-file", "cannot rename file: %s (%v,%+v)\n", err, err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildFilename(dir string, t time.Time) string {
|
|
||||||
// (epoch time in nanoseconds)__(YYYY_Month_DD__hh-mm-ss).har
|
|
||||||
filename := fmt.Sprintf("%d__%s.har", t.UnixNano(), t.Format("2006_Jan_02__15-04-05"))
|
|
||||||
return filepath.Join(dir, filename)
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
|
||||||
)
|
|
||||||
|
|
||||||
type requestResponsePair struct {
|
|
||||||
Request httpMessage `json:"request"`
|
|
||||||
Response httpMessage `json:"response"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type envoyMessageWrapper struct {
|
|
||||||
HttpBufferedTrace requestResponsePair `json:"http_buffered_trace"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type headerKeyVal struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type messageBody struct {
|
|
||||||
Truncated bool `json:"truncated"`
|
|
||||||
AsBytes string `json:"as_bytes"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpMessage struct {
|
|
||||||
IsRequest bool
|
|
||||||
Headers []headerKeyVal `json:"headers"`
|
|
||||||
HTTPVersion string `json:"httpVersion"`
|
|
||||||
Body messageBody `json:"body"`
|
|
||||||
captureTime time.Time
|
|
||||||
orig interface {}
|
|
||||||
requestSenderIp string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Key is {client_addr}:{client_port}->{dest_addr}:{dest_port}
|
|
||||||
type requestResponseMatcher struct {
|
|
||||||
openMessagesMap cmap.ConcurrentMap
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func createResponseRequestMatcher() requestResponseMatcher {
|
|
||||||
newMatcher := &requestResponseMatcher{openMessagesMap: cmap.New()}
|
|
||||||
return *newMatcher
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) registerRequest(ident string, request *http.Request, captureTime time.Time, body string, isHTTP2 bool) *envoyMessageWrapper {
|
|
||||||
split := splitIdent(ident)
|
|
||||||
key := genKey(split)
|
|
||||||
|
|
||||||
messageExtraHeaders := []headerKeyVal{
|
|
||||||
{Key: "x-up9-source", Value: split[0]},
|
|
||||||
{Key: "x-up9-destination", Value: split[1] + ":" + split[3]},
|
|
||||||
}
|
|
||||||
|
|
||||||
requestHTTPMessage := requestToMessage(request, captureTime, body, &messageExtraHeaders, isHTTP2, split[0])
|
|
||||||
|
|
||||||
if response, found := matcher.openMessagesMap.Pop(key); found {
|
|
||||||
// Type assertion always succeeds because all of the map's values are of httpMessage type
|
|
||||||
responseHTTPMessage := response.(*httpMessage)
|
|
||||||
if responseHTTPMessage.IsRequest {
|
|
||||||
SilentError("Request-Duplicate", "Got duplicate request with same identifier\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Debug("Matched open Response for %s\n", key)
|
|
||||||
return matcher.preparePair(&requestHTTPMessage, responseHTTPMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher.openMessagesMap.Set(key, &requestHTTPMessage)
|
|
||||||
Debug("Registered open Request for %s\n", key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) registerResponse(ident string, response *http.Response, captureTime time.Time, body string, isHTTP2 bool) *envoyMessageWrapper {
|
|
||||||
split := splitIdent(ident)
|
|
||||||
key := genKey(split)
|
|
||||||
|
|
||||||
responseHTTPMessage := responseToMessage(response, captureTime, body, isHTTP2)
|
|
||||||
|
|
||||||
if request, found := matcher.openMessagesMap.Pop(key); found {
|
|
||||||
// Type assertion always succeeds because all of the map's values are of httpMessage type
|
|
||||||
requestHTTPMessage := request.(*httpMessage)
|
|
||||||
if !requestHTTPMessage.IsRequest {
|
|
||||||
SilentError("Response-Duplicate", "Got duplicate response with same identifier\n")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Debug("Matched open Request for %s\n", key)
|
|
||||||
return matcher.preparePair(requestHTTPMessage, &responseHTTPMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher.openMessagesMap.Set(key, &responseHTTPMessage)
|
|
||||||
Debug("Registered open Response for %s\n", key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) preparePair(requestHTTPMessage *httpMessage, responseHTTPMessage *httpMessage) *envoyMessageWrapper {
|
|
||||||
matcher.addDuration(requestHTTPMessage, responseHTTPMessage)
|
|
||||||
|
|
||||||
return &envoyMessageWrapper{
|
|
||||||
HttpBufferedTrace: requestResponsePair{
|
|
||||||
Request: *requestHTTPMessage,
|
|
||||||
Response: *responseHTTPMessage,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestToMessage(request *http.Request, captureTime time.Time, body string, messageExtraHeaders *[]headerKeyVal, isHTTP2 bool, requestSenderIp string) httpMessage {
|
|
||||||
messageHeaders := make([]headerKeyVal, 0)
|
|
||||||
|
|
||||||
for key, value := range request.Header {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: key, Value: value[0]})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isHTTP2 {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":method", Value: request.Method})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":path", Value: request.RequestURI})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":authority", Value: request.Host})
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":scheme", Value: "http"})
|
|
||||||
}
|
|
||||||
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: "x-request-start", Value: fmt.Sprintf("%.3f", float64(captureTime.UnixNano()) / float64(1000000000))})
|
|
||||||
|
|
||||||
messageHeaders = append(messageHeaders, *messageExtraHeaders...)
|
|
||||||
|
|
||||||
httpVersion := request.Proto
|
|
||||||
|
|
||||||
requestBody := messageBody{Truncated: false, AsBytes: body}
|
|
||||||
|
|
||||||
return httpMessage{
|
|
||||||
IsRequest: true,
|
|
||||||
Headers: messageHeaders,
|
|
||||||
HTTPVersion: httpVersion,
|
|
||||||
Body: requestBody,
|
|
||||||
captureTime: captureTime,
|
|
||||||
orig: request,
|
|
||||||
requestSenderIp: requestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func responseToMessage(response *http.Response, captureTime time.Time, body string, isHTTP2 bool) httpMessage {
|
|
||||||
messageHeaders := make([]headerKeyVal, 0)
|
|
||||||
|
|
||||||
for key, value := range response.Header {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: key, Value: value[0]})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isHTTP2 {
|
|
||||||
messageHeaders = append(messageHeaders, headerKeyVal{Key: ":status", Value: strconv.Itoa(response.StatusCode)})
|
|
||||||
}
|
|
||||||
|
|
||||||
httpVersion := response.Proto
|
|
||||||
|
|
||||||
requestBody := messageBody{Truncated: false, AsBytes: body}
|
|
||||||
|
|
||||||
return httpMessage{
|
|
||||||
IsRequest: false,
|
|
||||||
Headers: messageHeaders,
|
|
||||||
HTTPVersion: httpVersion,
|
|
||||||
Body: requestBody,
|
|
||||||
captureTime: captureTime,
|
|
||||||
orig: response,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) addDuration(requestHTTPMessage *httpMessage, responseHTTPMessage *httpMessage) {
|
|
||||||
durationMs := float64(responseHTTPMessage.captureTime.UnixNano() / 1000000) - float64(requestHTTPMessage.captureTime.UnixNano() / 1000000)
|
|
||||||
if durationMs < 1 {
|
|
||||||
durationMs = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
responseHTTPMessage.Headers = append(responseHTTPMessage.Headers, headerKeyVal{Key: "x-up9-duration-ms", Value: fmt.Sprintf("%.0f", durationMs)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitIdent(ident string) []string {
|
|
||||||
ident = strings.Replace(ident, "->", " ", -1)
|
|
||||||
return strings.Split(ident, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func genKey(split []string) string {
|
|
||||||
key := fmt.Sprintf("%s:%s->%s:%s,%s", split[0], split[2], split[1], split[3], split[4])
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func (matcher *requestResponseMatcher) deleteOlderThan(t time.Time) int {
|
|
||||||
keysToPop := make([]string, 0)
|
|
||||||
for item := range matcher.openMessagesMap.IterBuffered() {
|
|
||||||
// Map only contains values of type httpMessage
|
|
||||||
message, _ := item.Val.(*httpMessage)
|
|
||||||
|
|
||||||
if message.captureTime.Before(t) {
|
|
||||||
keysToPop = append(keysToPop, item.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
numDeleted := len(keysToPop)
|
|
||||||
|
|
||||||
for _, key := range keysToPop {
|
|
||||||
_, _ = matcher.openMessagesMap.Pop(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return numDeleted
|
|
||||||
}
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
b64 "encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpReaderDataMsg struct {
|
|
||||||
bytes []byte
|
|
||||||
timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type tcpID struct {
|
|
||||||
srcIP string
|
|
||||||
dstIP string
|
|
||||||
srcPort string
|
|
||||||
dstPort string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tid *tcpID) String() string {
|
|
||||||
return fmt.Sprintf("%s->%s %s->%s", tid.srcIP, tid.dstIP, tid.srcPort, tid.dstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* httpReader gets reads from a channel of bytes of tcp payload, and parses it into HTTP/1 requests and responses.
|
|
||||||
* The payload is written to the channel by a tcpStream object that is dedicated to one tcp connection.
|
|
||||||
* An httpReader object is unidirectional: it parses either a client stream or a server stream.
|
|
||||||
* Implements io.Reader interface (Read)
|
|
||||||
*/
|
|
||||||
type httpReader struct {
|
|
||||||
ident string
|
|
||||||
tcpID tcpID
|
|
||||||
isClient bool
|
|
||||||
isHTTP2 bool
|
|
||||||
msgQueue chan httpReaderDataMsg // Channel of captured reassembled tcp payload
|
|
||||||
data []byte
|
|
||||||
captureTime time.Time
|
|
||||||
hexdump bool
|
|
||||||
parent *tcpStream
|
|
||||||
grpcAssembler GrpcAssembler
|
|
||||||
messageCount uint
|
|
||||||
harWriter *HarWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) Read(p []byte) (int, error) {
|
|
||||||
var msg httpReaderDataMsg
|
|
||||||
ok := true
|
|
||||||
for ok && len(h.data) == 0 {
|
|
||||||
msg, ok = <-h.msgQueue
|
|
||||||
h.data = msg.bytes
|
|
||||||
h.captureTime = msg.timestamp
|
|
||||||
}
|
|
||||||
if !ok || len(h.data) == 0 {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
l := copy(p, h.data)
|
|
||||||
h.data = h.data[l:]
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) run(wg *sync.WaitGroup) {
|
|
||||||
defer wg.Done()
|
|
||||||
b := bufio.NewReader(h)
|
|
||||||
|
|
||||||
if isHTTP2, err := checkIsHTTP2Connection(b, h.isClient); err != nil {
|
|
||||||
SilentError("HTTP/2-Prepare-Connection", "stream %s Failed to check if client is HTTP/2: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
// Do something?
|
|
||||||
} else {
|
|
||||||
h.isHTTP2 = isHTTP2
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.isHTTP2 {
|
|
||||||
err := prepareHTTP2Connection(b, h.isClient)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP/2-Prepare-Connection-After-Check", "stream %s error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
}
|
|
||||||
h.grpcAssembler = createGrpcAssembler(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
for true {
|
|
||||||
if h.isHTTP2 {
|
|
||||||
err := h.handleHTTP2Stream()
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP/2", "stream %s error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if h.isClient {
|
|
||||||
err := h.handleHTTP1ClientStream(b)
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP-request", "stream %s Request error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err := h.handleHTTP1ServerStream(b)
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
SilentError("HTTP-response", "stream %s Response error: %s (%v,%+v)\n", h.ident, err, err, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP2Stream() error {
|
|
||||||
streamID, messageHTTP1, body, err := h.grpcAssembler.readMessage()
|
|
||||||
h.messageCount++
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var reqResPair *envoyMessageWrapper
|
|
||||||
|
|
||||||
switch messageHTTP1 := messageHTTP1.(type) {
|
|
||||||
case http.Request:
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.srcIP, h.tcpID.dstIP, h.tcpID.srcPort, h.tcpID.dstPort, streamID)
|
|
||||||
reqResPair = reqResMatcher.registerRequest(ident, &messageHTTP1, h.captureTime, body, true)
|
|
||||||
case http.Response:
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.dstIP, h.tcpID.srcIP, h.tcpID.dstPort, h.tcpID.srcPort, streamID)
|
|
||||||
reqResPair = reqResMatcher.registerResponse(ident, &messageHTTP1, h.captureTime, body, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP1ClientStream(b *bufio.Reader) error {
|
|
||||||
req, err := http.ReadRequest(b)
|
|
||||||
h.messageCount++
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(req.Body)
|
|
||||||
req.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
|
||||||
s := len(body)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-request-body", "stream %s Got body err: %s\n", h.ident, err)
|
|
||||||
} else if h.hexdump {
|
|
||||||
Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
|
|
||||||
}
|
|
||||||
if err := req.Body.Close(); err != nil {
|
|
||||||
SilentError("HTTP-request-body-close", "stream %s Failed to close request body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
encoding := req.Header["Content-Encoding"]
|
|
||||||
bodyStr, err := readBody(body, encoding)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-request-body-decode", "stream %s Failed to decode body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
Info("HTTP/%s Request: %s %s (Body:%d)\n", h.ident, req.Method, req.URL, s)
|
|
||||||
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.srcIP, h.tcpID.dstIP, h.tcpID.srcPort, h.tcpID.dstPort, h.messageCount)
|
|
||||||
reqResPair := reqResMatcher.registerRequest(ident, req, h.captureTime, bodyStr, false)
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-marshal", "stream %s Error convert request response to json: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h.parent.Lock()
|
|
||||||
h.parent.urls = append(h.parent.urls, req.URL.String())
|
|
||||||
h.parent.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpReader) handleHTTP1ServerStream(b *bufio.Reader) error {
|
|
||||||
res, err := http.ReadResponse(b, nil)
|
|
||||||
h.messageCount++
|
|
||||||
var req string
|
|
||||||
h.parent.Lock()
|
|
||||||
if len(h.parent.urls) == 0 {
|
|
||||||
req = fmt.Sprintf("<no-request-seen>")
|
|
||||||
} else {
|
|
||||||
req, h.parent.urls = h.parent.urls[0], h.parent.urls[1:]
|
|
||||||
}
|
|
||||||
h.parent.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
|
||||||
res.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
|
|
||||||
s := len(body)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-response-body", "HTTP/%s: failed to get body(parsed len:%d): %s\n", h.ident, s, err)
|
|
||||||
}
|
|
||||||
if h.hexdump {
|
|
||||||
Info("Body(%d/0x%x)\n%s\n", len(body), len(body), hex.Dump(body))
|
|
||||||
}
|
|
||||||
if err := res.Body.Close(); err != nil {
|
|
||||||
SilentError("HTTP-response-body-close", "HTTP/%s: failed to close body(parsed len:%d): %s\n", h.ident, s, err)
|
|
||||||
}
|
|
||||||
sym := ","
|
|
||||||
if res.ContentLength > 0 && res.ContentLength != int64(s) {
|
|
||||||
sym = "!="
|
|
||||||
}
|
|
||||||
contentType, ok := res.Header["Content-Type"]
|
|
||||||
if !ok {
|
|
||||||
contentType = []string{http.DetectContentType(body)}
|
|
||||||
}
|
|
||||||
encoding := res.Header["Content-Encoding"]
|
|
||||||
Info("HTTP/%s Response: %s URL:%s (%d%s%d%s) -> %s\n", h.ident, res.Status, req, res.ContentLength, sym, s, contentType, encoding)
|
|
||||||
bodyStr, err := readBody(body, encoding)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-response-body-decode", "stream %s Failed to decode body: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ident := fmt.Sprintf("%s->%s %s->%s %d", h.tcpID.dstIP, h.tcpID.srcIP, h.tcpID.dstPort, h.tcpID.srcPort, h.messageCount)
|
|
||||||
reqResPair := reqResMatcher.registerResponse(ident, res, h.captureTime, bodyStr, false)
|
|
||||||
if reqResPair != nil {
|
|
||||||
if h.harWriter != nil {
|
|
||||||
h.harWriter.WritePair(
|
|
||||||
reqResPair.HttpBufferedTrace.Request.orig.(*http.Request),
|
|
||||||
reqResPair.HttpBufferedTrace.Request.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Response.orig.(*http.Response),
|
|
||||||
reqResPair.HttpBufferedTrace.Response.captureTime,
|
|
||||||
reqResPair.HttpBufferedTrace.Request.requestSenderIp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
jsonStr, err := json.Marshal(reqResPair)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-marshal", "stream %s Error convert request response to json: %s\n", h.ident, err)
|
|
||||||
}
|
|
||||||
broadcastReqResPair(jsonStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBody(bodyBytes []byte, encoding []string) (string, error) {
|
|
||||||
var bodyBuffer io.Reader
|
|
||||||
bodyBuffer = bytes.NewBuffer(bodyBytes)
|
|
||||||
var err error
|
|
||||||
if len(encoding) > 0 && (encoding[0] == "gzip" || encoding[0] == "deflate") {
|
|
||||||
bodyBuffer, err = gzip.NewReader(bodyBuffer)
|
|
||||||
if err != nil {
|
|
||||||
SilentError("HTTP-gunzip", "Failed to gzip decode: %s\n", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, ok := bodyBuffer.(*gzip.Reader); ok {
|
|
||||||
err = bodyBuffer.(*gzip.Reader).Close()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
_, err = buf.ReadFrom(bodyBuffer)
|
|
||||||
return b64.StdEncoding.EncodeToString(buf.Bytes()), err
|
|
||||||
}
|
|
||||||
@@ -1,530 +0,0 @@
|
|||||||
// Copyright 2012 Google, Inc. All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license
|
|
||||||
// that can be found in the LICENSE file in the root of the source
|
|
||||||
// tree.
|
|
||||||
|
|
||||||
// The pcapdump binary implements a tcpdump-like command line tool with gopacket
|
|
||||||
// using pcap as a backend data collection mechanism.
|
|
||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/up9inc/mizu/shared"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"runtime"
|
|
||||||
"runtime/pprof"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/examples/util"
|
|
||||||
"github.com/google/gopacket/ip4defrag"
|
|
||||||
"github.com/google/gopacket/layers" // pulls in all layers decoders
|
|
||||||
"github.com/google/gopacket/pcap"
|
|
||||||
"github.com/google/gopacket/reassembly"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AppPortsEnvVar = "APP_PORTS"
|
|
||||||
const OutPortEnvVar = "WEB_SOCKET_PORT"
|
|
||||||
const maxHTTP2DataLenEnvVar = "HTTP2_DATA_SIZE_LIMIT"
|
|
||||||
// default is 1MB, more than the max size accepted by collector and traffic-dumper
|
|
||||||
const maxHTTP2DataLenDefault = 1 * 1024 * 1024
|
|
||||||
const cleanPeriod = time.Second * 10
|
|
||||||
const outboundThrottleCacheExpiryPeriod = time.Minute * 15
|
|
||||||
var remoteOnlyOutboundPorts = []int { 80, 443 }
|
|
||||||
|
|
||||||
func parseAppPorts(appPortsList string) []int {
|
|
||||||
ports := make([]int, 0)
|
|
||||||
for _, portStr := range strings.Split(appPortsList, ",") {
|
|
||||||
parsedInt, parseError := strconv.Atoi(portStr)
|
|
||||||
if parseError != nil {
|
|
||||||
fmt.Println("Provided app port ", portStr, " is not a valid number!")
|
|
||||||
} else {
|
|
||||||
ports = append(ports, parsedInt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ports
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHostAppAddresses(hostAppAddressesString string) []string {
|
|
||||||
if len(hostAppAddressesString) == 0 {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return strings.Split(hostAppAddressesString, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxcount = flag.Int("c", -1, "Only grab this many packets, then exit")
|
|
||||||
var decoder = flag.String("decoder", "", "Name of the decoder to use (default: guess from capture)")
|
|
||||||
var statsevery = flag.Int("stats", 60, "Output statistics every N seconds")
|
|
||||||
var lazy = flag.Bool("lazy", false, "If true, do lazy decoding")
|
|
||||||
var nodefrag = flag.Bool("nodefrag", false, "If true, do not do IPv4 defrag")
|
|
||||||
var checksum = flag.Bool("checksum", false, "Check TCP checksum") // global
|
|
||||||
var nooptcheck = flag.Bool("nooptcheck", true, "Do not check TCP options (useful to ignore MSS on captures with TSO)") // global
|
|
||||||
var ignorefsmerr = flag.Bool("ignorefsmerr", true, "Ignore TCP FSM errors") // global
|
|
||||||
var allowmissinginit = flag.Bool("allowmissinginit", true, "Support streams without SYN/SYN+ACK/ACK sequence") // global
|
|
||||||
var verbose = flag.Bool("verbose", false, "Be verbose")
|
|
||||||
var debug = flag.Bool("debug", false, "Display debug information")
|
|
||||||
var quiet = flag.Bool("quiet", false, "Be quiet regarding errors")
|
|
||||||
|
|
||||||
// http
|
|
||||||
var nohttp = flag.Bool("nohttp", false, "Disable HTTP parsing")
|
|
||||||
var output = flag.String("output", "", "Path to create file for HTTP 200 OK responses")
|
|
||||||
var writeincomplete = flag.Bool("writeincomplete", false, "Write incomplete response")
|
|
||||||
|
|
||||||
var hexdump = flag.Bool("dump", false, "Dump HTTP request/response as hex") // global
|
|
||||||
var hexdumppkt = flag.Bool("dumppkt", false, "Dump packet as hex")
|
|
||||||
|
|
||||||
// capture
|
|
||||||
var iface = flag.String("i", "en0", "Interface to read packets from")
|
|
||||||
var fname = flag.String("r", "", "Filename to read from, overrides -i")
|
|
||||||
var snaplen = flag.Int("s", 65536, "Snap length (number of bytes max to read per packet")
|
|
||||||
var tstype = flag.String("timestamp_type", "", "Type of timestamps to use")
|
|
||||||
var promisc = flag.Bool("promisc", true, "Set promiscuous mode")
|
|
||||||
var anydirection = flag.Bool("anydirection", false, "Capture http requests to other hosts")
|
|
||||||
var staleTimeoutSeconds = flag.Int("staletimout", 120, "Max time in seconds to keep connections which don't transmit data")
|
|
||||||
var hostAppAddressesString = flag.String("targets", "", "Comma separated list of ip:ports to tap")
|
|
||||||
|
|
||||||
var memprofile = flag.String("memprofile", "", "Write memory profile")
|
|
||||||
|
|
||||||
// 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 harEntriesPerFile = flag.Int("harentriesperfile", 200, "Number of max number of har entries to store in each file")
|
|
||||||
|
|
||||||
var reqResMatcher = createResponseRequestMatcher() // global
|
|
||||||
var statsTracker = StatsTracker{}
|
|
||||||
|
|
||||||
// global
|
|
||||||
var stats struct {
|
|
||||||
ipdefrag int
|
|
||||||
missedBytes int
|
|
||||||
pkt int
|
|
||||||
sz int
|
|
||||||
totalsz int
|
|
||||||
rejectFsm int
|
|
||||||
rejectOpt int
|
|
||||||
rejectConnFsm int
|
|
||||||
reassembled int
|
|
||||||
outOfOrderBytes int
|
|
||||||
outOfOrderPackets int
|
|
||||||
biggestChunkBytes int
|
|
||||||
biggestChunkPackets int
|
|
||||||
overlapBytes int
|
|
||||||
overlapPackets int
|
|
||||||
}
|
|
||||||
|
|
||||||
type CollectorMessage struct {
|
|
||||||
MessageType string
|
|
||||||
Ports *[]int `json:"ports,omitempty"`
|
|
||||||
Addresses *[]string `json:"addresses,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputLevel int
|
|
||||||
var errorsMap map[string]uint
|
|
||||||
var errorsMapMutex sync.Mutex
|
|
||||||
var nErrors uint
|
|
||||||
var appPorts []int // global
|
|
||||||
var ownIps []string //global
|
|
||||||
var hostMode bool //global
|
|
||||||
var HostAppAddresses []string //global
|
|
||||||
|
|
||||||
/* minOutputLevel: Error will be printed only if outputLevel is above this value
|
|
||||||
* t: key for errorsMap (counting errors)
|
|
||||||
* s, a: arguments fmt.Printf
|
|
||||||
* Note: Too bad for perf that a... is evaluated
|
|
||||||
*/
|
|
||||||
func logError(minOutputLevel int, t string, s string, a ...interface{}) {
|
|
||||||
errorsMapMutex.Lock()
|
|
||||||
nErrors++
|
|
||||||
nb, _ := errorsMap[t]
|
|
||||||
errorsMap[t] = nb + 1
|
|
||||||
errorsMapMutex.Unlock()
|
|
||||||
if outputLevel >= minOutputLevel {
|
|
||||||
formatStr := fmt.Sprintf("%s: %s", t, s)
|
|
||||||
fmt.Printf(formatStr, a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func Error(t string, s string, a ...interface{}) {
|
|
||||||
logError(0, t, s, a...)
|
|
||||||
}
|
|
||||||
func SilentError(t string, s string, a ...interface{}) {
|
|
||||||
logError(2, t, s, a...)
|
|
||||||
}
|
|
||||||
func Info(s string, a ...interface{}) {
|
|
||||||
if outputLevel >= 1 {
|
|
||||||
fmt.Printf(s, a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func Debug(s string, a ...interface{}) {
|
|
||||||
if outputLevel >= 2 {
|
|
||||||
fmt.Printf(s, a...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func inArrayInt(arr []int, valueToCheck int) bool {
|
|
||||||
for _, value := range arr {
|
|
||||||
if value == valueToCheck {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func inArrayString(arr []string, valueToCheck string) bool {
|
|
||||||
for _, value := range arr {
|
|
||||||
if value == valueToCheck {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The assembler context
|
|
||||||
*/
|
|
||||||
type Context struct {
|
|
||||||
CaptureInfo gopacket.CaptureInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) GetCaptureInfo() gopacket.CaptureInfo {
|
|
||||||
return c.CaptureInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartPassiveTapper() <-chan *OutputChannelItem {
|
|
||||||
var harWriter *HarWriter
|
|
||||||
if *dumpToHar {
|
|
||||||
harWriter = NewHarWriter(*HarOutputDir, *harEntriesPerFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
go startPassiveTapper(harWriter)
|
|
||||||
|
|
||||||
if harWriter != nil {
|
|
||||||
return harWriter.OutChan
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startPassiveTapper(harWriter *HarWriter) {
|
|
||||||
defer util.Run()()
|
|
||||||
if *debug {
|
|
||||||
outputLevel = 2
|
|
||||||
} else if *verbose {
|
|
||||||
outputLevel = 1
|
|
||||||
} else if *quiet {
|
|
||||||
outputLevel = -1
|
|
||||||
}
|
|
||||||
errorsMap = make(map[string]uint)
|
|
||||||
|
|
||||||
if localhostIPs, err := getLocalhostIPs(); err != nil {
|
|
||||||
// TODO: think this over
|
|
||||||
fmt.Println("Failed to get self IP addresses")
|
|
||||||
Error("Getting-Self-Address", "Error getting self ip address: %s (%v,%+v)\n", err, err, err)
|
|
||||||
ownIps = make([]string, 0)
|
|
||||||
} else {
|
|
||||||
ownIps = localhostIPs
|
|
||||||
}
|
|
||||||
|
|
||||||
appPortsStr := os.Getenv(AppPortsEnvVar)
|
|
||||||
if appPortsStr == "" {
|
|
||||||
fmt.Println("Received empty/no APP_PORTS env var! only listening to http on port 80!")
|
|
||||||
appPorts = make([]int, 0)
|
|
||||||
} else {
|
|
||||||
appPorts = parseAppPorts(appPortsStr)
|
|
||||||
}
|
|
||||||
tapOutputPort := os.Getenv(OutPortEnvVar)
|
|
||||||
if tapOutputPort == "" {
|
|
||||||
fmt.Println("Received empty/no WEB_SOCKET_PORT env var! falling back to port 8080")
|
|
||||||
tapOutputPort = "8080"
|
|
||||||
}
|
|
||||||
envVal := os.Getenv(maxHTTP2DataLenEnvVar)
|
|
||||||
if envVal == "" {
|
|
||||||
fmt.Println("Received empty/no HTTP2_DATA_SIZE_LIMIT env var! falling back to", maxHTTP2DataLenDefault)
|
|
||||||
maxHTTP2DataLen = maxHTTP2DataLenDefault
|
|
||||||
} else {
|
|
||||||
if convertedInt, err := strconv.Atoi(envVal); err != nil {
|
|
||||||
fmt.Println("Received invalid HTTP2_DATA_SIZE_LIMIT env var! falling back to", maxHTTP2DataLenDefault)
|
|
||||||
maxHTTP2DataLen = maxHTTP2DataLenDefault
|
|
||||||
} else {
|
|
||||||
fmt.Println("Received HTTP2_DATA_SIZE_LIMIT env var:", maxHTTP2DataLenDefault)
|
|
||||||
maxHTTP2DataLen = convertedInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostMode = os.Getenv(shared.HostModeEnvVar) == "1"
|
|
||||||
|
|
||||||
fmt.Printf("App Ports: %v\n", appPorts)
|
|
||||||
fmt.Printf("Tap output websocket port: %s\n", tapOutputPort)
|
|
||||||
|
|
||||||
var onCollectorMessage = func(message []byte) {
|
|
||||||
var parsedMessage CollectorMessage
|
|
||||||
err := json.Unmarshal(message, &parsedMessage)
|
|
||||||
if err == nil {
|
|
||||||
|
|
||||||
if parsedMessage.MessageType == "setPorts" {
|
|
||||||
Debug("Got message from collector. Type: %s, Ports: %v\n", parsedMessage.MessageType, parsedMessage.Ports)
|
|
||||||
appPorts = *parsedMessage.Ports
|
|
||||||
} else if parsedMessage.MessageType == "setAddresses" {
|
|
||||||
Debug("Got message from collector. Type: %s, IPs: %v\n", parsedMessage.MessageType, parsedMessage.Addresses)
|
|
||||||
HostAppAddresses = *parsedMessage.Addresses
|
|
||||||
Info("Filtering for the following addresses: %s\n", HostAppAddresses)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Error("Collector-Message-Parsing", "Error parsing message from collector: %s (%v,%+v)\n", err, err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go startOutputServer(tapOutputPort, onCollectorMessage)
|
|
||||||
|
|
||||||
var handle *pcap.Handle
|
|
||||||
var err error
|
|
||||||
if *fname != "" {
|
|
||||||
if handle, err = pcap.OpenOffline(*fname); err != nil {
|
|
||||||
log.Fatal("PCAP OpenOffline error:", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This is a little complicated because we want to allow all possible options
|
|
||||||
// for creating the packet capture handle... instead of all this you can
|
|
||||||
// just call pcap.OpenLive if you want a simple handle.
|
|
||||||
inactive, err := pcap.NewInactiveHandle(*iface)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("could not create: %v", err)
|
|
||||||
}
|
|
||||||
defer inactive.CleanUp()
|
|
||||||
if err = inactive.SetSnapLen(*snaplen); err != nil {
|
|
||||||
log.Fatalf("could not set snap length: %v", err)
|
|
||||||
} else if err = inactive.SetPromisc(*promisc); err != nil {
|
|
||||||
log.Fatalf("could not set promisc mode: %v", err)
|
|
||||||
} else if err = inactive.SetTimeout(time.Second); err != nil {
|
|
||||||
log.Fatalf("could not set timeout: %v", err)
|
|
||||||
}
|
|
||||||
if *tstype != "" {
|
|
||||||
if t, err := pcap.TimestampSourceFromString(*tstype); err != nil {
|
|
||||||
log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps())
|
|
||||||
} else if err := inactive.SetTimestampSource(t); err != nil {
|
|
||||||
log.Fatalf("Supported timestamp types: %v", inactive.SupportedTimestamps())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if handle, err = inactive.Activate(); err != nil {
|
|
||||||
log.Fatal("PCAP Activate error:", err)
|
|
||||||
}
|
|
||||||
defer handle.Close()
|
|
||||||
}
|
|
||||||
if len(flag.Args()) > 0 {
|
|
||||||
bpffilter := strings.Join(flag.Args(), " ")
|
|
||||||
Info("Using BPF filter %q\n", bpffilter)
|
|
||||||
if err = handle.SetBPFFilter(bpffilter); err != nil {
|
|
||||||
log.Fatal("BPF filter error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if *dumpToHar {
|
|
||||||
harWriter.Start()
|
|
||||||
defer harWriter.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
var dec gopacket.Decoder
|
|
||||||
var ok bool
|
|
||||||
decoderName := *decoder
|
|
||||||
if decoderName == "" {
|
|
||||||
decoderName = fmt.Sprintf("%s", handle.LinkType())
|
|
||||||
}
|
|
||||||
if dec, ok = gopacket.DecodersByLayerName[decoderName]; !ok {
|
|
||||||
log.Fatalln("No decoder named", decoderName)
|
|
||||||
}
|
|
||||||
source := gopacket.NewPacketSource(handle, dec)
|
|
||||||
source.Lazy = *lazy
|
|
||||||
source.NoCopy = true
|
|
||||||
Info("Starting to read packets\n")
|
|
||||||
count := 0
|
|
||||||
bytes := int64(0)
|
|
||||||
start := time.Now()
|
|
||||||
defragger := ip4defrag.NewIPv4Defragmenter()
|
|
||||||
|
|
||||||
streamFactory := &tcpStreamFactory{doHTTP: !*nohttp, harWriter: harWriter}
|
|
||||||
streamPool := reassembly.NewStreamPool(streamFactory)
|
|
||||||
assembler := reassembly.NewAssembler(streamPool)
|
|
||||||
var assemblerMutex sync.Mutex
|
|
||||||
|
|
||||||
signalChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signalChan, os.Interrupt)
|
|
||||||
|
|
||||||
staleConnectionTimeout := time.Second * time.Duration(*staleTimeoutSeconds)
|
|
||||||
cleaner := Cleaner{
|
|
||||||
assembler: assembler,
|
|
||||||
assemblerMutex: &assemblerMutex,
|
|
||||||
matcher: &reqResMatcher,
|
|
||||||
cleanPeriod: cleanPeriod,
|
|
||||||
connectionTimeout: staleConnectionTimeout,
|
|
||||||
}
|
|
||||||
cleaner.start()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
statsPeriod := time.Second * time.Duration(*statsevery)
|
|
||||||
ticker := time.NewTicker(statsPeriod)
|
|
||||||
|
|
||||||
for true {
|
|
||||||
<-ticker.C
|
|
||||||
|
|
||||||
// Since the start
|
|
||||||
errorsMapMutex.Lock()
|
|
||||||
errorMapLen := len(errorsMap)
|
|
||||||
errorsSummery := fmt.Sprintf("%v", errorsMap)
|
|
||||||
errorsMapMutex.Unlock()
|
|
||||||
fmt.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)\nErrors Summary: %s\n",
|
|
||||||
count,
|
|
||||||
bytes,
|
|
||||||
time.Since(start),
|
|
||||||
nErrors,
|
|
||||||
errorMapLen,
|
|
||||||
errorsSummery,
|
|
||||||
)
|
|
||||||
|
|
||||||
// At this moment
|
|
||||||
memStats := runtime.MemStats{}
|
|
||||||
runtime.ReadMemStats(&memStats)
|
|
||||||
fmt.Printf(
|
|
||||||
"mem: %d, goroutines: %d, unmatched messages: %d\n",
|
|
||||||
memStats.HeapAlloc,
|
|
||||||
runtime.NumGoroutine(),
|
|
||||||
reqResMatcher.openMessagesMap.Count(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Since the last print
|
|
||||||
cleanStats := cleaner.dumpStats()
|
|
||||||
appStats := statsTracker.dumpStats()
|
|
||||||
fmt.Printf(
|
|
||||||
"flushed connections %d, closed connections: %d, deleted messages: %d, matched messages: %d\n",
|
|
||||||
cleanStats.flushed,
|
|
||||||
cleanStats.closed,
|
|
||||||
cleanStats.deleted,
|
|
||||||
appStats.matchedMessages,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for packet := range source.Packets() {
|
|
||||||
count++
|
|
||||||
Debug("PACKET #%d\n", count)
|
|
||||||
data := packet.Data()
|
|
||||||
bytes += int64(len(data))
|
|
||||||
if *hexdumppkt {
|
|
||||||
Debug("Packet content (%d/0x%x)\n%s\n", len(data), len(data), hex.Dump(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// defrag the IPv4 packet if required
|
|
||||||
if !*nodefrag {
|
|
||||||
ip4Layer := packet.Layer(layers.LayerTypeIPv4)
|
|
||||||
if ip4Layer == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ip4 := ip4Layer.(*layers.IPv4)
|
|
||||||
l := ip4.Length
|
|
||||||
newip4, err := defragger.DefragIPv4(ip4)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Error while de-fragmenting", err)
|
|
||||||
} else if newip4 == nil {
|
|
||||||
Debug("Fragment...\n")
|
|
||||||
continue // packet fragment, we don't have whole packet yet.
|
|
||||||
}
|
|
||||||
if newip4.Length != l {
|
|
||||||
stats.ipdefrag++
|
|
||||||
Debug("Decoding re-assembled packet: %s\n", newip4.NextLayerType())
|
|
||||||
pb, ok := packet.(gopacket.PacketBuilder)
|
|
||||||
if !ok {
|
|
||||||
panic("Not a PacketBuilder")
|
|
||||||
}
|
|
||||||
nextDecoder := newip4.NextLayerType()
|
|
||||||
nextDecoder.Decode(newip4.Payload, pb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tcp := packet.Layer(layers.LayerTypeTCP)
|
|
||||||
if tcp != nil {
|
|
||||||
tcp := tcp.(*layers.TCP)
|
|
||||||
if *checksum {
|
|
||||||
err := tcp.SetNetworkLayerForChecksum(packet.NetworkLayer())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to set network layer for checksum: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c := Context{
|
|
||||||
CaptureInfo: packet.Metadata().CaptureInfo,
|
|
||||||
}
|
|
||||||
stats.totalsz += len(tcp.Payload)
|
|
||||||
//fmt.Println(packet.NetworkLayer().NetworkFlow().Src(), ":", tcp.SrcPort, " -> ", packet.NetworkLayer().NetworkFlow().Dst(), ":", tcp.DstPort)
|
|
||||||
assemblerMutex.Lock()
|
|
||||||
assembler.AssembleWithContext(packet.NetworkLayer().NetworkFlow(), tcp, &c)
|
|
||||||
assemblerMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
done := *maxcount > 0 && count >= *maxcount
|
|
||||||
if done {
|
|
||||||
errorsMapMutex.Lock()
|
|
||||||
errorMapLen := len(errorsMap)
|
|
||||||
errorsMapMutex.Unlock()
|
|
||||||
fmt.Fprintf(os.Stderr, "Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)\n", count, bytes, time.Since(start), nErrors, errorMapLen)
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-signalChan:
|
|
||||||
fmt.Fprintf(os.Stderr, "\nCaught SIGINT: aborting\n")
|
|
||||||
done = true
|
|
||||||
default:
|
|
||||||
// NOP: continue
|
|
||||||
}
|
|
||||||
if done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assemblerMutex.Lock()
|
|
||||||
closed := assembler.FlushAll()
|
|
||||||
assemblerMutex.Unlock()
|
|
||||||
Debug("Final flush: %d closed", closed)
|
|
||||||
if outputLevel >= 2 {
|
|
||||||
streamPool.Dump()
|
|
||||||
}
|
|
||||||
|
|
||||||
if *memprofile != "" {
|
|
||||||
f, err := os.Create(*memprofile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
pprof.WriteHeapProfile(f)
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
streamFactory.WaitGoRoutines()
|
|
||||||
assemblerMutex.Lock()
|
|
||||||
Debug("%s\n", assembler.Dump())
|
|
||||||
assemblerMutex.Unlock()
|
|
||||||
if !*nodefrag {
|
|
||||||
fmt.Printf("IPdefrag:\t\t%d\n", stats.ipdefrag)
|
|
||||||
}
|
|
||||||
fmt.Printf("TCP stats:\n")
|
|
||||||
fmt.Printf(" missed bytes:\t\t%d\n", stats.missedBytes)
|
|
||||||
fmt.Printf(" total packets:\t\t%d\n", stats.pkt)
|
|
||||||
fmt.Printf(" rejected FSM:\t\t%d\n", stats.rejectFsm)
|
|
||||||
fmt.Printf(" rejected Options:\t%d\n", stats.rejectOpt)
|
|
||||||
fmt.Printf(" reassembled bytes:\t%d\n", stats.sz)
|
|
||||||
fmt.Printf(" total TCP bytes:\t%d\n", stats.totalsz)
|
|
||||||
fmt.Printf(" conn rejected FSM:\t%d\n", stats.rejectConnFsm)
|
|
||||||
fmt.Printf(" reassembled chunks:\t%d\n", stats.reassembled)
|
|
||||||
fmt.Printf(" out-of-order packets:\t%d\n", stats.outOfOrderPackets)
|
|
||||||
fmt.Printf(" out-of-order bytes:\t%d\n", stats.outOfOrderBytes)
|
|
||||||
fmt.Printf(" biggest-chunk packets:\t%d\n", stats.biggestChunkPackets)
|
|
||||||
fmt.Printf(" biggest-chunk bytes:\t%d\n", stats.biggestChunkBytes)
|
|
||||||
fmt.Printf(" overlap packets:\t%d\n", stats.overlapPackets)
|
|
||||||
fmt.Printf(" overlap bytes:\t\t%d\n", stats.overlapBytes)
|
|
||||||
fmt.Printf("Errors: %d\n", nErrors)
|
|
||||||
for e := range errorsMap {
|
|
||||||
fmt.Printf(" %s:\t\t%d\n", e, errorsMap[e])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AppStats struct {
|
|
||||||
matchedMessages int
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsTracker struct {
|
|
||||||
stats AppStats
|
|
||||||
statsMutex sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *StatsTracker) incMatchedMessages() {
|
|
||||||
st.statsMutex.Lock()
|
|
||||||
st.stats.matchedMessages++
|
|
||||||
st.statsMutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (st *StatsTracker) dumpStats() AppStats {
|
|
||||||
st.statsMutex.Lock()
|
|
||||||
|
|
||||||
stats := AppStats{
|
|
||||||
matchedMessages: st.stats.matchedMessages,
|
|
||||||
}
|
|
||||||
|
|
||||||
st.stats.matchedMessages = 0
|
|
||||||
|
|
||||||
st.statsMutex.Unlock()
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/patrickmn/go-cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Time allowed to write a message to the peer.
|
|
||||||
writeWait = 10 * time.Second
|
|
||||||
|
|
||||||
// Time allowed to read the next pong message from the peer.
|
|
||||||
pongWait = 60 * time.Second
|
|
||||||
|
|
||||||
// Send pings to peer with this period. Must be less than pongWait.
|
|
||||||
pingPeriod = (pongWait * 9) / 10
|
|
||||||
|
|
||||||
// Maximum message size allowed from peer.
|
|
||||||
maxMessageSize = 512
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
newline = []byte{'\n'}
|
|
||||||
space = []byte{' '}
|
|
||||||
hub *Hub
|
|
||||||
outboundSocketNotifyExpiringCache = cache.New(outboundThrottleCacheExpiryPeriod, outboundThrottleCacheExpiryPeriod)
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func (_ *http.Request) bool { return true },
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is a middleman between the websocket connection and the hub.
|
|
||||||
type Client struct {
|
|
||||||
hub *Hub
|
|
||||||
|
|
||||||
// The websocket connection.
|
|
||||||
conn *websocket.Conn
|
|
||||||
|
|
||||||
// Buffered channel of outbound messages.
|
|
||||||
send chan []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutBoundLinkMessage struct {
|
|
||||||
SourceIP string `json:"sourceIP"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// readPump pumps messages from the websocket connection to the hub.
|
|
||||||
//
|
|
||||||
// The application runs readPump in a per-connection goroutine. The application
|
|
||||||
// ensures that there is at most one reader on a connection by executing all
|
|
||||||
// reads from this goroutine.
|
|
||||||
func (c *Client) readPump() {
|
|
||||||
defer func() {
|
|
||||||
c.hub.unregister <- c
|
|
||||||
c.conn.Close()
|
|
||||||
}()
|
|
||||||
c.conn.SetReadLimit(maxMessageSize)
|
|
||||||
c.conn.SetReadDeadline(time.Now().Add(pongWait))
|
|
||||||
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
|
|
||||||
for {
|
|
||||||
_, message, err := c.conn.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
|
||||||
log.Printf("error: %v", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
|
|
||||||
c.hub.onMessageCallback(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writePump pumps messages from the hub to the websocket connection.
|
|
||||||
//
|
|
||||||
// A goroutine running writePump is started for each connection. The
|
|
||||||
// application ensures that there is at most one writer to a connection by
|
|
||||||
// executing all writes from this goroutine.
|
|
||||||
func (c *Client) writePump() {
|
|
||||||
ticker := time.NewTicker(pingPeriod)
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
c.conn.Close()
|
|
||||||
}()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case message, ok := <-c.send:
|
|
||||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if !ok {
|
|
||||||
// The hub closed the channel.
|
|
||||||
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := c.conn.NextWriter(websocket.TextMessage)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(message)
|
|
||||||
|
|
||||||
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-ticker.C:
|
|
||||||
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
|
||||||
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hub struct {
|
|
||||||
// Registered clients.
|
|
||||||
clients map[*Client]bool
|
|
||||||
|
|
||||||
// Inbound messages from the clients.
|
|
||||||
broadcast chan []byte
|
|
||||||
|
|
||||||
// Register requests from the clients.
|
|
||||||
register chan *Client
|
|
||||||
|
|
||||||
// Unregister requests from clients.
|
|
||||||
unregister chan *Client
|
|
||||||
|
|
||||||
// Handle messages from client
|
|
||||||
onMessageCallback func([]byte)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHub(onMessageCallback func([]byte)) *Hub {
|
|
||||||
return &Hub{
|
|
||||||
broadcast: make(chan []byte),
|
|
||||||
register: make(chan *Client),
|
|
||||||
unregister: make(chan *Client),
|
|
||||||
clients: make(map[*Client]bool),
|
|
||||||
onMessageCallback: onMessageCallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Hub) run() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case client := <-h.register:
|
|
||||||
h.clients[client] = true
|
|
||||||
case client := <-h.unregister:
|
|
||||||
if _, ok := h.clients[client]; ok {
|
|
||||||
delete(h.clients, client)
|
|
||||||
close(client.send)
|
|
||||||
}
|
|
||||||
case message := <-h.broadcast:
|
|
||||||
// matched messages counter is incremented in this thread instead of in multiple http reader
|
|
||||||
// threads in order to reduce contention.
|
|
||||||
statsTracker.incMatchedMessages()
|
|
||||||
|
|
||||||
for client := range h.clients {
|
|
||||||
select {
|
|
||||||
case client.send <- message:
|
|
||||||
default:
|
|
||||||
close(client.send)
|
|
||||||
delete(h.clients, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// serveWs handles websocket requests from the peer.
|
|
||||||
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
|
|
||||||
client.hub.register <- client
|
|
||||||
|
|
||||||
// Allow collection of memory referenced by the caller by doing all work in
|
|
||||||
// new goroutines.
|
|
||||||
go client.writePump()
|
|
||||||
go client.readPump()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startOutputServer(port string, messageCallback func([]byte)) {
|
|
||||||
hub = newHub(messageCallback)
|
|
||||||
go hub.run()
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
serveWs(hub, w, r)
|
|
||||||
})
|
|
||||||
err := http.ListenAndServe("0.0.0.0:" + port, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Output server error: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastReqResPair(reqResJson []byte) {
|
|
||||||
hub.broadcast <- reqResJson
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcastOutboundLink(srcIP string, dstIP string, dstPort int) {
|
|
||||||
cacheKey := fmt.Sprintf("%s -> %s:%d", srcIP, dstIP, dstPort)
|
|
||||||
_, isInCache := outboundSocketNotifyExpiringCache.Get(cacheKey)
|
|
||||||
if isInCache {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
outboundSocketNotifyExpiringCache.SetDefault(cacheKey, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
socketMessage := OutBoundLinkMessage{
|
|
||||||
SourceIP: srcIP,
|
|
||||||
IP: dstIP,
|
|
||||||
Port: dstPort,
|
|
||||||
Type: "outboundSocketDetected",
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonStr, err := json.Marshal(socketMessage)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshalling outbound socket detection object: %v", err)
|
|
||||||
} else {
|
|
||||||
hub.broadcast <- jsonStr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package tap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers" // pulls in all layers decoders
|
|
||||||
"github.com/google/gopacket/reassembly"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The TCP factory: returns a new Stream
|
|
||||||
* Implements gopacket.reassembly.StreamFactory interface (New)
|
|
||||||
* Generates a new tcp stream for each new tcp connection. Closes the stream when the connection closes.
|
|
||||||
*/
|
|
||||||
type tcpStreamFactory struct {
|
|
||||||
wg sync.WaitGroup
|
|
||||||
doHTTP bool
|
|
||||||
harWriter *HarWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
|
|
||||||
Debug("* NEW: %s %s\n", net, transport)
|
|
||||||
fsmOptions := reassembly.TCPSimpleFSMOptions{
|
|
||||||
SupportMissingEstablishment: *allowmissinginit,
|
|
||||||
}
|
|
||||||
Debug("Current App Ports: %v\n", appPorts)
|
|
||||||
dstIp := net.Dst().String()
|
|
||||||
dstPort := int(tcp.DstPort)
|
|
||||||
|
|
||||||
if factory.shouldNotifyOnOutboundLink(dstIp, dstPort) {
|
|
||||||
broadcastOutboundLink(net.Src().String(), dstIp, dstPort)
|
|
||||||
}
|
|
||||||
isHTTP := factory.shouldTap(dstIp, dstPort)
|
|
||||||
stream := &tcpStream{
|
|
||||||
net: net,
|
|
||||||
transport: transport,
|
|
||||||
isDNS: tcp.SrcPort == 53 || tcp.DstPort == 53,
|
|
||||||
isHTTP: isHTTP && factory.doHTTP,
|
|
||||||
reversed: tcp.SrcPort == 80,
|
|
||||||
tcpstate: reassembly.NewTCPSimpleFSM(fsmOptions),
|
|
||||||
ident: fmt.Sprintf("%s:%s", net, transport),
|
|
||||||
optchecker: reassembly.NewTCPOptionCheck(),
|
|
||||||
}
|
|
||||||
if stream.isHTTP {
|
|
||||||
stream.client = httpReader{
|
|
||||||
msgQueue: make(chan httpReaderDataMsg),
|
|
||||||
ident: fmt.Sprintf("%s %s", net, transport),
|
|
||||||
tcpID: tcpID{
|
|
||||||
srcIP: net.Src().String(),
|
|
||||||
dstIP: net.Dst().String(),
|
|
||||||
srcPort: transport.Src().String(),
|
|
||||||
dstPort: transport.Dst().String(),
|
|
||||||
},
|
|
||||||
hexdump: *hexdump,
|
|
||||||
parent: stream,
|
|
||||||
isClient: true,
|
|
||||||
harWriter: factory.harWriter,
|
|
||||||
}
|
|
||||||
stream.server = httpReader{
|
|
||||||
msgQueue: make(chan httpReaderDataMsg),
|
|
||||||
ident: fmt.Sprintf("%s %s", net.Reverse(), transport.Reverse()),
|
|
||||||
tcpID: tcpID{
|
|
||||||
srcIP: net.Dst().String(),
|
|
||||||
dstIP: net.Src().String(),
|
|
||||||
srcPort: transport.Dst().String(),
|
|
||||||
dstPort: transport.Src().String(),
|
|
||||||
},
|
|
||||||
hexdump: *hexdump,
|
|
||||||
parent: stream,
|
|
||||||
harWriter: factory.harWriter,
|
|
||||||
}
|
|
||||||
factory.wg.Add(2)
|
|
||||||
// Start reading from channels stream.client.bytes and stream.server.bytes
|
|
||||||
go stream.client.run(&factory.wg)
|
|
||||||
go stream.server.run(&factory.wg)
|
|
||||||
}
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) WaitGoRoutines() {
|
|
||||||
factory.wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) shouldTap(dstIP string, dstPort int) bool {
|
|
||||||
if hostMode {
|
|
||||||
if inArrayString(HostAppAddresses, fmt.Sprintf("%s:%d", dstIP, dstPort)) == true {
|
|
||||||
return true
|
|
||||||
} else if inArrayString(HostAppAddresses, dstIP) == true {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
isTappedPort := dstPort == 80 || (appPorts != nil && (inArrayInt(appPorts, dstPort)))
|
|
||||||
if !isTappedPort {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*anydirection {
|
|
||||||
isDirectedHere := inArrayString(ownIps, dstIP)
|
|
||||||
if !isDirectedHere {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *tcpStreamFactory) shouldNotifyOnOutboundLink(dstIP string, dstPort int) bool {
|
|
||||||
if inArrayInt(remoteOnlyOutboundPorts, dstPort) {
|
|
||||||
isDirectedHere := inArrayString(ownIps, dstIP)
|
|
||||||
return !isDirectedHere && !isPrivateIP(dstIP)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"log"
|
|
||||||
"mizuserver/pkg/models"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"reflect"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartServer starts the server with a graceful shutdown
|
|
||||||
func StartServer(app *fiber.App) {
|
|
||||||
signals := make(chan os.Signal, 2)
|
|
||||||
signal.Notify(signals,
|
|
||||||
os.Interrupt, // this catch ctrl + c
|
|
||||||
syscall.SIGTSTP, // this catch ctrl + z
|
|
||||||
)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = <-signals
|
|
||||||
fmt.Println("Shutting down...")
|
|
||||||
_ = app.Shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Run server.
|
|
||||||
if err := app.Listen(":8899"); err != nil {
|
|
||||||
log.Printf("Oops... Server is not running! Reason: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReverseSlice(data interface{}) {
|
|
||||||
value := reflect.ValueOf(data)
|
|
||||||
valueLen := value.Len()
|
|
||||||
for i := 0; i <= int((valueLen-1)/2); i++ {
|
|
||||||
reverseIndex := valueLen - 1 - i
|
|
||||||
tmp := value.Index(reverseIndex).Interface()
|
|
||||||
value.Index(reverseIndex).Set(value.Index(i))
|
|
||||||
value.Index(i).Set(reflect.ValueOf(tmp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckErr(e error) {
|
|
||||||
if e != nil {
|
|
||||||
log.Printf("%v", e)
|
|
||||||
//panic(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetHostname(address, newHostname string) string {
|
|
||||||
replacedUrl, err := url.Parse(address)
|
|
||||||
if err != nil{
|
|
||||||
log.Printf("error replacing hostname to %s in address %s, returning original %v",newHostname, address, err)
|
|
||||||
return address
|
|
||||||
}
|
|
||||||
replacedUrl.Host = newHostname
|
|
||||||
return replacedUrl.String()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetResolvedBaseEntry(entry models.MizuEntry) models.BaseEntryDetails {
|
|
||||||
entryUrl := entry.Url
|
|
||||||
service := entry.Service
|
|
||||||
if entry.ResolvedDestination != "" {
|
|
||||||
entryUrl = SetHostname(entryUrl, entry.ResolvedDestination)
|
|
||||||
service = SetHostname(service, entry.ResolvedDestination)
|
|
||||||
}
|
|
||||||
return models.BaseEntryDetails{
|
|
||||||
Id: entry.EntryId,
|
|
||||||
Url: entryUrl,
|
|
||||||
Service: service,
|
|
||||||
Path: entry.Path,
|
|
||||||
StatusCode: entry.Status,
|
|
||||||
Method: entry.Method,
|
|
||||||
Timestamp: entry.Timestamp,
|
|
||||||
RequestSenderIp: entry.RequestSenderIp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetBytesFromStruct(v interface{}) []byte{
|
|
||||||
a, _ := json.Marshal(v)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
./mizuagent -i any -hardump -targets ${TAPPED_ADDRESSES}
|
|
||||||
BIN
assets/mizu-example.png
Normal file
BIN
assets/mizu-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 811 KiB |
24
assets/mizu-logo.svg
Normal file
24
assets/mizu-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/mizu-ui.png
Normal file
BIN
assets/mizu-ui.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 491 KiB |
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 |
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SERVER_NAME=mizu
|
|
||||||
GCP_PROJECT=up9-docker-hub
|
|
||||||
REPOSITORY=gcr.io/$GCP_PROJECT
|
|
||||||
GIT_BRANCH=$(git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]')
|
|
||||||
DOCKER_TAGGED_BUILD=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH:latest
|
|
||||||
|
|
||||||
if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ]
|
|
||||||
then
|
|
||||||
echo "Pushing to $GIT_BRANCH is allowed only via CI"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "building $DOCKER_TAGGED_BUILD"
|
|
||||||
docker build -t "$DOCKER_TAGGED_BUILD" .
|
|
||||||
|
|
||||||
echo pushing to "$REPOSITORY"
|
|
||||||
docker push "$DOCKER_TAGGED_BUILD"
|
|
||||||
1
cli/.gitignore
vendored
Normal file
1
cli/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
bin
|
||||||
17
cli/Makefile
17
cli/Makefile
@@ -3,6 +3,7 @@ COMMIT_HASH=$(shell git rev-parse HEAD)
|
|||||||
GIT_BRANCH=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
GIT_BRANCH=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
||||||
GIT_VERSION=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
GIT_VERSION=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]')
|
||||||
BUILD_TIMESTAMP=$(shell date +%s)
|
BUILD_TIMESTAMP=$(shell date +%s)
|
||||||
|
export SEM_VER?=0.0.0
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
@@ -13,7 +14,7 @@ help: ## This help.
|
|||||||
install:
|
install:
|
||||||
go install mizu.go
|
go install mizu.go
|
||||||
|
|
||||||
build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables)
|
build: ## Build mizu CLI binary (select platform via GOOS / GOARCH env variables).
|
||||||
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
go build -ldflags="-X 'github.com/up9inc/mizu/cli/mizu.GitCommitHash=$(COMMIT_HASH)' \
|
||||||
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
-X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \
|
||||||
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
-X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \
|
||||||
@@ -21,20 +22,24 @@ build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables
|
|||||||
-o bin/mizu_$(SUFFIX) mizu.go
|
-o bin/mizu_$(SUFFIX) mizu.go
|
||||||
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
(cd bin && shasum -a 256 mizu_${SUFFIX} > mizu_${SUFFIX}.sha256)
|
||||||
|
|
||||||
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) GOOS=windows GOARCH=amd64
|
@$(MAKE) build GOOS=darwin GOARCH=arm64
|
||||||
|
@$(MAKE) build GOOS=windows GOARCH=amd64
|
||||||
|
@mv ./bin/mizu_windows_amd64 ./bin/mizu.exe
|
||||||
@# $(MAKE) GOOS=linux GOARCH=386
|
@# $(MAKE) GOOS=linux GOARCH=386
|
||||||
@# $(MAKE) GOOS=windows GOARCH=386
|
@# $(MAKE) GOOS=windows GOARCH=386
|
||||||
@# $(MAKE) GOOS=darwin GOARCH=arm64
|
|
||||||
@# $(MAKE) GOOS=linux GOARCH=arm64
|
@# $(MAKE) GOOS=linux GOARCH=arm64
|
||||||
@# $(MAKE) GOOS=windows GOARCH=arm64
|
@# $(MAKE) GOOS=windows GOARCH=arm64
|
||||||
@echo "---------"
|
@echo "---------"
|
||||||
@find ./bin -ls
|
@find ./bin -ls
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# mizu CLI
|
|
||||||
## Usage
|
|
||||||
`./mizu {pod_name_regex}`
|
|
||||||
|
|
||||||
### Optional Flags
|
|
||||||
|
|
||||||
| flag | default | purpose |
|
|
||||||
|----------------------|------------------|--------------------------------------------------------------------------------------------------------------|
|
|
||||||
| `--no-gui` | `false` | Don't host the web interface (not applicable at the moment) |
|
|
||||||
| `--gui-port` | `8899` | local port that web interface will be forwarded to |
|
|
||||||
| `--namespace` | | use namespace different than the one found in kubeconfig |
|
|
||||||
| `--kubeconfig` | | Path to custom kubeconfig file |
|
|
||||||
|
|
||||||
There are some extra flags defined in code that will show up in `./mizu --help`, these are non functional stubs for now
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
Make sure your go version is at least 1.11
|
|
||||||
1. cd to `mizu/cli`
|
|
||||||
2. Run `go mod download` (may take a moment)
|
|
||||||
3. Run `go build mizu.go`
|
|
||||||
|
|
||||||
Alternatively, you can build+run directly using `go run mizu.go {pod_name_regex}`
|
|
||||||
|
|
||||||
|
|
||||||
## Known issues
|
|
||||||
* mid-flight port forwarding failures are not detected and no indication will be shown when this occurs
|
|
||||||
29
cli/README.md.TEMPLATE
Normal file
29
cli/README.md.TEMPLATE
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Mizu release _SEM_VER_
|
||||||
|
|
||||||
|
Download Mizu for your platform
|
||||||
|
|
||||||
|
**Mac** (Intel)
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_darwin_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mac** (Apple M1 silicon)
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_darwin_arm64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux**
|
||||||
|
```
|
||||||
|
curl -Lo mizu https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu_linux_amd64 && chmod 755 mizu
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows** (Intel 64bit)
|
||||||
|
```
|
||||||
|
curl -LO https://github.com/up9inc/mizu/releases/download/_SEM_VER_/mizu.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
SHA256 checksums available for compiled binaries.
|
||||||
|
Run `shasum -a 256 -c mizu_OS_ARCH.sha256` to verify.
|
||||||
|
|
||||||
|
|
||||||
133
cli/apiserver/provider.go
Normal file
133
cli/apiserver/provider.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
core "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type apiServerProvider struct {
|
||||||
|
url string
|
||||||
|
isReady bool
|
||||||
|
retries int
|
||||||
|
}
|
||||||
|
|
||||||
|
var Provider = apiServerProvider{retries: config.GetIntEnvConfig(config.ApiServerRetries, 20)}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) InitAndTestConnection(url string) error {
|
||||||
|
healthUrl := fmt.Sprintf("%s/", url)
|
||||||
|
retriesLeft := provider.retries
|
||||||
|
for retriesLeft > 0 {
|
||||||
|
if response, err := http.Get(healthUrl); err != nil {
|
||||||
|
logger.Log.Debugf("[ERROR] failed connecting to api server %v", err)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
responseBody := ""
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr == nil {
|
||||||
|
responseBody = string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("can't connect to api server yet, response status code: %v, body: %v", response.StatusCode, responseBody)
|
||||||
|
|
||||||
|
response.Body.Close()
|
||||||
|
} else {
|
||||||
|
logger.Log.Debugf("connection test to api server passed successfully")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
retriesLeft -= 1
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if retriesLeft == 0 {
|
||||||
|
provider.isReady = false
|
||||||
|
return fmt.Errorf("couldn't reach the api server after %v retries", provider.retries)
|
||||||
|
}
|
||||||
|
provider.url = url
|
||||||
|
provider.isReady = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) ReportTappedPods(pods []core.Pod) error {
|
||||||
|
if !provider.isReady {
|
||||||
|
return fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
tappedPodsUrl := fmt.Sprintf("%s/status/tappedPods", provider.url)
|
||||||
|
|
||||||
|
podInfos := make([]shared.PodInfo, 0)
|
||||||
|
for _, pod := range pods {
|
||||||
|
podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace})
|
||||||
|
}
|
||||||
|
tapStatus := shared.TapStatus{Pods: podInfos}
|
||||||
|
|
||||||
|
if jsonValue, err := json.Marshal(tapStatus); err != nil {
|
||||||
|
return fmt.Errorf("failed Marshal the tapped pods %w", err)
|
||||||
|
} else {
|
||||||
|
if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil {
|
||||||
|
return fmt.Errorf("failed sending to API server the tapped pods %w", err)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("failed sending to API server the tapped pods, response status code %v", response.StatusCode)
|
||||||
|
} else {
|
||||||
|
logger.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) GetGeneralStats() (map[string]interface{}, error) {
|
||||||
|
if !provider.isReady {
|
||||||
|
return nil, fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
generalStatsUrl := fmt.Sprintf("%s/status/general", provider.url)
|
||||||
|
|
||||||
|
response, requestErr := http.Get(generalStatsUrl)
|
||||||
|
if requestErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get general stats for telemetry, err: %w", requestErr)
|
||||||
|
} else if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("failed to get general stats for telemetry, status code: %v", response.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = response.Body.Close() }()
|
||||||
|
|
||||||
|
data, readErr := ioutil.ReadAll(response.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read general stats for telemetry, err: %v", readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var generalStats map[string]interface{}
|
||||||
|
if parseErr := json.Unmarshal(data, &generalStats); parseErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse general stats for telemetry, err: %v", parseErr)
|
||||||
|
}
|
||||||
|
return generalStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *apiServerProvider) GetVersion() (string, error) {
|
||||||
|
if !provider.isReady {
|
||||||
|
return "", fmt.Errorf("trying to reach api server when not initialized yet")
|
||||||
|
}
|
||||||
|
versionUrl, _ := url.Parse(fmt.Sprintf("%s/metadata/version", provider.url))
|
||||||
|
req := &http.Request{
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: versionUrl,
|
||||||
|
}
|
||||||
|
statusResp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer statusResp.Body.Close()
|
||||||
|
|
||||||
|
versionResponse := &shared.VersionResponse{}
|
||||||
|
if err := json.NewDecoder(statusResp.Body).Decode(&versionResponse); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionResponse.SemVer, nil
|
||||||
|
}
|
||||||
159
cli/auth/authProvider.go
Normal file
159
cli/auth/authProvider.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginTimeoutInMin = 2
|
||||||
|
|
||||||
|
// Ports are configured in keycloak "cli" client as valid redirect URIs. A change here must be reflected there as well.
|
||||||
|
var listenPorts = []int{3141, 4001, 5002, 6003, 7004, 8005, 9006, 10007}
|
||||||
|
|
||||||
|
func Login() error {
|
||||||
|
token, loginErr := loginInteractively()
|
||||||
|
if loginErr != nil {
|
||||||
|
return fmt.Errorf("failed login interactively, err: %v", loginErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
authConfig := configStructs.AuthConfig{
|
||||||
|
EnvName: config.Config.Auth.EnvName,
|
||||||
|
Token: token.AccessToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile, defaultConfigErr := config.GetConfigWithDefaults()
|
||||||
|
if defaultConfigErr != nil {
|
||||||
|
return fmt.Errorf("failed getting config with defaults, err: %v", defaultConfigErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.LoadConfigFile(config.Config.ConfigFilePath, configFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed getting config file, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile.Auth = authConfig
|
||||||
|
|
||||||
|
if err := config.WriteConfig(configFile); err != nil {
|
||||||
|
return fmt.Errorf("failed writing config with auth, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Config.Auth = authConfig
|
||||||
|
|
||||||
|
logger.Log.Infof("Login successfully, token stored in config path: %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginInteractively() (*oauth2.Token, error) {
|
||||||
|
tokenChannel := make(chan *oauth2.Token)
|
||||||
|
errorChannel := make(chan error)
|
||||||
|
|
||||||
|
server := http.Server{}
|
||||||
|
go startLoginServer(tokenChannel, errorChannel, &server)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := server.Shutdown(context.Background()); err != nil {
|
||||||
|
logger.Log.Debugf("Error shutting down server, err: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(loginTimeoutInMin * time.Minute):
|
||||||
|
return nil, errors.New("auth timed out")
|
||||||
|
case err := <-errorChannel:
|
||||||
|
return nil, err
|
||||||
|
case token := <-tokenChannel:
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, server *http.Server) {
|
||||||
|
for _, port := range listenPorts {
|
||||||
|
var authConfig = &oauth2.Config{
|
||||||
|
ClientID: "cli",
|
||||||
|
RedirectURL: fmt.Sprintf("http://localhost:%v/callback", port),
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/auth", config.Config.Auth.EnvName),
|
||||||
|
TokenURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/token", config.Config.Auth.EnvName),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
state := uuid.New()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
server.Handler = mux
|
||||||
|
mux.Handle("/callback", loginCallbackHandler(tokenChannel, errorChannel, authConfig, state))
|
||||||
|
|
||||||
|
listener, listenErr := net.Listen("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", port))
|
||||||
|
if listenErr != nil {
|
||||||
|
logger.Log.Debugf("failed to start listening on port %v, err: %v", port, listenErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationUrl := authConfig.AuthCodeURL(state.String())
|
||||||
|
uiUtils.OpenBrowser(authorizationUrl)
|
||||||
|
|
||||||
|
serveErr := server.Serve(listener)
|
||||||
|
if serveErr == http.ErrServerClosed {
|
||||||
|
logger.Log.Debugf("received server shutdown, server on port %v is closed", port)
|
||||||
|
return
|
||||||
|
} else if serveErr != nil {
|
||||||
|
logger.Log.Debugf("failed to start serving on port %v, err: %v", port, serveErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("didn't receive server closed on port %v", port)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChannel <- fmt.Errorf("failed to start serving on all listen ports, ports: %v", listenPorts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan error, authConfig *oauth2.Config, state uuid.UUID) http.Handler {
|
||||||
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if err := request.ParseForm(); err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("failed to parse form, err: %v", err)
|
||||||
|
http.Error(writer, errorMsg, http.StatusBadRequest)
|
||||||
|
errorChannel <- fmt.Errorf(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestState := request.Form.Get("state")
|
||||||
|
if requestState != state.String() {
|
||||||
|
errorMsg := fmt.Sprintf("state invalid, requestState: %v, authState:%v", requestState, state.String())
|
||||||
|
http.Error(writer, errorMsg, http.StatusBadRequest)
|
||||||
|
errorChannel <- fmt.Errorf(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := request.Form.Get("code")
|
||||||
|
if code == "" {
|
||||||
|
errorMsg := "code not found"
|
||||||
|
http.Error(writer, errorMsg, http.StatusBadRequest)
|
||||||
|
errorChannel <- fmt.Errorf(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := authConfig.Exchange(context.Background(), code)
|
||||||
|
if err != nil {
|
||||||
|
errorMsg := fmt.Sprintf("failed to create token, err: %v", err)
|
||||||
|
http.Error(writer, errorMsg, http.StatusInternalServerError)
|
||||||
|
errorChannel <- fmt.Errorf(errorMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenChannel <- token
|
||||||
|
|
||||||
|
http.Redirect(writer, request, fmt.Sprintf("https://%s/CliLogin", config.Config.Auth.EnvName), http.StatusFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
48
cli/cmd/common.go
Normal file
48
cli/cmd/common.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"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/mizu"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetApiServerUrl() string {
|
||||||
|
return fmt.Sprintf("http://%s", kubernetes.GetMizuApiServerProxiedHostAndPath(config.Config.Tap.GuiPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||||
|
err := kubernetes.StartProxy(kubernetesProvider, config.Config.Tap.GuiPort, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error occured while running k8s proxy %v\n"+
|
||||||
|
"Try setting different port by using --%s", errormessage.FormatError(err), configStructs.GuiPortTapName))
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("proxy ended")
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
|
||||||
|
logger.Log.Debugf("waiting for finish...")
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
||||||
|
|
||||||
|
// block until ctx cancel is called or termination signal is received
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Log.Debugf("ctx done")
|
||||||
|
break
|
||||||
|
case <-sigChan:
|
||||||
|
logger.Log.Debugf("Got termination signal, canceling execution...")
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
56
cli/cmd/config.go
Normal file
56
cli/cmd/config.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Generate config with default values",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
go telemetry.ReportRun("config", config.Config.Config)
|
||||||
|
|
||||||
|
configWithDefaults, err := config.GetConfigWithDefaults()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed generating config with defaults, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Config.Config.Regenerate {
|
||||||
|
if err := config.WriteConfig(configWithDefaults); err != nil {
|
||||||
|
logger.Log.Errorf("Failed writing config with defaults, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath)))
|
||||||
|
} else {
|
||||||
|
template, err := uiUtils.PrettyYaml(configWithDefaults)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("Failed converting config with defaults to yaml, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Writing template config.\n%v", template)
|
||||||
|
fmt.Printf("%v", template)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(configCmd)
|
||||||
|
|
||||||
|
defaultConfig := config.ConfigStruct{}
|
||||||
|
defaults.Set(&defaultConfig)
|
||||||
|
|
||||||
|
configCmd.Flags().BoolP(configStructs.RegenerateConfigName, "r", defaultConfig.Config.Regenerate, fmt.Sprintf("Regenerate the config file with default values to path %s or to chosen path using --%s", defaultConfig.ConfigFilePath, config.ConfigFilePathCommandName))
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MizuFetchOptions struct {
|
|
||||||
Limit uint16
|
|
||||||
Directory string
|
|
||||||
}
|
|
||||||
|
|
||||||
var mizuFetchOptions = MizuFetchOptions{}
|
|
||||||
|
|
||||||
var fetchCmd = &cobra.Command{
|
|
||||||
Use: "fetch",
|
|
||||||
Short: "Download recorded traffic to files",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
RunMizuFetch(&mizuFetchOptions)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(fetchCmd)
|
|
||||||
|
|
||||||
fetchCmd.Flags().Uint16VarP(&mizuFetchOptions.Limit, "limit", "l", 1000, "Provide a custom limit for entries to fetch")
|
|
||||||
fetchCmd.Flags().StringVarP(&mizuFetchOptions.Directory, "directory", "d", ".", "Provide a custom directory for fetched entries")
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RunMizuFetch(fetch *MizuFetchOptions) {
|
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:8899/api/har?limit=%v", fetch.Limit))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = Unzip(zipReader, fetch.Directory)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unzip(reader *zip.Reader, dest string) error {
|
|
||||||
dest, _ = filepath.Abs(dest)
|
|
||||||
_ = os.MkdirAll(dest, os.ModePerm)
|
|
||||||
|
|
||||||
// Closure to address file descriptors issue with all the deferred .Close() methods
|
|
||||||
extractAndWriteFile := func(f *zip.File) error {
|
|
||||||
rc, err := f.Open()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := rc.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
path := filepath.Join(dest, f.Name)
|
|
||||||
|
|
||||||
// Check for ZipSlip (Directory traversal)
|
|
||||||
if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
|
|
||||||
return fmt.Errorf("illegal file path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
|
||||||
_ = os.MkdirAll(path, f.Mode())
|
|
||||||
} else {
|
|
||||||
_ = os.MkdirAll(filepath.Dir(path), f.Mode())
|
|
||||||
fmt.Print("writing HAR file [ ", path, " ] .. ")
|
|
||||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println(" done")
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = io.Copy(f, rc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range reader.File {
|
|
||||||
err := extractAndWriteFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
51
cli/cmd/logs.go
Normal file
51
cli/cmd/logs.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
|
"github.com/up9inc/mizu/cli/mizu/fsUtils"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logsCmd = &cobra.Command{
|
||||||
|
Use: "logs",
|
||||||
|
Short: "Create a zip file with logs for Github issue or troubleshoot",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
go telemetry.ReportRun("logs", config.Config.Logs)
|
||||||
|
|
||||||
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Error(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx, _ := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
if validationErr := config.Config.Logs.Validate(); validationErr != nil {
|
||||||
|
return errormessage.FormatError(validationErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Using file path %s", config.Config.Logs.FilePath())
|
||||||
|
|
||||||
|
if dumpLogsErr := fsUtils.DumpLogs(ctx, kubernetesProvider, config.Config.Logs.FilePath()); dumpLogsErr != nil {
|
||||||
|
logger.Log.Errorf("Failed dump logs %v", dumpLogsErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(logsCmd)
|
||||||
|
|
||||||
|
defaultLogsConfig := configStructs.LogsConfig{}
|
||||||
|
defaults.Set(&defaultLogsConfig)
|
||||||
|
|
||||||
|
logsCmd.Flags().StringP(configStructs.FileLogsName, "f", defaultLogsConfig.FileStr, "Path for zip file (default current <pwd>\\mizu_logs.zip)")
|
||||||
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"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"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -9,10 +19,43 @@ var rootCmd = &cobra.Command{
|
|||||||
Short: "A web traffic viewer for kubernetes",
|
Short: "A web traffic viewer for kubernetes",
|
||||||
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 {
|
||||||
|
if err := config.InitConfig(cmd); err != nil {
|
||||||
|
logger.Log.Fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultConfig := config.ConfigStruct{}
|
||||||
|
defaults.Set(&defaultConfig)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringSlice(config.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", config.SetCommandName))
|
||||||
|
rootCmd.PersistentFlags().String(config.ConfigFilePathCommandName, defaultConfig.ConfigFilePath, fmt.Sprintf("Override config file path using --%s", config.ConfigFilePathCommandName))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNewVersionIfNeeded(versionChan chan string) {
|
||||||
|
select {
|
||||||
|
case versionMsg := <-versionChan:
|
||||||
|
if versionMsg != "" {
|
||||||
|
logger.Log.Infof(uiUtils.Yellow, versionMsg)
|
||||||
|
}
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(fsUtils.GetLogFilePath())
|
||||||
|
|
||||||
|
versionChan := make(chan string)
|
||||||
|
defer printNewVersionIfNeeded(versionChan)
|
||||||
|
go version.CheckNewerVersion(versionChan)
|
||||||
|
|
||||||
cobra.CheckErr(rootCmd.Execute())
|
cobra.CheckErr(rootCmd.Execute())
|
||||||
}
|
}
|
||||||
|
|||||||
105
cli/cmd/tap.go
105
cli/cmd/tap.go
@@ -3,55 +3,104 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/up9inc/mizu/cli/mizu"
|
"os"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
|
"github.com/creasty/defaults"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/up9inc/mizu/cli/auth"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MizuTapOptions struct {
|
const uploadTrafficMessageToConfirm = `NOTE: running mizu with --%s flag will upload recorded traffic for further analysis and enriched presentation options.`
|
||||||
GuiPort uint16
|
|
||||||
Namespace string
|
|
||||||
AllNamespaces bool
|
|
||||||
KubeConfigPath string
|
|
||||||
MizuImage string
|
|
||||||
MizuPodPort uint16
|
|
||||||
PlainTextFilterRegexes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var mizuTapOptions = &MizuTapOptions{}
|
|
||||||
|
|
||||||
var tapCmd = &cobra.Command{
|
var tapCmd = &cobra.Command{
|
||||||
Use: "tap [POD REGEX]",
|
Use: "tap [POD REGEX]",
|
||||||
Short: "Record ingoing traffic of a kubernetes pod",
|
Short: "Record ingoing traffic of a kubernetes pod",
|
||||||
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 {
|
||||||
if len(args) == 0 {
|
go telemetry.ReportRun("tap", config.Config.Tap)
|
||||||
return errors.New("POD REGEX argument is required")
|
RunMizuTap()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 1 {
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
regex, err := regexp.Compile(args[0])
|
if err := config.Config.Tap.Validate(); err != nil {
|
||||||
if err != nil {
|
return errormessage.FormatError(err)
|
||||||
return errors.New(fmt.Sprintf("%s is not a valid regex %s", args[0], err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RunMizuTap(regex, mizuTapOptions)
|
if config.Config.Tap.Workspace != "" {
|
||||||
|
askConfirmation(configStructs.WorkspaceTapName)
|
||||||
|
|
||||||
|
if config.Config.Auth.Token == "" {
|
||||||
|
logger.Log.Infof("This action requires authentication, please log in to continue")
|
||||||
|
if err := auth.Login(); err != nil {
|
||||||
|
logger.Log.Errorf("failed to log in, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokenExpired, err := shared.IsTokenExpired(config.Config.Auth.Token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf("failed to check if token is expired, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenExpired {
|
||||||
|
logger.Log.Infof("Token expired, please log in again to continue")
|
||||||
|
if err := auth.Login(); err != nil {
|
||||||
|
logger.Log.Errorf("failed to log in, err: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Config.Tap.Analysis {
|
||||||
|
askConfirmation(configStructs.AnalysisTapName)
|
||||||
|
|
||||||
|
config.Config.Auth.Token = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func askConfirmation(flagName string) {
|
||||||
|
logger.Log.Infof(fmt.Sprintf(uploadTrafficMessageToConfirm, flagName))
|
||||||
|
if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") {
|
||||||
|
logger.Log.Infof("You can always run mizu without %s, aborting", flagName)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(tapCmd)
|
rootCmd.AddCommand(tapCmd)
|
||||||
|
|
||||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver")
|
defaultTapConfig := configStructs.TapConfig{}
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.Namespace, "namespace", "n", "", "Namespace selector")
|
defaults.Set(&defaultTapConfig)
|
||||||
tapCmd.Flags().BoolVarP(&mizuTapOptions.AllNamespaces, "all-namespaces", "A", false, "Tap all namespaces")
|
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.KubeConfigPath, "kube-config", "k", "", "Path to kube-config file")
|
tapCmd.Flags().Uint16P(configStructs.GuiPortTapName, "p", defaultTapConfig.GuiPort, "Provide a custom port for the web interface webserver")
|
||||||
tapCmd.Flags().StringVarP(&mizuTapOptions.MizuImage, "mizu-image", "", fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:latest", mizu.Branch), "Custom image for mizu collector")
|
tapCmd.Flags().StringSliceP(configStructs.NamespacesTapName, "n", defaultTapConfig.Namespaces, "Namespaces selector")
|
||||||
tapCmd.Flags().Uint16VarP(&mizuTapOptions.MizuPodPort, "mizu-port", "", 8899, "Port which mizu cli will attempt to forward from the mizu collector pod")
|
tapCmd.Flags().Bool(configStructs.AnalysisTapName, defaultTapConfig.Analysis, "Uploads traffic to UP9 for further analysis (Beta)")
|
||||||
tapCmd.Flags().StringArrayVarP(&mizuTapOptions.PlainTextFilterRegexes, "regex-masking", "r", nil, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
tapCmd.Flags().BoolP(configStructs.AllNamespacesTapName, "A", defaultTapConfig.AllNamespaces, "Tap all namespaces")
|
||||||
|
tapCmd.Flags().StringSliceP(configStructs.PlainTextFilterRegexesTapName, "r", defaultTapConfig.PlainTextFilterRegexes, "List of regex expressions that are used to filter matching values from text/plain http bodies")
|
||||||
|
tapCmd.Flags().Bool(configStructs.DisableRedactionTapName, defaultTapConfig.DisableRedaction, "Disables redaction of potentially sensitive request/response headers and body values")
|
||||||
|
tapCmd.Flags().String(configStructs.HumanMaxEntriesDBSizeTapName, defaultTapConfig.HumanMaxEntriesDBSize, "Override the default max entries db size")
|
||||||
|
tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them")
|
||||||
|
tapCmd.Flags().StringP(configStructs.WorkspaceTapName, "w", defaultTapConfig.Workspace, "Uploads traffic to your UP9 workspace for further analysis (requires auth)")
|
||||||
|
tapCmd.Flags().String(configStructs.EnforcePolicyFile, defaultTapConfig.EnforcePolicyFile, "Yaml file path with policy rules")
|
||||||
|
tapCmd.Flags().String(configStructs.ContractFile, defaultTapConfig.ContractFile, "OAS/Swagger file to validate to monitor the contracts")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,167 +3,441 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/up9inc/mizu/shared"
|
"io/ioutil"
|
||||||
"os"
|
"path"
|
||||||
"os/signal"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"syscall"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
core "k8s.io/api/core/v1"
|
core "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
|
||||||
|
"github.com/getkin/kin-openapi/openapi3"
|
||||||
|
"github.com/up9inc/mizu/cli/apiserver"
|
||||||
|
"github.com/up9inc/mizu/cli/config"
|
||||||
|
"github.com/up9inc/mizu/cli/config/configStructs"
|
||||||
|
"github.com/up9inc/mizu/cli/errormessage"
|
||||||
|
|
||||||
"github.com/up9inc/mizu/cli/debounce"
|
|
||||||
"github.com/up9inc/mizu/cli/kubernetes"
|
"github.com/up9inc/mizu/cli/kubernetes"
|
||||||
"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/goUtils"
|
||||||
|
"github.com/up9inc/mizu/cli/telemetry"
|
||||||
|
"github.com/up9inc/mizu/cli/uiUtils"
|
||||||
|
"github.com/up9inc/mizu/shared"
|
||||||
|
"github.com/up9inc/mizu/shared/debounce"
|
||||||
|
"github.com/up9inc/mizu/shared/logger"
|
||||||
|
"github.com/up9inc/mizu/tap/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var mizuServiceAccountExists bool
|
|
||||||
var aggregatorService *core.Service
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
cleanupTimeout = time.Minute
|
||||||
updateTappersDelay = 5 * time.Second
|
updateTappersDelay = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var currentlyTappedPods []core.Pod
|
type tapState struct {
|
||||||
|
apiServerService *core.Service
|
||||||
|
currentlyTappedPods []core.Pod
|
||||||
|
mizuServiceAccountExists bool
|
||||||
|
}
|
||||||
|
|
||||||
func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
var state tapState
|
||||||
mizuApiFilteringOptions, err := getMizuApiFilteringOptions(tappingOptions)
|
|
||||||
|
func RunMizuTap() {
|
||||||
|
mizuApiFilteringOptions, err := getMizuApiFilteringOptions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error parsing regex-masking: %v", errormessage.FormatError(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
kubernetesProvider := kubernetes.NewProvider(tappingOptions.KubeConfigPath)
|
var mizuValidationRules string
|
||||||
|
if config.Config.Tap.EnforcePolicyFile != "" {
|
||||||
|
mizuValidationRules, err = readValidationRules(config.Config.Tap.EnforcePolicyFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading policy file: %v", errormessage.FormatError(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and validate the OAS file
|
||||||
|
var contract string
|
||||||
|
if config.Config.Tap.ContractFile != "" {
|
||||||
|
bytes, err := ioutil.ReadFile(config.Config.Tap.ContractFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading contract file: %v", errormessage.FormatError(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contract = string(bytes)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
loader := &openapi3.Loader{Context: ctx}
|
||||||
|
doc, err := loader.LoadFromData(bytes)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error loading contract file: %v", errormessage.FormatError(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = doc.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error validating contract file: %v", errormessage.FormatError(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kubernetesProvider, err := kubernetes.NewProvider(config.Config.KubeConfigPath())
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
defer cleanUpMizuResources(kubernetesProvider)
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel() // cancel will be called when this function exits
|
defer cancel() // cancel will be called when this function exits
|
||||||
|
|
||||||
if matchingPods, err := kubernetesProvider.GetAllPodsMatchingRegex(ctx, podRegexQuery); err != nil {
|
targetNamespaces := getNamespaces(kubernetesProvider)
|
||||||
return
|
|
||||||
|
if config.Config.IsNsRestrictedMode() {
|
||||||
|
if len(targetNamespaces) != 1 || !shared.Contains(targetNamespaces, config.Config.MizuResourcesNamespace) {
|
||||||
|
logger.Log.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+
|
||||||
|
"You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, config.MizuResourcesNamespaceConfigName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var namespacesStr string
|
||||||
|
if !shared.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
|
||||||
|
namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \""))
|
||||||
} else {
|
} else {
|
||||||
currentlyTappedPods = matchingPods
|
namespacesStr = "all namespaces"
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods)
|
logger.Log.Infof("Tapping pods in %s", namespacesStr)
|
||||||
if err != nil {
|
|
||||||
|
if err, _ := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces); err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error getting pods by regex: %v", errormessage.FormatError(err)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions, mizuApiFilteringOptions); err != nil {
|
if len(state.currentlyTappedPods) == 0 {
|
||||||
|
var suggestionStr string
|
||||||
|
if !shared.Contains(targetNamespaces, mizu.K8sAllNamespaces) {
|
||||||
|
suggestionStr = ". Select a different namespace with -n or tap all namespaces with -A"
|
||||||
|
}
|
||||||
|
logger.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Did not find any pods matching the regex argument%s", suggestionStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Config.Tap.DryRun {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go portForwardApiPod(ctx, kubernetesProvider, cancel, tappingOptions) // TODO convert this to job for built in pod ttl or have the running app handle this
|
defer finishMizuExecution(kubernetesProvider)
|
||||||
go watchPodsForTapping(ctx, kubernetesProvider, cancel, podRegexQuery, tappingOptions)
|
if err := createMizuResources(ctx, kubernetesProvider, mizuValidationRules, contract); err != nil {
|
||||||
go syncApiStatus(ctx, cancel, tappingOptions)
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error creating resources: %v", errormessage.FormatError(err)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//block until exit signal or error
|
go goUtils.HandleExcWrapper(watchApiServerPod, ctx, kubernetesProvider, cancel, mizuApiFilteringOptions)
|
||||||
|
go goUtils.HandleExcWrapper(watchTapperPod, ctx, kubernetesProvider, cancel)
|
||||||
|
go goUtils.HandleExcWrapper(watchPodsForTapping, ctx, kubernetesProvider, targetNamespaces, cancel, mizuApiFilteringOptions)
|
||||||
|
|
||||||
|
// block until exit signal or error
|
||||||
waitForFinish(ctx, cancel)
|
waitForFinish(ctx, cancel)
|
||||||
|
|
||||||
// TODO handle incoming traffic from tapper using a channel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, tappingOptions *MizuTapOptions, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error {
|
func readValidationRules(file string) (string, error) {
|
||||||
if err := createMizuAggregator(ctx, kubernetesProvider, tappingOptions, mizuApiFilteringOptions); err != nil {
|
rules, err := shared.DecodeEnforcePolicy(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
newContent, _ := yaml.Marshal(&rules)
|
||||||
|
return string(newContent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, mizuValidationRules string, contract string) error {
|
||||||
|
if !config.Config.IsNsRestrictedMode() {
|
||||||
|
if err := createMizuNamespace(ctx, kubernetesProvider); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createMizuApiServer(ctx, kubernetesProvider); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := createMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions); err != nil {
|
if err := createMizuConfigmap(ctx, kubernetesProvider, mizuValidationRules, contract); err != nil {
|
||||||
return 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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMizuAggregator(ctx context.Context, kubernetesProvider *kubernetes.Provider, tappingOptions *MizuTapOptions, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error {
|
func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, data string, contract string) error {
|
||||||
|
err := kubernetesProvider.CreateConfigMap(ctx, config.Config.MizuResourcesNamespace, mizu.ConfigMapName, data, contract)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Provider) error {
|
||||||
|
_, err := kubernetesProvider.CreateNamespace(ctx, config.Config.MizuResourcesNamespace)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Provider) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
mizuServiceAccountExists = createRBACIfNecessary(ctx, kubernetesProvider)
|
state.mizuServiceAccountExists, err = createRBACIfNecessary(ctx, kubernetesProvider)
|
||||||
_, err = kubernetesProvider.CreateMizuAggregatorPod(ctx, mizu.ResourcesNamespace, mizu.AggregatorPodName, tappingOptions.MizuImage, mizuServiceAccountExists, mizuApiFilteringOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating mizu collector pod: %v\n", 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)))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregatorService, err = kubernetesProvider.CreateService(ctx, mizu.ResourcesNamespace, mizu.AggregatorPodName, mizu.AggregatorPodName)
|
var serviceAccountName string
|
||||||
|
if state.mizuServiceAccountExists {
|
||||||
|
serviceAccountName = mizu.ServiceAccountName
|
||||||
|
} else {
|
||||||
|
serviceAccountName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &kubernetes.ApiServerOptions{
|
||||||
|
Namespace: config.Config.MizuResourcesNamespace,
|
||||||
|
PodName: mizu.ApiServerPodName,
|
||||||
|
PodImage: config.Config.AgentImage,
|
||||||
|
ServiceAccountName: serviceAccountName,
|
||||||
|
IsNamespaceRestricted: config.Config.IsNsRestrictedMode(),
|
||||||
|
SyncEntriesConfig: getSyncEntriesConfig(),
|
||||||
|
MaxEntriesDBSizeBytes: config.Config.Tap.MaxEntriesDBSizeBytes(),
|
||||||
|
Resources: config.Config.Tap.ApiServerResources,
|
||||||
|
ImagePullPolicy: config.Config.ImagePullPolicy(),
|
||||||
|
}
|
||||||
|
_, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating mizu collector service: %v\n", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
logger.Log.Debugf("Successfully created API server pod: %s", mizu.ApiServerPodName)
|
||||||
|
|
||||||
|
state.apiServerService, err = kubernetesProvider.CreateService(ctx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName, mizu.ApiServerPodName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMizuApiFilteringOptions(tappingOptions *MizuTapOptions) (*shared.TrafficFilteringOptions, error) {
|
func getMizuApiFilteringOptions() (*api.TrafficFilteringOptions, error) {
|
||||||
if tappingOptions.PlainTextFilterRegexes == nil || len(tappingOptions.PlainTextFilterRegexes) == 0 {
|
var compiledRegexSlice []*api.SerializableRegexp
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
compiledRegexSlice := make([]*shared.SerializableRegexp, 0)
|
if config.Config.Tap.PlainTextFilterRegexes != nil && len(config.Config.Tap.PlainTextFilterRegexes) > 0 {
|
||||||
for _, regexStr := range tappingOptions.PlainTextFilterRegexes {
|
compiledRegexSlice = make([]*api.SerializableRegexp, 0)
|
||||||
compiledRegex, err := shared.CompileRegexToSerializableRegexp(regexStr)
|
for _, regexStr := range config.Config.Tap.PlainTextFilterRegexes {
|
||||||
if err != nil {
|
compiledRegex, err := api.CompileRegexToSerializableRegexp(regexStr)
|
||||||
fmt.Printf("Regex %s is invalid: %v", regexStr, err)
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
compiledRegexSlice = append(compiledRegexSlice, compiledRegex)
|
||||||
}
|
}
|
||||||
compiledRegexSlice = append(compiledRegexSlice, compiledRegex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &shared.TrafficFilteringOptions{PlainTextMaskingRegexes: compiledRegexSlice}, nil
|
return &api.TrafficFilteringOptions{
|
||||||
|
PlainTextMaskingRegexes: compiledRegexSlice,
|
||||||
|
IgnoredUserAgents: config.Config.Tap.IgnoredUserAgents,
|
||||||
|
DisableRedaction: config.Config.Tap.DisableRedaction,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, tappingOptions *MizuTapOptions) error {
|
func getSyncEntriesConfig() *shared.SyncEntriesConfig {
|
||||||
if err := kubernetesProvider.ApplyMizuTapperDaemonSet(
|
if !config.Config.Tap.Analysis && config.Config.Tap.Workspace == "" {
|
||||||
ctx,
|
return nil
|
||||||
mizu.ResourcesNamespace,
|
}
|
||||||
mizu.TapperDaemonSetName,
|
|
||||||
tappingOptions.MizuImage,
|
return &shared.SyncEntriesConfig{
|
||||||
mizu.TapperPodName,
|
Token: config.Config.Auth.Token,
|
||||||
fmt.Sprintf("%s.%s.svc.cluster.local", aggregatorService.Name, aggregatorService.Namespace),
|
Env: config.Config.Auth.EnvName,
|
||||||
nodeToTappedPodIPMap,
|
Workspace: config.Config.Tap.Workspace,
|
||||||
mizuServiceAccountExists,
|
UploadIntervalSec: config.Config.Tap.UploadIntervalSec,
|
||||||
); err != nil {
|
}
|
||||||
fmt.Printf("Error creating mizu tapper daemonset: %v\n", err)
|
}
|
||||||
return err
|
|
||||||
|
func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, mizuApiFilteringOptions *api.TrafficFilteringOptions) error {
|
||||||
|
nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods)
|
||||||
|
|
||||||
|
if len(nodeToTappedPodIPMap) > 0 {
|
||||||
|
var serviceAccountName string
|
||||||
|
if state.mizuServiceAccountExists {
|
||||||
|
serviceAccountName = mizu.ServiceAccountName
|
||||||
|
} else {
|
||||||
|
serviceAccountName = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.ApplyMizuTapperDaemonSet(
|
||||||
|
ctx,
|
||||||
|
config.Config.MizuResourcesNamespace,
|
||||||
|
mizu.TapperDaemonSetName,
|
||||||
|
config.Config.AgentImage,
|
||||||
|
mizu.TapperPodName,
|
||||||
|
fmt.Sprintf("%s.%s.svc.cluster.local", state.apiServerService.Name, state.apiServerService.Namespace),
|
||||||
|
nodeToTappedPodIPMap,
|
||||||
|
serviceAccountName,
|
||||||
|
config.Config.Tap.TapperResources,
|
||||||
|
config.Config.ImagePullPolicy(),
|
||||||
|
mizuApiFilteringOptions,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap))
|
||||||
|
} else {
|
||||||
|
if err := kubernetesProvider.RemoveDaemonSet(ctx, config.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) {
|
func finishMizuExecution(kubernetesProvider *kubernetes.Provider) {
|
||||||
fmt.Printf("\nRemoving mizu resources\n")
|
telemetry.ReportAPICalls()
|
||||||
|
removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout)
|
||||||
|
defer cancel()
|
||||||
|
dumpLogsIfNeeded(removalCtx, kubernetesProvider)
|
||||||
|
cleanUpMizuResources(removalCtx, cancel, kubernetesProvider)
|
||||||
|
}
|
||||||
|
|
||||||
removalCtx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
func dumpLogsIfNeeded(ctx context.Context, kubernetesProvider *kubernetes.Provider) {
|
||||||
if err := kubernetesProvider.RemovePod(removalCtx, mizu.ResourcesNamespace, mizu.AggregatorPodName); err != nil {
|
if !config.Config.DumpLogs {
|
||||||
fmt.Printf("Error removing Pod %s in namespace %s: %s (%v,%+v)\n", mizu.AggregatorPodName, mizu.ResourcesNamespace, err, err, err)
|
return
|
||||||
}
|
}
|
||||||
if err := kubernetesProvider.RemoveService(removalCtx, mizu.ResourcesNamespace, mizu.AggregatorPodName); err != nil {
|
mizuDir := mizu.GetMizuFolderPath()
|
||||||
fmt.Printf("Error removing Service %s in namespace %s: %s (%v,%+v)\n", mizu.AggregatorPodName, mizu.ResourcesNamespace, err, err, err)
|
filePath := path.Join(mizuDir, fmt.Sprintf("mizu_logs_%s.zip", time.Now().Format("2006_01_02__15_04_05")))
|
||||||
}
|
if err := fsUtils.DumpLogs(ctx, kubernetesProvider, filePath); err != nil {
|
||||||
if err := kubernetesProvider.RemoveDaemonSet(removalCtx, mizu.ResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
logger.Log.Errorf("Failed dump logs %v", err)
|
||||||
fmt.Printf("Error removing DaemonSet %s in namespace %s: %s (%v,%+v)\n", mizu.TapperDaemonSetName, mizu.ResourcesNamespace, err, err, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, podRegex *regexp.Regexp, tappingOptions *MizuTapOptions) {
|
func cleanUpMizuResources(ctx context.Context, cancel context.CancelFunc, kubernetesProvider *kubernetes.Provider) {
|
||||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, getNamespace(tappingOptions, kubernetesProvider)), podRegex)
|
logger.Log.Infof("\nRemoving mizu resources\n")
|
||||||
|
|
||||||
|
var leftoverResources []string
|
||||||
|
|
||||||
|
if config.Config.IsNsRestrictedMode() {
|
||||||
|
leftoverResources = cleanUpRestrictedMode(ctx, kubernetesProvider)
|
||||||
|
} else {
|
||||||
|
leftoverResources = cleanUpNonRestrictedMode(ctx, cancel, kubernetesProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(leftoverResources) > 0 {
|
||||||
|
errMsg := fmt.Sprintf("Failed to remove the following resources, for more info check logs at %s:", fsUtils.GetLogFilePath())
|
||||||
|
for _, resource := range leftoverResources {
|
||||||
|
errMsg += "\n- " + resource
|
||||||
|
}
|
||||||
|
logger.Log.Errorf(uiUtils.Error, errMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpRestrictedMode(ctx context.Context, kubernetesProvider *kubernetes.Provider) []string {
|
||||||
|
leftoverResources := make([]string, 0)
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemovePod(ctx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("Pod %s in namespace %s", mizu.ApiServerPodName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveService(ctx, config.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("Service %s in namespace %s", mizu.ApiServerPodName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveDaemonSet(ctx, config.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("DaemonSet %s in namespace %s", mizu.TapperDaemonSetName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveConfigMap(ctx, config.Config.MizuResourcesNamespace, mizu.ConfigMapName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("ConfigMap %s in namespace %s", mizu.ConfigMapName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveServicAccount(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("Service Account %s in namespace %s", mizu.ServiceAccountName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveRole(ctx, config.Config.MizuResourcesNamespace, mizu.RoleName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("Role %s in namespace %s", mizu.RoleName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveRoleBinding(ctx, config.Config.MizuResourcesNamespace, mizu.RoleBindingName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("RoleBinding %s in namespace %s", mizu.RoleBindingName, config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftoverResources
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpNonRestrictedMode(ctx context.Context, cancel context.CancelFunc, kubernetesProvider *kubernetes.Provider) []string {
|
||||||
|
leftoverResources := make([]string, 0)
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveNamespace(ctx, config.Config.MizuResourcesNamespace); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("Namespace %s", config.Config.MizuResourcesNamespace)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
} else {
|
||||||
|
defer waitUntilNamespaceDeleted(ctx, cancel, kubernetesProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveClusterRole(ctx, mizu.ClusterRoleName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("ClusterRole %s", mizu.ClusterRoleName)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kubernetesProvider.RemoveClusterRoleBinding(ctx, mizu.ClusterRoleBindingName); err != nil {
|
||||||
|
resourceDesc := fmt.Sprintf("ClusterRoleBinding %s", mizu.ClusterRoleBindingName)
|
||||||
|
handleDeletionError(err, resourceDesc, &leftoverResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftoverResources
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDeletionError(err error, resourceDesc string, leftoverResources *[]string) {
|
||||||
|
logger.Log.Debugf("Error removing %s: %v", resourceDesc, errormessage.FormatError(err))
|
||||||
|
*leftoverResources = append(*leftoverResources, resourceDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitUntilNamespaceDeleted(ctx context.Context, cancel context.CancelFunc, kubernetesProvider *kubernetes.Provider) {
|
||||||
|
// Call cancel if a terminating signal was received. Allows user to skip the wait.
|
||||||
|
go func() {
|
||||||
|
waitForFinish(ctx, cancel)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, config.Config.MizuResourcesNamespace); err != nil {
|
||||||
|
switch {
|
||||||
|
case ctx.Err() == context.Canceled:
|
||||||
|
logger.Log.Debugf("Do nothing. User interrupted the wait")
|
||||||
|
case err == wait.ErrWaitTimeout:
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Timeout while removing Namespace %s", config.Config.MizuResourcesNamespace))
|
||||||
|
default:
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error while waiting for Namespace %s to be deleted: %v", config.Config.MizuResourcesNamespace, errormessage.FormatError(err)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, targetNamespaces []string, cancel context.CancelFunc, mizuApiFilteringOptions *api.TrafficFilteringOptions) {
|
||||||
|
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, targetNamespaces, config.Config.Tap.PodRegex())
|
||||||
|
|
||||||
restartTappers := func() {
|
restartTappers := func() {
|
||||||
if matchingPods, err := kubernetesProvider.GetAllPodsMatchingRegex(ctx, podRegex); err != nil {
|
err, changeFound := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces)
|
||||||
fmt.Printf("Error getting pods by regex: %s (%v,%+v)\n", err, err, err)
|
|
||||||
cancel()
|
|
||||||
} else {
|
|
||||||
currentlyTappedPods = matchingPods
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error building node to ips map: %s (%v,%+v)\n", err, err, err)
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Failed to update currently tapped pods: %v", err))
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := createMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions); err != nil {
|
if !changeFound {
|
||||||
fmt.Printf("Error updating daemonset: %s (%v,%+v)\n", err, err, err)
|
logger.Log.Debugf("Nothing changed update tappers not needed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := apiserver.Provider.ReportTappedPods(state.currentlyTappedPods); err != nil {
|
||||||
|
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateMizuTappers(ctx, kubernetesProvider, mizuApiFilteringOptions); err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error updating tappers: %v", errormessage.FormatError(err)))
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,94 +445,287 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case newTarget := <-added:
|
case pod, ok := <-added:
|
||||||
fmt.Printf("+%s\n", newTarget.Name)
|
if !ok {
|
||||||
|
added = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
case removedTarget := <-removed:
|
logger.Log.Debugf("Added matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||||
fmt.Printf("-%s\n", removedTarget.Name)
|
|
||||||
restartTappersDebouncer.SetOn()
|
restartTappersDebouncer.SetOn()
|
||||||
|
case pod, ok := <-removed:
|
||||||
|
if !ok {
|
||||||
|
removed = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
case modifiedTarget := <-modified:
|
logger.Log.Debugf("Removed matching pod %s, ns: %s", pod.Name, pod.Namespace)
|
||||||
|
restartTappersDebouncer.SetOn()
|
||||||
|
case pod, ok := <-modified:
|
||||||
|
if !ok {
|
||||||
|
modified = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
// - Pod reaches start state
|
// - Pod reaches start state
|
||||||
// - Pod reaches ready state
|
// - Pod reaches ready state
|
||||||
// Ready/unready transitions might also trigger this event.
|
// Ready/unready transitions might also trigger this event.
|
||||||
if modifiedTarget.Status.PodIP != "" {
|
if pod.Status.PodIP != "" {
|
||||||
restartTappersDebouncer.SetOn()
|
restartTappersDebouncer.SetOn()
|
||||||
}
|
}
|
||||||
|
case err, ok := <-errorChan:
|
||||||
|
if !ok {
|
||||||
|
errorChan = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
case <-errorChan:
|
logger.Log.Debugf("Watching pods loop, got error %v, stopping `restart tappers debouncer`", err)
|
||||||
|
restartTappersDebouncer.Cancel()
|
||||||
// TODO: Does this also perform cleanup?
|
// TODO: Does this also perform cleanup?
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
logger.Log.Debugf("Watching pods loop, context done, stopping `restart tappers debouncer`")
|
||||||
|
restartTappersDebouncer.Cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, tappingOptions *MizuTapOptions) {
|
func updateCurrentlyTappedPods(kubernetesProvider *kubernetes.Provider, ctx context.Context, targetNamespaces []string) (error, bool) {
|
||||||
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.AggregatorPodName))
|
changeFound := false
|
||||||
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, mizu.ResourcesNamespace), podExactRegex)
|
if matchingPods, err := kubernetesProvider.ListAllRunningPodsMatchingRegex(ctx, config.Config.Tap.PodRegex(), targetNamespaces); err != nil {
|
||||||
|
return err, false
|
||||||
|
} else {
|
||||||
|
podsToTap := excludeMizuPods(matchingPods)
|
||||||
|
addedPods, removedPods := getPodArrayDiff(state.currentlyTappedPods, podsToTap)
|
||||||
|
for _, addedPod := range addedPods {
|
||||||
|
changeFound = true
|
||||||
|
logger.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", addedPod.Name))
|
||||||
|
}
|
||||||
|
for _, removedPod := range removedPods {
|
||||||
|
changeFound = true
|
||||||
|
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("-%s", removedPod.Name))
|
||||||
|
}
|
||||||
|
state.currentlyTappedPods = podsToTap
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, changeFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func excludeMizuPods(pods []core.Pod) []core.Pod {
|
||||||
|
mizuPrefixRegex := regexp.MustCompile("^" + mizu.MizuResourcesPrefix)
|
||||||
|
|
||||||
|
nonMizuPods := make([]core.Pod, 0)
|
||||||
|
for _, pod := range pods {
|
||||||
|
if !mizuPrefixRegex.MatchString(pod.Name) {
|
||||||
|
nonMizuPods = append(nonMizuPods, pod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonMizuPods
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPodArrayDiff(oldPods []core.Pod, newPods []core.Pod) (added []core.Pod, removed []core.Pod) {
|
||||||
|
added = getMissingPods(newPods, oldPods)
|
||||||
|
removed = getMissingPods(oldPods, newPods)
|
||||||
|
|
||||||
|
return added, removed
|
||||||
|
}
|
||||||
|
|
||||||
|
//returns pods present in pods1 array and missing in pods2 array
|
||||||
|
func getMissingPods(pods1 []core.Pod, pods2 []core.Pod) []core.Pod {
|
||||||
|
missingPods := make([]core.Pod, 0)
|
||||||
|
for _, pod1 := range pods1 {
|
||||||
|
var found = false
|
||||||
|
for _, pod2 := range pods2 {
|
||||||
|
if pod1.UID == pod2.UID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
missingPods = append(missingPods, pod1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missingPods
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, mizuApiFilteringOptions *api.TrafficFilteringOptions) {
|
||||||
|
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.ApiServerPodName))
|
||||||
|
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, []string{config.Config.MizuResourcesNamespace}, podExactRegex)
|
||||||
isPodReady := false
|
isPodReady := false
|
||||||
var portForward *kubernetes.PortForward
|
timeAfter := time.After(25 * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-added:
|
case _, ok := <-added:
|
||||||
continue
|
if !ok {
|
||||||
case <-removed:
|
added = nil
|
||||||
fmt.Printf("%s removed\n", mizu.AggregatorPodName)
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Watching API Server pod loop, added")
|
||||||
|
case _, ok := <-removed:
|
||||||
|
if !ok {
|
||||||
|
removed = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("%s removed", mizu.ApiServerPodName)
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
case modifiedPod := <-modified:
|
case modifiedPod, ok := <-modified:
|
||||||
if modifiedPod.Status.Phase == "Running" && !isPodReady {
|
if !ok {
|
||||||
isPodReady = true
|
modified = nil
|
||||||
var err error
|
continue
|
||||||
portForward, err = kubernetes.NewPortForward(kubernetesProvider, mizu.ResourcesNamespace, mizu.AggregatorPodName, tappingOptions.GuiPort, tappingOptions.MizuPodPort, cancel)
|
}
|
||||||
fmt.Printf("Web interface is now available at http://localhost:%d\n", tappingOptions.GuiPort)
|
|
||||||
if err != nil {
|
logger.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase)
|
||||||
fmt.Printf("error forwarding port to pod %s\n", err)
|
|
||||||
|
if modifiedPod.Status.Phase == core.PodPending {
|
||||||
|
if modifiedPod.Status.Conditions[0].Type == core.PodScheduled && modifiedPod.Status.Conditions[0].Status != core.ConditionTrue {
|
||||||
|
logger.Log.Debugf("Wasn't able to deploy the API server. Reason: \"%s\"", modifiedPod.Status.Conditions[0].Message)
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Wasn't able to deploy the API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
|
||||||
cancel()
|
cancel()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(modifiedPod.Status.ContainerStatuses) > 0 && modifiedPod.Status.ContainerStatuses[0].State.Waiting != nil && modifiedPod.Status.ContainerStatuses[0].State.Waiting.Reason == "ErrImagePull" {
|
||||||
|
logger.Log.Debugf("Wasn't able to deploy the API server. (ErrImagePull) Reason: \"%s\"", modifiedPod.Status.ContainerStatuses[0].State.Waiting.Message)
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Wasn't able to deploy the API server: failed to pull the image, for more info check logs at %v", fsUtils.GetLogFilePath()))
|
||||||
|
cancel()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(25 * time.Second):
|
if modifiedPod.Status.Phase == core.PodRunning && !isPodReady {
|
||||||
if !isPodReady {
|
isPodReady = true
|
||||||
fmt.Printf("error: %s pod was not ready in time", mizu.AggregatorPodName)
|
go startProxyReportErrorIfAny(kubernetesProvider, cancel)
|
||||||
cancel()
|
|
||||||
|
url := GetApiServerUrl()
|
||||||
|
if err := apiserver.Provider.InitAndTestConnection(url); err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Couldn't connect to API server, for more info check logs at %s", fsUtils.GetLogFilePath()))
|
||||||
|
cancel()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err := updateMizuTappers(ctx, kubernetesProvider, mizuApiFilteringOptions); err != nil {
|
||||||
|
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error updating tappers: %v", errormessage.FormatError(err)))
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Infof("Mizu is available at %s\n", url)
|
||||||
|
uiUtils.OpenBrowser(url)
|
||||||
|
if err := apiserver.Provider.ReportTappedPods(state.currentlyTappedPods); err != nil {
|
||||||
|
logger.Log.Debugf("[Error] failed update tapped pods %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-errorChan:
|
||||||
|
if !ok {
|
||||||
|
errorChan = nil
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-errorChan:
|
logger.Log.Debugf("[ERROR] Agent creation, watching %v namespace, error: %v", config.Config.MizuResourcesNamespace, err)
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-timeAfter:
|
||||||
if portForward != nil {
|
if !isPodReady {
|
||||||
portForward.Stop()
|
logger.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time")
|
||||||
|
cancel()
|
||||||
}
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Log.Debugf("Watching API Server pod loop, ctx done")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) bool {
|
func watchTapperPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
|
||||||
mizuRBACExists, err := kubernetesProvider.DoesMizuRBACExist(ctx, mizu.ResourcesNamespace)
|
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s.*", mizu.TapperDaemonSetName))
|
||||||
if err != nil {
|
added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, []string{config.Config.MizuResourcesNamespace}, podExactRegex)
|
||||||
fmt.Printf("warning: could not ensure mizu rbac resources exist %v\n", err)
|
var prevPodPhase core.PodPhase
|
||||||
return false
|
for {
|
||||||
}
|
select {
|
||||||
if !mizuRBACExists {
|
case addedPod, ok := <-added:
|
||||||
err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.ResourcesNamespace, mizu.RBACVersion)
|
if !ok {
|
||||||
if err != nil {
|
added = nil
|
||||||
fmt.Printf("warning: could not create mizu rbac resources %v\n", err)
|
continue
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Tapper is created [%s]", addedPod.Name)
|
||||||
|
case removedPod, ok := <-removed:
|
||||||
|
if !ok {
|
||||||
|
removed = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Tapper is removed [%s]", removedPod.Name)
|
||||||
|
case modifiedPod, ok := <-modified:
|
||||||
|
if !ok {
|
||||||
|
modified = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if modifiedPod.Status.Phase == core.PodPending && modifiedPod.Status.Conditions[0].Type == core.PodScheduled && modifiedPod.Status.Conditions[0].Status != core.ConditionTrue {
|
||||||
|
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("Wasn't able to deploy the tapper %s. Reason: \"%s\"", modifiedPod.Name, modifiedPod.Status.Conditions[0].Message))
|
||||||
|
cancel()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
podStatus := modifiedPod.Status
|
||||||
|
if podStatus.Phase == core.PodPending && prevPodPhase == podStatus.Phase {
|
||||||
|
logger.Log.Debugf("Tapper %s is %s", modifiedPod.Name, strings.ToLower(string(podStatus.Phase)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prevPodPhase = podStatus.Phase
|
||||||
|
|
||||||
|
if podStatus.Phase == core.PodRunning {
|
||||||
|
state := podStatus.ContainerStatuses[0].State
|
||||||
|
if state.Terminated != nil {
|
||||||
|
switch state.Terminated.Reason {
|
||||||
|
case "OOMKilled":
|
||||||
|
logger.Log.Infof(uiUtils.Red, fmt.Sprintf("Tapper %s was terminated (reason: OOMKilled). You should consider increasing machine resources.", modifiedPod.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("Tapper %s is %s", modifiedPod.Name, strings.ToLower(string(podStatus.Phase)))
|
||||||
|
case err, ok := <-errorChan:
|
||||||
|
if !ok {
|
||||||
|
errorChan = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log.Debugf("[Error] Error in mizu tapper watch, err: %v", err)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Log.Debugf("Watching tapper pod loop, ctx done")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) (map[string][]string, error) {
|
func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) (bool, error) {
|
||||||
|
if !config.Config.IsNsRestrictedMode() {
|
||||||
|
err := kubernetesProvider.CreateMizuRBAC(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := kubernetesProvider.CreateMizuRBACNamespaceRestricted(ctx, config.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.RoleName, mizu.RoleBindingName, mizu.RBACVersion)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) map[string][]string {
|
||||||
nodeToTappedPodIPMap := make(map[string][]string, 0)
|
nodeToTappedPodIPMap := make(map[string][]string, 0)
|
||||||
for _, pod := range tappedPods {
|
for _, pod := range tappedPods {
|
||||||
existingList := nodeToTappedPodIPMap[pod.Spec.NodeName]
|
existingList := nodeToTappedPodIPMap[pod.Spec.NodeName]
|
||||||
@@ -268,50 +735,15 @@ func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) (map[string][]string, e
|
|||||||
nodeToTappedPodIPMap[pod.Spec.NodeName] = append(nodeToTappedPodIPMap[pod.Spec.NodeName], pod.Status.PodIP)
|
nodeToTappedPodIPMap[pod.Spec.NodeName] = append(nodeToTappedPodIPMap[pod.Spec.NodeName], pod.Status.PodIP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodeToTappedPodIPMap, nil
|
return nodeToTappedPodIPMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForFinish(ctx context.Context, cancel context.CancelFunc) {
|
func getNamespaces(kubernetesProvider *kubernetes.Provider) []string {
|
||||||
sigChan := make(chan os.Signal, 1)
|
if config.Config.Tap.AllNamespaces {
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
|
return []string{mizu.K8sAllNamespaces}
|
||||||
|
} else if len(config.Config.Tap.Namespaces) > 0 {
|
||||||
// block until ctx cancel is called or termination signal is received
|
return shared.Unique(config.Config.Tap.Namespaces)
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
break
|
|
||||||
case <-sigChan:
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncApiStatus(ctx context.Context, cancel context.CancelFunc, tappingOptions *MizuTapOptions) {
|
|
||||||
controlSocket, err := mizu.CreateControlSocket(fmt.Sprintf("ws://localhost:%d/ws", tappingOptions.GuiPort))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error establishing control socket connection %s\n", err)
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
err = controlSocket.SendNewTappedPodsListMessage(currentlyTappedPods)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("error Sending message via control socket %s\n", err)
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNamespace(tappingOptions *MizuTapOptions, kubernetesProvider *kubernetes.Provider) string {
|
|
||||||
if tappingOptions.AllNamespaces {
|
|
||||||
return mizu.K8sAllNamespaces
|
|
||||||
} else if len(tappingOptions.Namespace) > 0 {
|
|
||||||
return tappingOptions.Namespace
|
|
||||||
} else {
|
} else {
|
||||||
return kubernetesProvider.CurrentNamespace()
|
return []string{kubernetesProvider.CurrentNamespace()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user