diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5beaeb446..531e75f34 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -66,6 +66,9 @@ jobs: COMMIT_HASH=${{ github.sha }} - name: Build and Push CLI 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 uses: ncipollo/release-action@v1 with: @@ -75,3 +78,4 @@ jobs: tag: ${{ steps.versioning.outputs.version }} prerelease: ${{ github.ref != 'refs/heads/main' }} bodyFile: 'cli/bin/README.md' + diff --git a/Dockerfile b/Dockerfile index 0fa90f951..c19b1805f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,10 +13,10 @@ ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64 RUN apk add libpcap-dev gcc g++ make -# Move to api working directory (/api-build). -WORKDIR /app/api-build +# Move to agent working directory (/agent-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 tap/go.mod tap/go.mod ../tap/ RUN go mod download @@ -28,10 +28,10 @@ ARG GIT_BRANCH ARG BUILD_TIMESTAMP ARG SEM_VER -# Copy and build api code +# Copy and build agent code COPY shared ../shared COPY tap ../tap -COPY api . +COPY agent . RUN go build -ldflags="-s -w \ -X 'mizuserver/pkg/version.GitCommitHash=${COMMIT_HASH}' \ -X 'mizuserver/pkg/version.Branch=${GIT_BRANCH}' \ @@ -45,10 +45,13 @@ RUN apk add bash libpcap-dev tcpdump WORKDIR /app # 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=site-build ["/app/ui-build/build", "site"] -COPY api/start.sh . +COPY agent/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 ENTRYPOINT "/app/mizuagent" diff --git a/Makefile b/Makefile index b304676bc..bb85ff8ea 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ SHELL=/bin/bash # HELP # This will output the help for each task # 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. @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -19,34 +19,30 @@ help: ## This help. TS_SUFFIX="$(shell date '+%s')" GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')" 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; ) @ls -l ui/build -cli: # build CLI +cli: ## Build CLI. @echo "building cli"; cd cli && $(MAKE) build -api: ## build API server - @(echo "building API server .." ) - @(cd api; go build -o build/apiserver main.go) - @ls -l api/build +agent: ## Build agent. + @(echo "building mizu agent .." ) + @(cd agent; go build -o build/mizuagent main.go) + @ls -l agent/build -#tap: ## build tap binary -# @(cd tap; go build -o build/tap ./src) -# @ls -l tap/build +docker: ## Build and publish agent docker image. + $(MAKE) push-docker -docker: ## build Docker image - @(echo "building docker image" ) - ./build-push-featurebranch.sh +push: push-docker push-cli ## Build and publish agent docker image & CLI. -push: push-docker push-cli ## build and publish Mizu docker image & CLI - -push-docker: +push-docker: ## Build and publish agent docker image. @echo "publishing Docker image .. " ./build-push-featurebranch.sh -push-cli: +push-cli: ## Build and publish CLI. @echo "publishing CLI .. " @cd cli; $(MAKE) build-all @echo "publishing file ${OUTPUT_FILE} .." @@ -55,17 +51,17 @@ push-cli: gsutil setmeta -r -h "Cache-Control:public, max-age=30" gs://${BUCKET_PATH}/\* -clean: clean-ui clean-api clean-cli clean-docker ## Clean all build artifacts +clean: clean-ui clean-agent clean-cli clean-docker ## Clean all build artifacts. -clean-ui: +clean-ui: ## Clean UI. @(rm -rf ui/build ; echo "UI cleanup done" ) -clean-api: - @(rm -rf api/build ; echo "api cleanup done" ) +clean-agent: ## Clean agent. + @(rm -rf agent/build ; echo "agent cleanup done" ) -clean-cli: +clean-cli: ## Clean CLI. @(cd cli; make clean ; echo "CLI cleanup done" ) -clean-docker: +clean-docker: @(echo "DOCKER cleanup - NOT IMPLEMENTED YET " ) diff --git a/PERMISSIONS.md b/PERMISSIONS.md new file mode 100644 index 000000000..808b2a232 --- /dev/null +++ b/PERMISSIONS.md @@ -0,0 +1,328 @@ +![Mizu: The API Traffic Viewer for Kubernetes](assets/mizu-logo.svg) +# Kubernetes permissions for MIZU + +This document describes in details all permissions required for full and correct operation of Mizu + +We broke down this list into few categories: +- Required - what is needed for `mizu` to run properly on your k8s cluster +- Optional - permissions needed for proper name resolving for service & pod IPs + - addition required for policy validation + + + +# Required permissions + +Mizu needs following permissions on your Kubernetes cluster to run properly + +```yaml +- apiGroups: + - "" + resources: + - pods + verbs: + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - create + - delete +- apiGroups: + - apps + resources: + - daemonsets + verbs: + - create + - patch + - delete +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services/proxy + verbs: + - get +``` + +## Permissions required for service / pod name resolving (opt) + +Optionally, for proper resolving of IP addresses to Kubernetes service name, Mizu needs below permissions: + +```yaml +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - apps + resources: + - daemonsets + verbs: + - create + - patch + - delete +- apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services/proxy + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterroles + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - clusterrolebindings + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - get + - create + - delete +- apiGroups: + - apps + - extensions + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - apps + - extensions + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - "" + - apps + - extensions + resources: + - endpoints + verbs: + - get + - list + - watch +``` + +## Permissions for Policy rules validation feature (opt) + +Optionally, in order to use the policy rules validation feature, Mizu requires the following additional permissions: + +```yaml +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - create + - delete +``` + +- - - + +## Namespace-Restricted mode + +Alternatively, in order to restrict Mizu to one namespace only (by setting `agent.namespace` in the config file), Mizu needs the following permissions in that namespace: + +```yaml +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - create + - delete +- apiGroups: + - apps + resources: + - daemonsets + verbs: + - get + - create + - patch + - delete +- apiGroups: + - "" + resources: + - services/proxy + verbs: + - get +``` + +### Name resolving in Namespace-Restricted mode (opt) + +To restrict Mizu to one namespace while also resolving IPs, Mizu needs the following permissions in that namespace: + +```yaml +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - create + - delete +- apiGroups: + - apps + resources: + - daemonsets + verbs: + - get + - create + - patch + - delete +- apiGroups: + - "" + resources: + - services/proxy + verbs: + - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + verbs: + - get + - create + - delete +- apiGroups: + - rbac.authorization.k8s.io + resources: + - rolebindings + verbs: + - get + - create + - delete +- apiGroups: + - apps + - extensions + resources: + - pods + verbs: + - get + - list + - watch +- apiGroups: + - apps + - extensions + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - "" + - apps + - extensions + resources: + - endpoints + verbs: + - get + - list + - watch +``` diff --git a/README.md b/README.md index 1f4d07f87..91734b4c0 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ -# 水 mizu +![Mizu: The API Traffic Viewer for Kubernetes](assets/mizu-logo.svg) +# The API Traffic Viewer for Kubernetes + A simple-yet-powerful API traffic viewer for Kubernetes to help you troubleshoot and debug your microservices. Think TCPDump and Chrome Dev Tools combined. +![Simple UI](assets/mizu-ui.png) + +## 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 (on-prem) + ## 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 - Intel ``` @@ -23,64 +34,45 @@ https://github.com/up9inc/mizu/releases/latest/download/mizu_linux_amd64 \ SHA256 checksums are available on the [Releases](https://github.com/up9inc/mizu/releases) page. -### Development (unstable) build +### Development (unstable) Build Pick one from the [Releases](https://github.com/up9inc/mizu/releases) page. ## Prerequisites -1. Set `KUBECONFIG` environment variable to your kubernetes configuration. If this is not set, mizu assumes that configuration is at `${HOME}/.kube/config` -2. mizu needs following permissions on your kubernetes cluster to run -``` -- apiGroups: - - "" - - apps - resources: - - pods - - services - verbs: - - list - - get - - create - - delete -- apiGroups: - - "" - - apps - resources: - - daemonsets - verbs: - - list - - get - - create - - patch - - delete -``` -3. Optionally, for resolving traffic ip to kubernetes service name, mizu needs below permissions -``` -- apiGroups: - - "" - - apps - - "rbac.authorization.k8s.io" - resources: - - clusterroles - - clusterrolebindings - - serviceaccounts - verbs: - - get - - create - - delete -``` +1. 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) -## How to run +For detailed list of k8s permissions see [PERMISSIONS](PERMISSIONS.md) document -1. Find pod you'd like to tap to in your Kubernetes cluster -2. Run `mizu tap PODNAME` or `mizu tap REGEX` -3. Open browser on `http://localhost:8899` as instructed .. -4. Watch the WebAPI traffic flowing .. + +## 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 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 - ``` @@ -111,3 +103,33 @@ To tap multiple pods using regex - ^C ``` +## Configuration + +Mizu can work with config file which should be stored in ${HOME}/.mizu/config.yaml (macOS: ~/.mizu/config.yaml)
+In case no config file found, defaults will be used.
+In case of partial configuration defined, all other fields will be used with defaults.
+You can always override the defaults or config file with CLI flags. + +To get the default config params run `mizu config`
+To generate a new config file with default values use `mizu config -r` + +Mizu has several undocumented flags which can be set by using --set flag (e.g., `mizu tap --set dump-logs=true`) +* **mizu-resources-namespace**: Type - String, See [Namespace-Restricted Mode](#namespace-restricted-mode) +* **telemetry**: Type - Boolean, Reports telemetry +* **dump-logs**: Type - Boolean, At the end of the execution it creates a zip file with logs (in .mizu folder) +* **kube-config-path**: Type - String, Setting the path to kube config (which isn't in standard path) + +## 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. diff --git a/api/README.md b/agent/README.md similarity index 91% rename from api/README.md rename to agent/README.md index cfa4b4627..f275a0988 100644 --- a/api/README.md +++ b/agent/README.md @@ -1,5 +1,5 @@ -# mizu API server -API server for MIZU +# mizu agent +Agent for MIZU (API server and tapper) Basic APIs: * /fetch - retrieve traffic data * /stats - retrieve statistics of collected data @@ -14,7 +14,7 @@ Basic APIs: ### 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}` -2. Forward the debug port using `kubectl port-forward -n default mizu-collector 2345:2345` +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. Do note that dlv won't start the api until a debugger connects to it. diff --git a/api/go.mod b/agent/go.mod similarity index 75% rename from api/go.mod rename to agent/go.mod index 27833d4e4..9638a6e30 100644 --- a/api/go.mod +++ b/agent/go.mod @@ -3,27 +3,28 @@ module mizuserver go 1.16 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/fasthttp/websocket v1.4.3-beta.1 // indirect + github.com/fsnotify/fsnotify v1.4.9 + 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/universal-translator v0.17.0 github.com/go-playground/validator/v10 v10.5.0 - github.com/gofiber/fiber/v2 v2.8.0 github.com/google/martian v2.1.0+incompatible github.com/gorilla/websocket v1.4.2 - github.com/leodido/go-urn v1.2.1 // indirect + github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 github.com/up9inc/mizu/shared v0.0.0 github.com/up9inc/mizu/tap v0.0.0 + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 go.mongodb.org/mongo-driver v1.5.1 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.8 k8s.io/api v0.21.0 k8s.io/apimachinery v0.21.0 k8s.io/client-go v0.21.0 - github.com/fsnotify/fsnotify v1.4.9 ) replace github.com/up9inc/mizu/shared v0.0.0 => ../shared diff --git a/api/go.sum b/agent/go.sum similarity index 92% rename from api/go.sum rename to agent/go.sum index 28981733e..0917a0fbb 100644 --- a/api/go.sum +++ b/agent/go.sum @@ -41,15 +41,12 @@ 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/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/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/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M= +github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM= 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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -69,14 +66,18 @@ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb 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/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/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.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +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-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-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -96,6 +97,8 @@ 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/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/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/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -124,12 +127,6 @@ 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.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/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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -190,7 +187,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/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/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -202,6 +198,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW 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/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/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -210,13 +207,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/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/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.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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -226,13 +217,14 @@ 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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.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-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 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/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/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -255,6 +247,7 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -269,9 +262,6 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 h1:jkvpcEatpwuMF5O5LVxTnehj6YZ/aEZN4NWD/Xml4pI= github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7/go.mod h1:KTrHyWpO1sevuXPZwyeZc72ddWRFqNSKDFl7uVWKpg0= -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.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -283,28 +273,22 @@ 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/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.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +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.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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.9.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= -github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= -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/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 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/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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -377,11 +361,8 @@ 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-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-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-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= @@ -420,17 +401,15 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w 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-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-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-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-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-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-20201210223839-7e3030f88018/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= diff --git a/api/main.go b/agent/main.go similarity index 60% rename from api/main.go rename to agent/main.go index 40c930b73..381433832 100644 --- a/api/main.go +++ b/agent/main.go @@ -4,38 +4,41 @@ import ( "encoding/json" "flag" "fmt" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gin-contrib/static" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/romana/rlog" "github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/tap" "mizuserver/pkg/api" - "mizuserver/pkg/middleware" "mizuserver/pkg/models" "mizuserver/pkg/routes" "mizuserver/pkg/sensitiveDataFiltering" "mizuserver/pkg/utils" + "net/http" "os" "os/signal" "strings" ) -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") +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)") func main() { flag.Parse() hostMode := os.Getenv(shared.HostModeEnvVar) == "1" tapOpts := &tap.TapOpts{HostMode: hostMode} - if !*shouldTap && !*aggregator && !*standalone { + if !*tapperMode && !*apiServerMode && !*standaloneMode { panic("One of the flags --tap, --api or --standalone must be provided") } - if *standalone { + if *standaloneMode { + api.StartResolving(*namespace) + harOutputChannel, outboundLinkOutputChannel := tap.StartPassiveTapper(tapOpts) filteredHarChannel := make(chan *tap.OutputChannelItem) @@ -44,9 +47,9 @@ func main() { go api.StartReadingOutbound(outboundLinkOutputChannel) hostApi(nil) - } else if *shouldTap { - if *aggregatorAddress == "" { - panic("Aggregator address must be provided with --aggregator-address when using --tap") + } else if *tapperMode { + if *apiServerAddress == "" { + panic("API server address must be provided with --api-server-address when using --tap") } tapTargets := getTapTargets() @@ -57,14 +60,16 @@ func main() { harOutputChannel, outboundLinkOutputChannel := tap.StartPassiveTapper(tapOpts) - socketConnection, err := shared.ConnectToSocketServer(*aggregatorAddress, shared.DEFAULT_SOCKET_RETRIES, shared.DEFAULT_SOCKET_RETRY_SLEEP_TIME, false) + socketConnection, err := shared.ConnectToSocketServer(*apiServerAddress, 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)) + panic(fmt.Sprintf("Error connecting to socket server at %s %v", *apiServerAddress, err)) } - go pipeChannelToSocket(socketConnection, harOutputChannel) - go api.StartReadingOutbound(outboundLinkOutputChannel) - } else if *aggregator { + go pipeTapChannelToSocket(socketConnection, harOutputChannel) + go pipeOutboundLinksChannelToSocket(socketConnection, outboundLinkOutputChannel) + } else if *apiServerMode { + api.StartResolving(*namespace) + socketHarOutChannel := make(chan *tap.OutputChannelItem, 1000) filteredHarChannel := make(chan *tap.OutputChannelItem) @@ -82,31 +87,44 @@ func main() { } func hostApi(socketHarOutputChannel chan<- *tap.OutputChannelItem) { - app := fiber.New() + app := gin.Default() - app.Use(cors.New(cors.Config{ - AllowOrigins: "*", - AllowMethods: "*", - AllowHeaders: "*", - })) - 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 👋!") + app.GET("/echo", func(c *gin.Context) { + c.String(http.StatusOK, "Here is Mizu agent") }) + eventHandlers := api.RoutesEventHandlers{ SocketHarOutChannel: socketHarOutputChannel, } - routes.WebSocketRoutes(app, &eventHandlers) + + 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 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 getTapTargets() []string { nodeName := os.Getenv(shared.NodeNameEnvVar) var tappedAddressesPerNodeDict map[string][]string @@ -143,7 +161,9 @@ func filterHarItems(inChannel <-chan *tap.OutputChannelItem, outChannel chan *ta continue } - sensitiveDataFiltering.FilterSensitiveInfoFromHarRequest(message, filterOptions) + if !filterOptions.DisableRedaction { + sensitiveDataFiltering.FilterSensitiveInfoFromHarRequest(message, filterOptions) + } outChannel <- message } @@ -163,7 +183,7 @@ func isHealthCheckByUserAgent(message *tap.OutputChannelItem) bool { return false } -func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) { +func pipeTapChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan *tap.OutputChannelItem) { if connection == nil { panic("Websocket connection is nil") } @@ -186,3 +206,21 @@ func pipeChannelToSocket(connection *websocket.Conn, messageDataChannel <-chan * } } } + +func pipeOutboundLinksChannelToSocket(connection *websocket.Conn, outboundLinkChannel <-chan *tap.OutboundLink) { + for outboundLink := range outboundLinkChannel { + if outboundLink.SuggestedProtocol == tap.TLSProtocol { + marshaledData, err := models.CreateWebsocketOutboundLinkMessage(outboundLink) + if err != nil { + rlog.Infof("Error converting outbound link to json %s, (%v,%+v)", err, err, err) + continue + } + + err = connection.WriteMessage(websocket.TextMessage, marshaledData) + if err != nil { + rlog.Infof("error sending outbound link message through socket server %s, (%v,%+v)", err, err, err) + continue + } + } + } +} diff --git a/api/pkg/api/main.go b/agent/pkg/api/main.go similarity index 94% rename from api/pkg/api/main.go rename to agent/pkg/api/main.go index 711b64bf2..942f09569 100644 --- a/api/pkg/api/main.go +++ b/agent/pkg/api/main.go @@ -5,10 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/google/martian/har" - "github.com/romana/rlog" - "github.com/up9inc/mizu/tap" - "go.mongodb.org/mongo-driver/bson/primitive" "mizuserver/pkg/holder" "net/url" "os" @@ -17,6 +13,11 @@ import ( "strings" "time" + "github.com/google/martian/har" + "github.com/romana/rlog" + "github.com/up9inc/mizu/tap" + "go.mongodb.org/mongo-driver/bson/primitive" + "mizuserver/pkg/database" "mizuserver/pkg/models" "mizuserver/pkg/resolver" @@ -25,9 +26,9 @@ import ( var k8sResolver *resolver.Resolver -func init() { +func StartResolving(namespace string) { errOut := make(chan error, 100) - res, err := resolver.NewFromInCluster(errOut) + res, err := resolver.NewFromInCluster(errOut, namespace) if err != nil { rlog.Infof("error creating k8s resolver %s", err) return @@ -88,9 +89,9 @@ func startReadingFiles(workingDir string) { for _, entry := range inputHar.Log.Entries { time.Sleep(time.Millisecond * 250) connectionInfo := &tap.ConnectionInfo{ - ClientIP: fileInfo.Name(), + ClientIP: fileInfo.Name(), ClientPort: "", - ServerIP: "", + ServerIP: "", ServerPort: "", IsOutgoing: false, } @@ -118,7 +119,6 @@ func StartReadingOutbound(outboundLinkChannel <-chan *tap.OutboundLink) { } } - func saveHarToDb(entry *har.Entry, connectionInfo *tap.ConnectionInfo) { entryBytes, _ := json.Marshal(entry) serviceName, urlPath := getServiceNameFromUrl(entry.Request.URL) @@ -167,8 +167,10 @@ func saveHarToDb(entry *har.Entry, connectionInfo *tap.ConnectionInfo) { if err := models.GetEntry(&mizuEntry, &baseEntry); err != nil { return } + baseEntry.Rules = models.RunValidationRulesState(*entry, serviceName) + baseEntry.Latency = entry.Timings.Receive baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(&baseEntry) - broadcastToBrowserClients(baseEntryBytes) + BroadcastToBrowserClients(baseEntryBytes) } func getServiceNameFromUrl(inputUrl string) (string, string) { @@ -196,6 +198,5 @@ func getEstimatedEntrySizeBytes(mizuEntry models.MizuEntry) int { sizeBytes += 8 // SizeBytes bytes sizeBytes += 1 // IsOutgoing bytes - return sizeBytes } diff --git a/agent/pkg/api/socket_routes.go b/agent/pkg/api/socket_routes.go new file mode 100644 index 000000000..c44c1e047 --- /dev/null +++ b/agent/pkg/api/socket_routes.go @@ -0,0 +1,118 @@ +package api + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "github.com/romana/rlog" + "github.com/up9inc/mizu/shared/debounce" + "net/http" + "sync" + "time" +) + +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 { + rlog.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 { + rlog.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 { + rlog.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() { + rlog.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 { + rlog.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 +} diff --git a/agent/pkg/api/socket_server_handlers.go b/agent/pkg/api/socket_server_handlers.go new file mode 100644 index 000000000..f6b4627c8 --- /dev/null +++ b/agent/pkg/api/socket_server_handlers.go @@ -0,0 +1,130 @@ +package api + +import ( + "encoding/json" + "fmt" + "mizuserver/pkg/models" + "mizuserver/pkg/providers" + "mizuserver/pkg/up9" + "sync" + + "github.com/romana/rlog" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/tap" +) + +var browserClientSocketUUIDs = make([]int, 0) +var socketListLock = sync.Mutex{} + +type RoutesEventHandlers struct { + EventHandlers + SocketHarOutChannel chan<- *tap.OutputChannelItem +} + +func init() { + go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients) +} + +func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) { + if isTapper { + rlog.Infof("Websocket event - Tapper connected, socket ID: %d", socketId) + } else { + rlog.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 { + rlog.Infof("Websocket event - Tapper disconnected, socket ID: %d", socketId) + } else { + rlog.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 { + rlog.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 { + rlog.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 { + rlog.Infof("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(message, &statusMessage) + if err != nil { + rlog.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 { + rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err) + } else { + handleTLSLink(outboundLinkMessage) + } + default: + rlog.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 { + rlog.Errorf("Error marshaling outbound link message for broadcasting: %v", err) + } else { + rlog.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 +} diff --git a/api/pkg/controllers/entries_controller.go b/agent/pkg/controllers/entries_controller.go similarity index 67% rename from api/pkg/controllers/entries_controller.go rename to agent/pkg/controllers/entries_controller.go index af96feb18..a1ad22d42 100644 --- a/api/pkg/controllers/entries_controller.go +++ b/agent/pkg/controllers/entries_controller.go @@ -3,27 +3,30 @@ package controllers import ( "encoding/json" "fmt" - "github.com/gofiber/fiber/v2" - "github.com/google/martian/har" - "github.com/romana/rlog" "mizuserver/pkg/database" "mizuserver/pkg/models" + "mizuserver/pkg/providers" "mizuserver/pkg/up9" "mizuserver/pkg/utils" "mizuserver/pkg/validation" + "net/http" "strings" "time" + + "github.com/gin-gonic/gin" + "github.com/google/martian/har" + "github.com/romana/rlog" ) -func GetEntries(c *fiber.Ctx) error { +func GetEntries(c *gin.Context) { entriesFilter := &models.EntriesFilter{} - if err := c.QueryParser(entriesFilter); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + if err := c.BindQuery(entriesFilter); err != nil { + c.JSON(http.StatusBadRequest, err) } err := validation.Validate(entriesFilter) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + c.JSON(http.StatusBadRequest, err) } order := database.OperatorToOrderMapping[entriesFilter.Operator] @@ -50,18 +53,18 @@ func GetEntries(c *fiber.Ctx) error { baseEntries = append(baseEntries, harEntry) } - return c.Status(fiber.StatusOK).JSON(baseEntries) + c.JSON(http.StatusOK, baseEntries) } -func GetHARs(c *fiber.Ctx) error { - entriesFilter := &models.HarFetchRequestBody{} +func GetHARs(c *gin.Context) { + entriesFilter := &models.HarFetchRequestQuery{} order := database.OrderDesc - if err := c.QueryParser(entriesFilter); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + if err := c.BindQuery(entriesFilter); err != nil { + c.JSON(http.StatusBadRequest, err) } err := validation.Validate(entriesFilter) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + c.JSON(http.StatusBadRequest, err) } var timestampFrom, timestampTo int64 @@ -137,40 +140,45 @@ func GetHARs(c *fiber.Ctx) error { retObj[k] = bytesData } buffer := utils.ZipData(retObj) - return c.Status(fiber.StatusOK).SendStream(buffer) + c.Data(http.StatusOK, "application/octet-stream", buffer.Bytes()) } -func UploadEntries(c *fiber.Ctx) error { +func UploadEntries(c *gin.Context) { rlog.Infof("Upload entries - started\n") - uploadRequestBody := &models.UploadEntriesRequestBody{} - if err := c.QueryParser(uploadRequestBody); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + uploadParams := &models.UploadEntriesRequestQuery{} + if err := c.BindQuery(uploadParams); err != nil { + c.JSON(http.StatusBadRequest, err) + return } - if err := validation.Validate(uploadRequestBody); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + if err := validation.Validate(uploadParams); err != nil { + c.JSON(http.StatusBadRequest, err) + return } if up9.GetAnalyzeInfo().IsAnalyzing { - return c.Status(fiber.StatusBadRequest).SendString("Cannot analyze, mizu is already analyzing") + c.String(http.StatusBadRequest, "Cannot analyze, mizu is already analyzing") + return } - rlog.Infof("Upload entries - creating token. dest %s\n", uploadRequestBody.Dest) - token, err := up9.CreateAnonymousToken(uploadRequestBody.Dest) + + rlog.Infof("Upload entries - creating token. dest %s\n", uploadParams.Dest) + token, err := up9.CreateAnonymousToken(uploadParams.Dest) if err != nil { - return c.Status(fiber.StatusServiceUnavailable).SendString("Can't get token") + c.String(http.StatusServiceUnavailable, "Cannot analyze, mizu is already analyzing") + return } rlog.Infof("Upload entries - uploading. token: %s model: %s\n", token.Token, token.Model) - go up9.UploadEntriesImpl(token.Token, token.Model, uploadRequestBody.Dest, uploadRequestBody.SleepIntervalSec) - return c.Status(fiber.StatusOK).SendString("OK") + go up9.UploadEntriesImpl(token.Token, token.Model, uploadParams.Dest, uploadParams.SleepIntervalSec) + c.String(http.StatusOK, "OK") } -func GetFullEntries(c *fiber.Ctx) error { - entriesFilter := &models.HarFetchRequestBody{} - if err := c.QueryParser(entriesFilter); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) +func GetFullEntries(c *gin.Context) { + entriesFilter := &models.HarFetchRequestQuery{} + if err := c.BindQuery(entriesFilter); err != nil { + c.JSON(http.StatusBadRequest, err) } err := validation.Validate(entriesFilter) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(err) + c.JSON(http.StatusBadRequest, err) } var timestampFrom, timestampTo int64 @@ -195,38 +203,44 @@ func GetFullEntries(c *fiber.Ctx) error { } result = append(result, harEntry) } - - return c.Status(fiber.StatusOK).JSON(result) + c.JSON(http.StatusOK, result) } -func GetEntry(c *fiber.Ctx) error { +func GetEntry(c *gin.Context) { var entryData models.MizuEntry database.GetEntriesTable(). - Where(map[string]string{"entryId": c.Params("entryId")}). + Where(map[string]string{"entryId": c.Param("entryId")}). First(&entryData) fullEntry := models.FullEntryDetails{} if err := models.GetEntry(&entryData, &fullEntry); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + c.JSON(http.StatusInternalServerError, map[string]interface{}{ "error": true, "msg": "Can't get entry details", }) } - return c.Status(fiber.StatusOK).JSON(fullEntry) + fullEntryWithPolicy := models.FullEntryWithPolicy{} + if err := models.GetEntry(&entryData, &fullEntryWithPolicy); err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "error": true, + "msg": "Can't get entry details", + }) + } + c.JSON(http.StatusOK, fullEntryWithPolicy) } -func DeleteAllEntries(c *fiber.Ctx) error { +func DeleteAllEntries(c *gin.Context) { database.GetEntriesTable(). Where("1 = 1"). Delete(&models.MizuEntry{}) - return c.Status(fiber.StatusOK).JSON(fiber.Map{ + c.JSON(http.StatusOK, map[string]string{ "msg": "Success", }) } -func GetGeneralStats(c *fiber.Ctx) error { +func GetGeneralStats(c *gin.Context) { sqlQuery := "SELECT count(*) as count, min(timestamp) as min, max(timestamp) as max from mizu_entries" var result struct { Count int @@ -234,5 +248,17 @@ func GetGeneralStats(c *fiber.Ctx) error { Max int } database.GetEntriesTable().Raw(sqlQuery).Scan(&result) - return c.Status(fiber.StatusOK).JSON(&result) + c.JSON(http.StatusOK, result) +} + +func GetTappingStatus(c *gin.Context) { + c.JSON(http.StatusOK, providers.TapStatus) +} + +func AnalyzeInformation(c *gin.Context) { + c.JSON(http.StatusOK, up9.GetAnalyzeInfo()) +} + +func GetRecentTLSLinks(c *gin.Context) { + c.JSON(http.StatusOK, providers.GetAllRecentTLSAddresses()) } diff --git a/api/pkg/controllers/metadata_controller.go b/agent/pkg/controllers/metadata_controller.go similarity index 57% rename from api/pkg/controllers/metadata_controller.go rename to agent/pkg/controllers/metadata_controller.go index 702e682b4..37de565c8 100644 --- a/api/pkg/controllers/metadata_controller.go +++ b/agent/pkg/controllers/metadata_controller.go @@ -1,12 +1,13 @@ package controllers import ( - "github.com/gofiber/fiber/v2" + "github.com/gin-gonic/gin" "github.com/up9inc/mizu/shared" "mizuserver/pkg/version" + "net/http" ) -func GetVersion(c *fiber.Ctx) error { +func GetVersion(c *gin.Context) { resp := shared.VersionResponse{SemVer: version.SemVer} - return c.Status(fiber.StatusOK).JSON(resp) + c.JSON(http.StatusOK, resp) } diff --git a/agent/pkg/controllers/resolving_controller.go b/agent/pkg/controllers/resolving_controller.go new file mode 100644 index 000000000..dd9cc08bf --- /dev/null +++ b/agent/pkg/controllers/resolving_controller.go @@ -0,0 +1,12 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "mizuserver/pkg/holder" + "net/http" +) + +func GetCurrentResolvingInformation(c *gin.Context) { + c.JSON(http.StatusOK, holder.GetResolver().GetMap()) +} + diff --git a/agent/pkg/controllers/status_controller.go b/agent/pkg/controllers/status_controller.go new file mode 100644 index 000000000..5e046ee82 --- /dev/null +++ b/agent/pkg/controllers/status_controller.go @@ -0,0 +1,32 @@ +package controllers + +import ( + "encoding/json" + "github.com/gin-gonic/gin" + "github.com/romana/rlog" + "github.com/up9inc/mizu/shared" + "mizuserver/pkg/api" + "mizuserver/pkg/providers" + "mizuserver/pkg/validation" + "net/http" +) + +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 + } + rlog.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 { + rlog.Errorf("Could not Marshal message %v\n", err) + } else { + api.BroadcastToBrowserClients(jsonBytes) + } +} diff --git a/api/pkg/database/main.go b/agent/pkg/database/main.go similarity index 100% rename from api/pkg/database/main.go rename to agent/pkg/database/main.go diff --git a/api/pkg/database/size_enforcer.go b/agent/pkg/database/size_enforcer.go similarity index 85% rename from api/pkg/database/size_enforcer.go rename to agent/pkg/database/size_enforcer.go index 686775d35..c17c53d97 100644 --- a/api/pkg/database/size_enforcer.go +++ b/agent/pkg/database/size_enforcer.go @@ -1,8 +1,8 @@ package database import ( - "fmt" "github.com/fsnotify/fsnotify" + "github.com/romana/rlog" "github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/shared/debounce" "github.com/up9inc/mizu/shared/units" @@ -47,7 +47,7 @@ func StartEnforcingDatabaseSize() { if !ok { return // closed channel } - fmt.Printf("filesystem watcher encountered error:%v\n", err) + rlog.Errorf("filesystem watcher encountered error:%v", err) } } }() @@ -62,7 +62,7 @@ func getMaxEntriesDBByteSize() (int64, error) { maxEntriesDBByteSize := defaultMaxDatabaseSizeBytes var err error - maxEntriesDBSizeByteSEnvVarValue := os.Getenv(shared.MaxEntriesDBSizeByteSEnvVar) + maxEntriesDBSizeByteSEnvVarValue := os.Getenv(shared.MaxEntriesDBSizeBytesEnvVar) if maxEntriesDBSizeByteSEnvVarValue != "" { maxEntriesDBByteSize, err = strconv.ParseInt(maxEntriesDBSizeByteSEnvVarValue, 10, 64) } @@ -72,7 +72,7 @@ func getMaxEntriesDBByteSize() (int64, error) { func checkFileSize(maxSizeBytes int64) { fileStat, err := os.Stat(DBPath) if err != nil { - fmt.Printf("Error checking %s file size: %v\n", DBPath, err) + rlog.Errorf("Error checking %s file size: %v", DBPath, err) } else { if fileStat.Size() > maxSizeBytes { pruneOldEntries(fileStat.Size()) @@ -83,13 +83,13 @@ func checkFileSize(maxSizeBytes int64) { 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}() + defer func() { IsDBLocked = false }() amountOfBytesToTrim := currentFileSize / (100 / percentageOfMaxSizeBytesToPrune) rows, err := GetEntriesTable().Limit(10000).Order("id").Rows() if err != nil { - fmt.Printf("Error getting 10000 first db rows: %v\n", err) + rlog.Errorf("Error getting 10000 first db rows: %v", err) return } @@ -102,7 +102,7 @@ func pruneOldEntries(currentFileSize int64) { var entry models.MizuEntry err = DB.ScanRows(rows, &entry) if err != nil { - fmt.Printf("Error scanning db row: %v\n", err) + rlog.Errorf("Error scanning db row: %v", err) continue } @@ -114,8 +114,8 @@ func pruneOldEntries(currentFileSize int64) { GetEntriesTable().Where(entryIdsToRemove).Delete(models.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") - fmt.Printf("Removed %d rows and cleared %s\n", len(entryIdsToRemove), units.BytesToHumanReadable(bytesToBeRemoved)) + rlog.Errorf("Removed %d rows and cleared %s", len(entryIdsToRemove), units.BytesToHumanReadable(bytesToBeRemoved)) } else { - fmt.Println("Found no rows to remove when pruning") + rlog.Error("Found no rows to remove when pruning") } } diff --git a/api/pkg/holder/main.go b/agent/pkg/holder/main.go similarity index 100% rename from api/pkg/holder/main.go rename to agent/pkg/holder/main.go diff --git a/api/pkg/models/models.go b/agent/pkg/models/models.go similarity index 61% rename from api/pkg/models/models.go rename to agent/pkg/models/models.go index c12119db9..0648d4f5c 100644 --- a/api/pkg/models/models.go +++ b/agent/pkg/models/models.go @@ -2,11 +2,14 @@ package models import ( "encoding/json" + + "mizuserver/pkg/rules" + "mizuserver/pkg/utils" + "time" + "github.com/google/martian/har" "github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/tap" - "mizuserver/pkg/utils" - "time" ) type DataUnmarshaler interface { @@ -33,19 +36,33 @@ type MizuEntry struct { ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"` ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"` IsOutgoing bool `json:"isOutgoing,omitempty" gorm:"column:isOutgoing"` - EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"` + EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"` } 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"` - IsOutgoing bool `json:"isOutgoing,omitempty"` + 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"` + IsOutgoing bool `json:"isOutgoing,omitempty"` + Latency int64 `json:"latency,omitempty"` + Rules ApplicableRules `json:"rules,omitempty"` +} + +type ApplicableRules struct { + Latency int64 `json:"latency,omitempty"` + Status bool `json:"status,omitempty"` +} + +func NewApplicableRules(status bool, latency int64) ApplicableRules { + ar := ApplicableRules{} + ar.Status = status + ar.Latency = latency + return ar } type FullEntryDetails struct { @@ -101,25 +118,20 @@ func (fedex *FullEntryDetailsExtra) UnmarshalData(entry *MizuEntry) error { return nil } -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"` + 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 UploadEntriesRequestBody struct { - Dest string `query:"dest"` - SleepIntervalSec int `query:"interval"` +type UploadEntriesRequestQuery struct { + Dest string `form:"dest"` + SleepIntervalSec int `form:"interval"` } -type HarFetchRequestBody struct { - From int64 `query:"from"` - To int64 `query:"to"` +type HarFetchRequestQuery struct { + From int64 `form:"from"` + To int64 `form:"to"` } type WebSocketEntryMessage struct { @@ -132,6 +144,11 @@ type WebSocketTappedEntryMessage struct { Data *tap.OutputChannelItem } +type WebsocketOutboundLinkMessage struct { + *shared.WebSocketMessageMetadata + Data *tap.OutboundLink +} + func CreateBaseEntryWebSocketMessage(base *BaseEntryDetails) ([]byte, error) { message := &WebSocketEntryMessage{ WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{ @@ -152,6 +169,16 @@ func CreateWebsocketTappedEntryMessage(base *tap.OutputChannelItem) ([]byte, err 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"` @@ -171,3 +198,27 @@ type ExtendedCreator struct { *har.Creator Source *string `json:"_source"` } + +type FullEntryWithPolicy struct { + RulesMatched []rules.RulesMatched `json:"rulesMatched,omitempty"` + Entry har.Entry `json:"entry"` + Service string `json:"service"` +} + +func (fewp *FullEntryWithPolicy) UnmarshalData(entry *MizuEntry) error { + if err := json.Unmarshal([]byte(entry.Entry), &fewp.Entry); err != nil { + return err + } + + _, resultPolicyToSend := rules.MatchRequestPolicy(fewp.Entry, entry.Service) + fewp.RulesMatched = resultPolicyToSend + fewp.Service = entry.Service + return nil +} + +func RunValidationRulesState(harEntry har.Entry, service string) ApplicableRules { + numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service) + statusPolicyToSend, latency := rules.PassedValidationRules(resultPolicyToSend, numberOfRules) + ar := NewApplicableRules(statusPolicyToSend, latency) + return ar +} diff --git a/agent/pkg/providers/status_provider.go b/agent/pkg/providers/status_provider.go new file mode 100644 index 000000000..4a6b14468 --- /dev/null +++ b/agent/pkg/providers/status_provider.go @@ -0,0 +1,28 @@ +package providers + +import ( + "github.com/patrickmn/go-cache" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/tap" + "time" +) + +const tlsLinkRetainmentTime = time.Minute * 15 + +var ( + TapStatus shared.TapStatus + RecentTLSLinks = cache.New(tlsLinkRetainmentTime, tlsLinkRetainmentTime) +) + +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 +} diff --git a/api/pkg/resolver/README.md b/agent/pkg/resolver/README.md similarity index 89% rename from api/pkg/resolver/README.md rename to agent/pkg/resolver/README.md index 0ce3b933c..747787d78 100644 --- a/api/pkg/resolver/README.md +++ b/agent/pkg/resolver/README.md @@ -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) k8sResolver, err := resolver.NewFromOutOfCluster("", errOut) if err != nil { - fmt.Printf("error creating k8s resolver %s", err) + rlog.Errorf("error creating k8s resolver %s", err) } 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 if resolvedName != nil { - fmt.Printf("resolved 10.107.251.91=%s", *resolvedName) + rlog.Errorf("resolved 10.107.251.91=%s", *resolvedName) } else { - fmt.Printf("Could not find a resolved name for 10.107.251.91") + rlog.Error("Could not find a resolved name for 10.107.251.91") } for { select { case err := <- errOut: - fmt.Printf("name resolving error %s", err) + rlog.Errorf("name resolving error %s", err) } } ``` diff --git a/api/pkg/resolver/go.sum b/agent/pkg/resolver/go.sum similarity index 100% rename from api/pkg/resolver/go.sum rename to agent/pkg/resolver/go.sum diff --git a/agent/pkg/resolver/loader.go b/agent/pkg/resolver/loader.go new file mode 100644 index 000000000..d3b1d353d --- /dev/null +++ b/agent/pkg/resolver/loader.go @@ -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 +} diff --git a/api/pkg/resolver/resolver.go b/agent/pkg/resolver/resolver.go similarity index 58% rename from api/pkg/resolver/resolver.go rename to agent/pkg/resolver/resolver.go index e2b09e4c3..d68896acb 100644 --- a/api/pkg/resolver/resolver.go +++ b/agent/pkg/resolver/resolver.go @@ -7,28 +7,32 @@ import ( "github.com/romana/rlog" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "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 map[string]string - serviceMap map[string]string - isStarted bool - errOut chan error + 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) @@ -36,97 +40,97 @@ func (resolver *Resolver) Start(ctx context.Context) { } func (resolver *Resolver) Resolve(name string) string { - resolvedName, isFound := resolver.nameMap[name] + resolvedName, isFound := resolver.nameMap.Get(name) if !isFound { return "" } - return resolvedName + return resolvedName.(string) } -func (resolver *Resolver) GetMap() map[string]string { +func (resolver *Resolver) GetMap() cmap.ConcurrentMap { return resolver.nameMap } func (resolver *Resolver) CheckIsServiceIP(address string) bool { - _, isFound := resolver.serviceMap[address] + _, 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("").Watch(ctx, metav1.ListOptions{Watch: true}) + 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 + 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}) + 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) - } + 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) - } - } - } - } + 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 + } + 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}) + 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(): + case event := <-watcher.ResultChan(): if event.Object == nil { return errors.New("error in kubectl service watch") } @@ -142,7 +146,7 @@ func (resolver *Resolver) watchServices(ctx context.Context) error { resolver.saveResolvedName(ingress.IP, serviceHostname, event.Type) } } - case <- ctx.Done(): + case <-ctx.Done(): watcher.Stop() return nil } @@ -151,19 +155,19 @@ func (resolver *Resolver) watchServices(ctx context.Context) error { func (resolver *Resolver) saveResolvedName(key string, resolved string, eventType watch.EventType) { if eventType == watch.Deleted { - delete(resolver.nameMap, key) + resolver.nameMap.Remove(key) rlog.Infof("setting %s=nil\n", key) } else { - resolver.nameMap[key] = resolved + resolver.nameMap.Set(key, resolved) rlog.Infof("setting %s=%s\n", key, resolved) } } func (resolver *Resolver) saveServiceIP(key string, resolved string, eventType watch.EventType) { if eventType == watch.Deleted { - delete(resolver.serviceMap, key) + resolver.serviceMap.Remove(key) } else { - resolver.serviceMap[key] = resolved + resolver.serviceMap.Set(key, resolved) } } @@ -186,4 +190,3 @@ func (resolver *Resolver) infiniteErrorHandleRetryFunc(ctx context.Context, fun } } } - diff --git a/agent/pkg/routes/entries_routes.go b/agent/pkg/routes/entries_routes.go new file mode 100644 index 000000000..af88b5bd4 --- /dev/null +++ b/agent/pkg/routes/entries_routes.go @@ -0,0 +1,26 @@ +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("/api") + + routeGroup.GET("/entries", controllers.GetEntries) // get entries (base/thin entries) + routeGroup.GET("/entries/:entryId", controllers.GetEntry) // get single (full) entry + routeGroup.GET("/exportEntries", controllers.GetFullEntries) + routeGroup.GET("/uploadEntries", controllers.UploadEntries) + routeGroup.GET("/resolving", controllers.GetCurrentResolvingInformation) + + 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 + routeGroup.GET("/analyzeStatus", controllers.AnalyzeInformation) + routeGroup.GET("/recentTLSLinks", controllers.GetRecentTLSLinks) +} diff --git a/agent/pkg/routes/metadata_routes.go b/agent/pkg/routes/metadata_routes.go new file mode 100644 index 000000000..4f32ff771 --- /dev/null +++ b/agent/pkg/routes/metadata_routes.go @@ -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) +} diff --git a/agent/pkg/routes/not_found_route.go b/agent/pkg/routes/not_found_route.go new file mode 100644 index 000000000..bde062ff0 --- /dev/null +++ b/agent/pkg/routes/not_found_route.go @@ -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", + }) + }, + ) +} diff --git a/agent/pkg/routes/status_routes.go b/agent/pkg/routes/status_routes.go new file mode 100644 index 000000000..a6aa41151 --- /dev/null +++ b/agent/pkg/routes/status_routes.go @@ -0,0 +1,12 @@ +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) +} diff --git a/agent/pkg/rules/models.go b/agent/pkg/rules/models.go new file mode 100644 index 000000000..8b1f1f617 --- /dev/null +++ b/agent/pkg/rules/models.go @@ -0,0 +1,110 @@ +package rules + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "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) (int, []RulesMatched) { + enforcePolicy, _ := shared.DecodeEnforcePolicy(fmt.Sprintf("%s/%s", shared.RulePolicyPath, shared.RulePolicyFileName)) + var resultPolicyToSend []RulesMatched + for _, rule := range enforcePolicy.Rules { + if !ValidatePath(rule.Path, harEntry.Request.URL) || !ValidateService(rule.Service, service) { + continue + } + if rule.Type == "json" { + var bodyJsonMap interface{} + if err := json.Unmarshal(harEntry.Response.Content.Text, &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 + } + } 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 len(enforcePolicy.Rules), resultPolicyToSend +} + +func PassedValidationRules(rulesMatched []RulesMatched, numberOfRules int) (bool, int64) { + if len(rulesMatched) == 0 { + return false, 0 + } + for _, rule := range rulesMatched { + if rule.Matched == false { + return false, -1 + } + } + for _, rule := range rulesMatched { + if strings.ToLower(rule.Rule.Type) == "latency" { + return true, rule.Rule.Latency + } + } + return true, -1 +} diff --git a/api/pkg/sensitiveDataFiltering/consts.go b/agent/pkg/sensitiveDataFiltering/consts.go similarity index 100% rename from api/pkg/sensitiveDataFiltering/consts.go rename to agent/pkg/sensitiveDataFiltering/consts.go diff --git a/api/pkg/sensitiveDataFiltering/messageSensitiveDataCleaner.go b/agent/pkg/sensitiveDataFiltering/messageSensitiveDataCleaner.go similarity index 100% rename from api/pkg/sensitiveDataFiltering/messageSensitiveDataCleaner.go rename to agent/pkg/sensitiveDataFiltering/messageSensitiveDataCleaner.go diff --git a/api/pkg/up9/main.go b/agent/pkg/up9/main.go similarity index 100% rename from api/pkg/up9/main.go rename to agent/pkg/up9/main.go diff --git a/api/pkg/utils/pathUtils.go b/agent/pkg/utils/pathUtils.go similarity index 100% rename from api/pkg/utils/pathUtils.go rename to agent/pkg/utils/pathUtils.go diff --git a/api/pkg/utils/randomString.go b/agent/pkg/utils/randomString.go similarity index 100% rename from api/pkg/utils/randomString.go rename to agent/pkg/utils/randomString.go diff --git a/api/pkg/utils/truncating_logger.go b/agent/pkg/utils/truncating_logger.go similarity index 91% rename from api/pkg/utils/truncating_logger.go rename to agent/pkg/utils/truncating_logger.go index f3a0c72a6..39aa834d3 100644 --- a/api/pkg/utils/truncating_logger.go +++ b/agent/pkg/utils/truncating_logger.go @@ -3,6 +3,7 @@ package utils import ( "context" "fmt" + "github.com/romana/rlog" "gorm.io/gorm/logger" "gorm.io/gorm/utils" "time" @@ -10,7 +11,7 @@ import ( // 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 + LogLevel logger.LogLevel SlowThreshold time.Duration } @@ -23,21 +24,21 @@ func (truncatingLogger *TruncatingLogger) Info(_ context.Context, message string if truncatingLogger.LogLevel < logger.Info { return } - fmt.Printf("gorm info: %.150s\n", message) + rlog.Errorf("gorm info: %.150s", message) } func (truncatingLogger *TruncatingLogger) Warn(_ context.Context, message string, __ ...interface{}) { if truncatingLogger.LogLevel < logger.Warn { return } - fmt.Printf("gorm warning: %.150s\n", message) + rlog.Errorf("gorm warning: %.150s", message) } func (truncatingLogger *TruncatingLogger) Error(_ context.Context, message string, __ ...interface{}) { if truncatingLogger.LogLevel < logger.Error { return } - fmt.Printf("gorm error: %.150s\n", message) + rlog.Errorf("gorm error: %.150s", message) } func (truncatingLogger *TruncatingLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { diff --git a/api/pkg/utils/utils.go b/agent/pkg/utils/utils.go similarity index 79% rename from api/pkg/utils/utils.go rename to agent/pkg/utils/utils.go index 6d66b9674..cdb5a35cb 100644 --- a/api/pkg/utils/utils.go +++ b/agent/pkg/utils/utils.go @@ -1,32 +1,42 @@ package utils import ( - "github.com/gofiber/fiber/v2" + "context" + "github.com/gin-gonic/gin" "github.com/romana/rlog" "log" + "net/http" "net/url" "os" "os/signal" "reflect" "syscall" + "time" ) // StartServer starts the server with a graceful shutdown -func StartServer(app *fiber.App) { +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 rlog.Infof("Shutting down...") - _ = app.Shutdown() + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + _ = srv.Shutdown(ctx) + os.Exit(0) }() // Run server. - if err := app.Listen(":8899"); err != nil { + if err := app.Run(":8899"); err != nil { log.Printf("Oops... Server is not running! Reason: %v", err) } } diff --git a/api/pkg/utils/zip.go b/agent/pkg/utils/zip.go similarity index 100% rename from api/pkg/utils/zip.go rename to agent/pkg/utils/zip.go diff --git a/api/pkg/validation/validation.go b/agent/pkg/validation/validation.go similarity index 100% rename from api/pkg/validation/validation.go rename to agent/pkg/validation/validation.go diff --git a/api/pkg/version/consts.go b/agent/pkg/version/consts.go similarity index 100% rename from api/pkg/version/consts.go rename to agent/pkg/version/consts.go diff --git a/api/start.sh b/agent/start.sh similarity index 100% rename from api/start.sh rename to agent/start.sh diff --git a/api/pkg/api/socket_server_handlers.go b/api/pkg/api/socket_server_handlers.go deleted file mode 100644 index c705bb9e3..000000000 --- a/api/pkg/api/socket_server_handlers.go +++ /dev/null @@ -1,99 +0,0 @@ -package api - -import ( - "encoding/json" - "github.com/antoniodipinto/ikisocket" - "github.com/romana/rlog" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/tap" - "mizuserver/pkg/controllers" - "mizuserver/pkg/models" - "mizuserver/pkg/routes" - "mizuserver/pkg/up9" -) - -var browserClientSocketUUIDs = make([]string, 0) - -type RoutesEventHandlers struct { - routes.EventHandlers - SocketHarOutChannel chan<- *tap.OutputChannelItem -} - -func init() { - go up9.UpdateAnalyzeStatus(broadcastToBrowserClients) -} - -func (h *RoutesEventHandlers) WebSocketConnect(ep *ikisocket.EventPayload) { - if ep.Kws.GetAttribute("is_tapper") == true { - rlog.Infof("Websocket Connection event - Tapper connected: %s", ep.SocketUUID) - } else { - rlog.Infof("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 { - rlog.Infof("Disconnection event - Tapper connected: %s", ep.SocketUUID) - } else { - rlog.Infof("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 { - rlog.Infof("Websocket Close event - Tapper connected: %s", ep.SocketUUID) - } else { - rlog.Infof("Websocket Close event - Browser socket connected: %s", ep.SocketUUID) - removeSocketUUIDFromBrowserSlice(ep.SocketUUID) - } -} - -func (h *RoutesEventHandlers) WebSocketError(ep *ikisocket.EventPayload) { - rlog.Infof("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 { - rlog.Infof("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 { - rlog.Infof("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 { - rlog.Infof("Could not unmarshal message of message type %s %v\n", socketMessageBase.MessageType, err) - } else { - controllers.TapStatus = statusMessage.TappingStatus - broadcastToBrowserClients(ep.Data) - } - default: - rlog.Infof("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 -} diff --git a/api/pkg/controllers/resolving_controller.go b/api/pkg/controllers/resolving_controller.go deleted file mode 100644 index cdd3df4f5..000000000 --- a/api/pkg/controllers/resolving_controller.go +++ /dev/null @@ -1,11 +0,0 @@ -package controllers - -import ( - "github.com/gofiber/fiber/v2" - "mizuserver/pkg/holder" -) - -func GetCurrentResolvingInformation(c *fiber.Ctx) error { - return c.Status(fiber.StatusOK).JSON(holder.GetResolver().GetMap()) -} - diff --git a/api/pkg/controllers/status_controller.go b/api/pkg/controllers/status_controller.go deleted file mode 100644 index e4fed4c55..000000000 --- a/api/pkg/controllers/status_controller.go +++ /dev/null @@ -1,17 +0,0 @@ -package controllers - -import ( - "github.com/gofiber/fiber/v2" - "github.com/up9inc/mizu/shared" - "mizuserver/pkg/up9" -) - -var TapStatus shared.TapStatus - -func GetTappingStatus(c *fiber.Ctx) error { - return c.Status(fiber.StatusOK).JSON(TapStatus) -} - -func AnalyzeInformation(c *fiber.Ctx) error { - return c.Status(fiber.StatusOK).JSON(up9.GetAnalyzeInfo()) -} diff --git a/api/pkg/middleware/fiber_middleware.go b/api/pkg/middleware/fiber_middleware.go deleted file mode 100644 index 24bc453a7..000000000 --- a/api/pkg/middleware/fiber_middleware.go +++ /dev/null @@ -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(), - ) -} diff --git a/api/pkg/resolver/loader.go b/api/pkg/resolver/loader.go deleted file mode 100644 index b9cd6e1fc..000000000 --- a/api/pkg/resolver/loader.go +++ /dev/null @@ -1,67 +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" - "os" -) - -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), serviceMap: make(map[string]string), errOut: errOut}, nil -} - -func NewFromOutOfCluster(kubeConfigPath string, errOut chan error) (*Resolver, error) { - if kubeConfigPath == "" { - env := os.Getenv("KUBECONFIG") - if env != "" { - kubeConfigPath = env - } else { - 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), serviceMap: 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), serviceMap: make(map[string]string), errOut: errOut} -} diff --git a/api/pkg/routes/entries_routes.go b/api/pkg/routes/entries_routes.go deleted file mode 100644 index 9f5f751be..000000000 --- a/api/pkg/routes/entries_routes.go +++ /dev/null @@ -1,25 +0,0 @@ -package routes - -import ( - "github.com/gofiber/fiber/v2" - "mizuserver/pkg/controllers" -) - -// EntriesRoutes defines the group of har entries 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("/exportEntries", controllers.GetFullEntries) - routeGroup.Get("/uploadEntries", controllers.UploadEntries) - routeGroup.Get("/resolving", controllers.GetCurrentResolvingInformation) - - 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 - routeGroup.Get("/analyzeStatus", controllers.AnalyzeInformation) -} diff --git a/api/pkg/routes/metadata_routes.go b/api/pkg/routes/metadata_routes.go deleted file mode 100644 index 3a3d3da11..000000000 --- a/api/pkg/routes/metadata_routes.go +++ /dev/null @@ -1,13 +0,0 @@ -package routes - -import ( - "github.com/gofiber/fiber/v2" - "mizuserver/pkg/controllers" -) - -// MetadataRoutes defines the group of metadata routes. -func MetadataRoutes(fiberApp *fiber.App) { - routeGroup := fiberApp.Group("/metadata") - - routeGroup.Get("/version", controllers.GetVersion) -} diff --git a/api/pkg/routes/not_found_route.go b/api/pkg/routes/not_found_route.go deleted file mode 100644 index 452caa8e0..000000000 --- a/api/pkg/routes/not_found_route.go +++ /dev/null @@ -1,15 +0,0 @@ -package routes - -import "github.com/gofiber/fiber/v2" - -// NotFoundRoute defines the 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", - }) - }, - ) -} diff --git a/api/pkg/routes/socket_routes.go b/api/pkg/routes/socket_routes.go deleted file mode 100644 index a66cc401b..000000000 --- a/api/pkg/routes/socket_routes.go +++ /dev/null @@ -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 -} diff --git a/assets/mizu-example.png b/assets/mizu-example.png new file mode 100644 index 000000000..329da58ae Binary files /dev/null and b/assets/mizu-example.png differ diff --git a/assets/mizu-logo.svg b/assets/mizu-logo.svg new file mode 100644 index 000000000..cbf980097 --- /dev/null +++ b/assets/mizu-logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/mizu-ui.png b/assets/mizu-ui.png new file mode 100644 index 000000000..29a1d17ba Binary files /dev/null and b/assets/mizu-ui.png differ diff --git a/build-push-featurebranch.sh b/build-push-featurebranch.sh index 5fbf32619..0441c3960 100755 --- a/build-push-featurebranch.sh +++ b/build-push-featurebranch.sh @@ -5,7 +5,9 @@ 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 +SEM_VER=${SEM_VER=0.0.0} +DOCKER_REPO=$REPOSITORY/$SERVER_NAME/$GIT_BRANCH +DOCKER_TAGGED_BUILDS=("$DOCKER_REPO:latest" "$DOCKER_REPO:$SEM_VER") if [ "$GIT_BRANCH" = 'develop' -o "$GIT_BRANCH" = 'master' -o "$GIT_BRANCH" = 'main' ] then @@ -13,8 +15,12 @@ then exit 1 fi -echo "building $DOCKER_TAGGED_BUILD" -docker build -t "$DOCKER_TAGGED_BUILD" --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} . +echo "building ${DOCKER_TAGGED_BUILDS[@]}" +DOCKER_TAGS_ARGS=$(echo ${DOCKER_TAGGED_BUILDS[@]/#/-t }) # "-t FIRST_TAG -t SECOND_TAG ..." +docker build $DOCKER_TAGS_ARGS --build-arg SEM_VER=${SEM_VER} --build-arg BUILD_TIMESTAMP=${BUILD_TIMESTAMP} --build-arg GIT_BRANCH=${GIT_BRANCH} --build-arg COMMIT_HASH=${COMMIT_HASH} . -echo pushing to "$REPOSITORY" -docker push "$DOCKER_TAGGED_BUILD" +for DOCKER_TAG in "${DOCKER_TAGGED_BUILDS[@]}" +do + echo pushing "$DOCKER_TAG" + docker push "$DOCKER_TAG" +done diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 000000000..ba077a403 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +bin diff --git a/cli/Makefile b/cli/Makefile index b228e9fd1..b4841ba0c 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -3,6 +3,7 @@ COMMIT_HASH=$(shell git rev-parse HEAD) GIT_BRANCH=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]') GIT_VERSION=$(shell git branch --show-current | tr '[:upper:]' '[:lower:]') BUILD_TIMESTAMP=$(shell date +%s) +export SEM_VER?=0.0.0 .PHONY: help .DEFAULT_GOAL := help @@ -13,7 +14,7 @@ help: ## This help. install: 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)' \ -X 'github.com/up9inc/mizu/cli/mizu.Branch=$(GIT_BRANCH)' \ -X 'github.com/up9inc/mizu/cli/mizu.BuildTimestamp=$(BUILD_TIMESTAMP)' \ @@ -21,7 +22,7 @@ build: ## build mizu CLI binary (select platform via GOOS / GOARCH env variables -o bin/mizu_$(SUFFIX) mizu.go (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" @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 @$(MAKE) build GOOS=darwin GOARCH=amd64 @@ -35,6 +36,6 @@ build-all: ## build for all supported platforms @echo "---------" @find ./bin -ls -clean: ## clean all build artifacts +clean: ## Clean all build artifacts. go clean rm -rf ./bin/* diff --git a/cli/README.md b/cli/README.md deleted file mode 100644 index 5dd208131..000000000 --- a/cli/README.md +++ /dev/null @@ -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 diff --git a/cli/cmd/config.go b/cli/cmd/config.go new file mode 100644 index 000000000..66d4485d8 --- /dev/null +++ b/cli/cmd/config.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/uiUtils" + "io/ioutil" +) + +var regenerateFile bool + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Generate config with default values", + RunE: func(cmd *cobra.Command, args []string) error { + template, err := mizu.GetConfigWithDefaults() + if err != nil { + mizu.Log.Errorf("Failed generating config with defaults %v", err) + return nil + } + if regenerateFile { + data := []byte(template) + if err := ioutil.WriteFile(mizu.GetConfigFilePath(), data, 0644); err != nil { + mizu.Log.Errorf("Failed writing config %v", err) + return nil + } + mizu.Log.Infof(fmt.Sprintf("Template File written to %s", fmt.Sprintf(uiUtils.Purple, mizu.GetConfigFilePath()))) + } else { + mizu.Log.Debugf("Writing template config.\n%v", template) + fmt.Printf("%v", template) + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(configCmd) + configCmd.Flags().BoolVarP(®enerateFile, "regenerate", "r", false, fmt.Sprintf("Regenerate the config file with default values %s", mizu.GetConfigFilePath())) +} diff --git a/cli/cmd/fetch.go b/cli/cmd/fetch.go index 62caaf4e6..3b9b4cc09 100644 --- a/cli/cmd/fetch.go +++ b/cli/cmd/fetch.go @@ -1,29 +1,23 @@ package cmd import ( + "github.com/creasty/defaults" "github.com/spf13/cobra" "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/mizu/configStructs" ) -type MizuFetchOptions struct { - FromTimestamp int64 - ToTimestamp int64 - Directory string - MizuPort uint16 -} - -var mizuFetchOptions = MizuFetchOptions{} - var fetchCmd = &cobra.Command{ Use: "fetch", Short: "Download recorded traffic to files", RunE: func(cmd *cobra.Command, args []string) error { - if isCompatible, err := mizu.CheckVersionCompatibility(mizuFetchOptions.MizuPort); err != nil { + go mizu.ReportRun("fetch", mizu.Config.Fetch) + if isCompatible, err := mizu.CheckVersionCompatibility(mizu.Config.Fetch.GuiPort); err != nil { return err } else if !isCompatible { return nil } - RunMizuFetch(&mizuFetchOptions) + RunMizuFetch() return nil }, } @@ -31,8 +25,11 @@ var fetchCmd = &cobra.Command{ func init() { rootCmd.AddCommand(fetchCmd) - fetchCmd.Flags().StringVarP(&mizuFetchOptions.Directory, "directory", "d", ".", "Provide a custom directory for fetched entries") - fetchCmd.Flags().Int64Var(&mizuFetchOptions.FromTimestamp, "from", 0, "Custom start timestamp for fetched entries") - fetchCmd.Flags().Int64Var(&mizuFetchOptions.ToTimestamp, "to", 0, "Custom end timestamp fetched entries") - fetchCmd.Flags().Uint16VarP(&mizuFetchOptions.MizuPort, "port", "p", 8899, "Custom port for mizu") + defaultFetchConfig := configStructs.FetchConfig{} + defaults.Set(&defaultFetchConfig) + + fetchCmd.Flags().StringP(configStructs.DirectoryFetchName, "d", defaultFetchConfig.Directory, "Provide a custom directory for fetched entries") + fetchCmd.Flags().Int(configStructs.FromTimestampFetchName, defaultFetchConfig.FromTimestamp, "Custom start timestamp for fetched entries") + fetchCmd.Flags().Int(configStructs.ToTimestampFetchName, defaultFetchConfig.ToTimestamp, "Custom end timestamp fetched entries") + fetchCmd.Flags().Uint16P(configStructs.GuiPortFetchName, "p", defaultFetchConfig.GuiPort, "Provide a custom port for the web interface webserver") } diff --git a/cli/cmd/fetchRunner.go b/cli/cmd/fetchRunner.go index 36e6182f9..9c372a632 100644 --- a/cli/cmd/fetchRunner.go +++ b/cli/cmd/fetchRunner.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" "github.com/up9inc/mizu/cli/kubernetes" + "github.com/up9inc/mizu/cli/mizu" "io" "io/ioutil" "log" @@ -14,9 +15,9 @@ import ( "strings" ) -func RunMizuFetch(fetch *MizuFetchOptions) { - mizuProxiedUrl := kubernetes.GetMizuCollectorProxiedHostAndPath(fetch.MizuPort) - resp, err := http.Get(fmt.Sprintf("http://%s/api/har?from=%v&to=%v", mizuProxiedUrl, fetch.FromTimestamp, fetch.ToTimestamp)) +func RunMizuFetch() { + mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Fetch.GuiPort) + resp, err := http.Get(fmt.Sprintf("http://%s/api/har?from=%v&to=%v", mizuProxiedUrl, mizu.Config.Fetch.FromTimestamp, mizu.Config.Fetch.ToTimestamp)) if err != nil { log.Fatal(err) } @@ -32,8 +33,8 @@ func RunMizuFetch(fetch *MizuFetchOptions) { if err != nil { log.Fatal(err) } - _ = Unzip(zipReader, fetch.Directory) + _ = Unzip(zipReader, mizu.Config.Fetch.Directory) } func Unzip(reader *zip.Reader, dest string) error { @@ -63,7 +64,7 @@ func Unzip(reader *zip.Reader, dest string) error { _ = os.MkdirAll(path, f.Mode()) } else { _ = os.MkdirAll(filepath.Dir(path), f.Mode()) - fmt.Print("writing HAR file [ ", path, " ] .. ") + mizu.Log.Infof("writing HAR file [ %v ]", path) f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err @@ -72,7 +73,7 @@ func Unzip(reader *zip.Reader, dest string) error { if err := f.Close(); err != nil { panic(err) } - fmt.Println(" done") + mizu.Log.Info(" done") }() _, err = io.Copy(f, rc) diff --git a/cli/cmd/logs.go b/cli/cmd/logs.go new file mode 100644 index 000000000..ec639fc7e --- /dev/null +++ b/cli/cmd/logs.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "context" + "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/fsUtils" + "github.com/up9inc/mizu/cli/kubernetes" + "github.com/up9inc/mizu/cli/mizu" + "os" + "path" +) + +var filePath string + +var logsCmd = &cobra.Command{ + Use: "logs", + Short: "Create a zip file with logs for Github issue or troubleshoot", + RunE: func(cmd *cobra.Command, args []string) error { + kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath) + if err != nil { + return nil + } + ctx, _ := context.WithCancel(context.Background()) + + if filePath == "" { + pwd, err := os.Getwd() + if err != nil { + mizu.Log.Errorf("Failed to get PWD, %v (try using `mizu logs -f )`", err) + return nil + } + filePath = path.Join(pwd, "mizu_logs.zip") + } + mizu.Log.Debugf("Using file path %s", filePath) + + if err := fsUtils.DumpLogs(kubernetesProvider, ctx, filePath); err != nil { + mizu.Log.Errorf("Failed dump logs %v", err) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(logsCmd) + logsCmd.Flags().StringVarP(&filePath, "file", "f", "", "Path for zip file (default current \\mizu_logs.zip)") +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 239d76a62..84f6dde3b 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -1,7 +1,10 @@ package cmd import ( + "fmt" "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/fsUtils" + "github.com/up9inc/mizu/cli/mizu" ) var rootCmd = &cobra.Command{ @@ -9,6 +12,21 @@ var rootCmd = &cobra.Command{ Short: "A web traffic viewer for kubernetes", Long: `A web traffic viewer for kubernetes Further info is available at https://github.com/up9inc/mizu`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := fsUtils.EnsureDir(mizu.GetMizuFolderPath()); err != nil { + mizu.Log.Errorf("Failed to use mizu folder, %v", err) + } + mizu.InitLogger() + if err := mizu.InitConfig(cmd); err != nil { + mizu.Log.Fatal(err) + } + + return nil + }, +} + +func init() { + rootCmd.PersistentFlags().StringSlice(mizu.SetCommandName, []string{}, fmt.Sprintf("Override values using --%s", mizu.SetCommandName)) } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cli/cmd/tap.go b/cli/cmd/tap.go index 245b3d7df..72e71f3fb 100644 --- a/cli/cmd/tap.go +++ b/cli/cmd/tap.go @@ -2,41 +2,17 @@ package cmd import ( "errors" - "fmt" - "github.com/spf13/cobra" - "github.com/up9inc/mizu/cli/mizu" - "github.com/up9inc/mizu/cli/uiUtils" - "github.com/up9inc/mizu/shared/units" "os" - "regexp" - "strings" + + "github.com/creasty/defaults" + "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/errormessage" + "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/mizu/configStructs" + "github.com/up9inc/mizu/cli/uiUtils" ) -type MizuTapOptions struct { - GuiPort uint16 - Namespace string - AllNamespaces bool - Analysis bool - AnalysisDestination string - KubeConfigPath string - MizuImage string - PlainTextFilterRegexes []string - TapOutgoing bool - HideHealthChecks bool - MaxEntriesDBSizeBytes int64 - SleepIntervalSec uint16 -} - -var mizuTapOptions = &MizuTapOptions{} -var direction string -var humanMaxEntriesDBSize string -var regex *regexp.Regexp -const maxEntriesDBSizeFlagName = "max-entries-db-size" - - -const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic -to UP9 cloud for further analysis and enriched presentation options. -` +const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic for further analysis and enriched presentation options.` var tapCmd = &cobra.Command{ Use: "tap [POD REGEX]", @@ -44,45 +20,35 @@ var tapCmd = &cobra.Command{ Long: `Record the ingoing traffic of a kubernetes pod. Supported protocols are HTTP and gRPC.`, RunE: func(cmd *cobra.Command, args []string) error { - RunMizuTap(regex, mizuTapOptions) + go mizu.ReportRun("tap", mizu.Config.Tap) + RunMizuTap() return nil }, PreRunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("POD REGEX argument is required") + if len(args) == 1 { + mizu.Config.Tap.PodRegexStr = args[0] } else if len(args) > 1 { return errors.New("unexpected number of arguments") } - var compileErr error - regex, compileErr = regexp.Compile(args[0]) - if compileErr != nil { - return errors.New(fmt.Sprintf("%s is not a valid regex %s", args[0], compileErr)) + if err := mizu.Config.Validate(); err != nil { + return errormessage.FormatError(err) } - var parseHumanDataSizeErr error - mizuTapOptions.MaxEntriesDBSizeBytes, parseHumanDataSizeErr = units.HumanReadableToBytes(humanMaxEntriesDBSize) - if parseHumanDataSizeErr != nil { - return errors.New(fmt.Sprintf("Could not parse --max-entries-db-size value %s", humanMaxEntriesDBSize)) - } - fmt.Printf("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.\n", units.BytesToHumanReadable(mizuTapOptions.MaxEntriesDBSizeBytes)) - - directionLowerCase := strings.ToLower(direction) - if directionLowerCase == "any" { - mizuTapOptions.TapOutgoing = true - } else if directionLowerCase == "in" { - mizuTapOptions.TapOutgoing = false - } else { - return errors.New(fmt.Sprintf("%s is not a valid value for flag --direction. Acceptable values are in/any.", direction)) + if err := mizu.Config.Tap.Validate(); err != nil { + return errormessage.FormatError(err) } - if mizuTapOptions.Analysis { - fmt.Printf(analysisMessageToConfirm) - if !uiUtils.AskForConfirmation("Would you like to proceed [y/n]: ") { - fmt.Println("You can always run mizu without analysis, aborting") + mizu.Log.Infof("Mizu will store up to %s of traffic, old traffic will be cleared once the limit is reached.", mizu.Config.Tap.HumanMaxEntriesDBSize) + + if mizu.Config.Tap.Analysis { + mizu.Log.Infof(analysisMessageToConfirm) + if !uiUtils.AskForConfirmation("Would you like to proceed [Y/n]: ") { + mizu.Log.Infof("You can always run mizu without analysis, aborting") os.Exit(0) } } + return nil }, } @@ -90,16 +56,18 @@ Supported protocols are HTTP and gRPC.`, func init() { rootCmd.AddCommand(tapCmd) - tapCmd.Flags().Uint16VarP(&mizuTapOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver") - tapCmd.Flags().StringVarP(&mizuTapOptions.Namespace, "namespace", "n", "", "Namespace selector") - tapCmd.Flags().BoolVar(&mizuTapOptions.Analysis, "analysis", false, "Uploads traffic to UP9 for further analysis (Beta)") - tapCmd.Flags().StringVar(&mizuTapOptions.AnalysisDestination, "dest", "up9.app", "Destination environment") - tapCmd.Flags().Uint16VarP(&mizuTapOptions.SleepIntervalSec, "upload-interval", "", 10, "Interval in seconds for uploading data to UP9") - 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().StringVarP(&mizuTapOptions.MizuImage, "mizu-image", "", fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", mizu.Branch, mizu.SemVer), "Custom image for mizu collector") - 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().StringVarP(&direction, "direction", "", "in", "Record traffic that goes in this direction (relative to the tapped pod): in/any") - tapCmd.Flags().BoolVar(&mizuTapOptions.HideHealthChecks, "hide-healthchecks", false, "hides requests with kube-probe or prometheus user-agent headers") - tapCmd.Flags().StringVarP(&humanMaxEntriesDBSize, maxEntriesDBSizeFlagName, "", "200MB", "override the default max entries db size of 200mb") + defaultTapConfig := configStructs.TapConfig{} + defaults.Set(&defaultTapConfig) + + tapCmd.Flags().Uint16P(configStructs.GuiPortTapName, "p", defaultTapConfig.GuiPort, "Provide a custom port for the web interface webserver") + tapCmd.Flags().StringArrayP(configStructs.NamespacesTapName, "n", defaultTapConfig.Namespaces, "Namespaces selector") + tapCmd.Flags().Bool(configStructs.AnalysisTapName, defaultTapConfig.Analysis, "Uploads traffic to UP9 for further analysis (Beta)") + tapCmd.Flags().BoolP(configStructs.AllNamespacesTapName, "A", defaultTapConfig.AllNamespaces, "Tap all namespaces") + tapCmd.Flags().StringArrayP(configStructs.PlainTextFilterRegexesTapName, "r", defaultTapConfig.PlainTextFilterRegexes, "List of regex expressions that are used to filter matching values from text/plain http bodies") + tapCmd.Flags().Bool(configStructs.HideHealthChecksTapName, defaultTapConfig.HideHealthChecks, "Hides requests with kube-probe or prometheus user-agent headers") + tapCmd.Flags().Bool(configStructs.DisableRedactionTapName, defaultTapConfig.DisableRedaction, "Disables redaction of potentially sensitive request/response headers and body values") + tapCmd.Flags().String(configStructs.HumanMaxEntriesDBSizeTapName, defaultTapConfig.HumanMaxEntriesDBSize, "Override the default max entries db size") + tapCmd.Flags().String(configStructs.DirectionTapName, defaultTapConfig.Direction, "Record traffic that goes in this direction (relative to the tapped pod): in/any") + tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them") + tapCmd.Flags().String(configStructs.EnforcePolicyFile, defaultTapConfig.EnforcePolicyFile, "Yaml file with policy rules") } diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index de2691397..401b7f77d 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -1,153 +1,240 @@ package cmd import ( + "bytes" "context" + "encoding/json" "fmt" - "github.com/romana/rlog" - "github.com/up9inc/mizu/cli/kubernetes" - "github.com/up9inc/mizu/cli/mizu" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/shared/debounce" - core "k8s.io/api/core/v1" - "log" + "github.com/up9inc/mizu/cli/fsUtils" + "github.com/up9inc/mizu/cli/goUtils" + "github.com/up9inc/mizu/cli/mizu/configStructs" "net/http" "net/url" "os" "os/signal" + "path" "regexp" + "strings" "syscall" "time" + + "github.com/up9inc/mizu/cli/errormessage" + "github.com/up9inc/mizu/cli/kubernetes" + "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/uiUtils" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/shared/debounce" + yaml "gopkg.in/yaml.v3" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/wait" ) -var mizuServiceAccountExists bool -var aggregatorService *core.Service - const ( + cleanupTimeout = time.Minute updateTappersDelay = 5 * time.Second ) -var currentlyTappedPods []core.Pod +type tapState struct { + apiServerService *core.Service + currentlyTappedPods []core.Pod + mizuServiceAccountExists bool + doNotRemoveConfigMap bool +} -func RunMizuTap(podRegexQuery *regexp.Regexp, tappingOptions *MizuTapOptions) { - mizuApiFilteringOptions, err := getMizuApiFilteringOptions(tappingOptions) +var state tapState + +func RunMizuTap() { + mizuApiFilteringOptions, err := getMizuApiFilteringOptions() if err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error parsing regex-masking: %v", errormessage.FormatError(err))) + return + } + var mizuValidationRules string + if mizu.Config.Tap.EnforcePolicyFile != "" { + mizuValidationRules, err = readValidationRules(mizu.Config.Tap.EnforcePolicyFile) + if err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error reading policy file: %v", errormessage.FormatError(err))) + return + } + } + + kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.KubeConfigPath) + if err != nil { + mizu.Log.Error(err) return } - kubernetesProvider := kubernetes.NewProvider(tappingOptions.KubeConfigPath) - - defer cleanUpMizuResources(kubernetesProvider) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel will be called when this function exits - targetNamespace := getNamespace(tappingOptions, kubernetesProvider) - if matchingPods, err := kubernetesProvider.GetAllPodsMatchingRegex(ctx, podRegexQuery, targetNamespace); err != nil { - return - } else { - currentlyTappedPods = matchingPods - } + targetNamespaces := getNamespaces(kubernetesProvider) var namespacesStr string - if targetNamespace != mizu.K8sAllNamespaces { - namespacesStr = fmt.Sprintf("namespace \"%s\"", targetNamespace) + if targetNamespaces[0] != mizu.K8sAllNamespaces { + namespacesStr = fmt.Sprintf("namespaces \"%s\"", strings.Join(targetNamespaces, "\", \"")) } else { namespacesStr = "all namespaces" } - fmt.Printf("Tapping pods in %s\n", namespacesStr) + mizu.CheckNewerVersion() + mizu.Log.Infof("Tapping pods in %s", namespacesStr) - if len(currentlyTappedPods) == 0 { + if err, _ := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error getting pods by regex: %v", errormessage.FormatError(err))) + return + } + + if len(state.currentlyTappedPods) == 0 { var suggestionStr string - if targetNamespace != mizu.K8sAllNamespaces { - suggestionStr = "\nSelect a different namespace with -n or tap all namespaces with -A" + if targetNamespaces[0] != mizu.K8sAllNamespaces { + suggestionStr = ". Select a different namespace with -n or tap all namespaces with -A" } - fmt.Printf("Did not find any pods matching the regex argument%s\n", suggestionStr) + mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Did not find any pods matching the regex argument%s", suggestionStr)) } - nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods) - if err != nil { + if mizu.Config.Tap.DryRun { return } - if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions, mizuApiFilteringOptions); err != nil { + nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods) + + defer cleanUpMizuResources(kubernetesProvider) + if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions, mizuValidationRules); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error creating resources: %v", errormessage.FormatError(err))) return } - go portForwardApiPod(ctx, kubernetesProvider, cancel, tappingOptions) // TODO convert this to job for built in pod ttl or have the running app handle this - go watchPodsForTapping(ctx, kubernetesProvider, cancel, podRegexQuery, tappingOptions) - go syncApiStatus(ctx, cancel, tappingOptions) + go goUtils.HandleExcWrapper(createProxyToApiServerPod, ctx, kubernetesProvider, cancel) + go goUtils.HandleExcWrapper(watchPodsForTapping, ctx, kubernetesProvider, targetNamespaces, cancel) //block until exit signal or error waitForFinish(ctx, cancel) } -func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, tappingOptions *MizuTapOptions, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error { - if err := createMizuAggregator(ctx, kubernetesProvider, tappingOptions, mizuApiFilteringOptions); err != nil { +func readValidationRules(file string) (string, error) { + 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, nodeToTappedPodIPMap map[string][]string, mizuApiFilteringOptions *shared.TrafficFilteringOptions, mizuValidationRules string) error { + if !mizu.Config.IsNsRestrictedMode() { + if err := createMizuNamespace(ctx, kubernetesProvider); err != nil { + return err + } + } + + if err := createMizuApiServer(ctx, kubernetesProvider, mizuApiFilteringOptions); err != nil { return err } - if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions); err != nil { + if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap); err != nil { return err } + if err := createMizuConfigmap(ctx, kubernetesProvider, mizuValidationRules); err != nil { + mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to create resources required for policy validation. Mizu will not validate policy rules. error: %v\n", errormessage.FormatError(err))) + state.doNotRemoveConfigMap = true + } else if mizuValidationRules == "" { + state.doNotRemoveConfigMap = true + } + 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) error { + err := kubernetesProvider.CreateConfigMap(ctx, mizu.Config.MizuResourcesNamespace, mizu.ConfigMapName, data) + return err +} + +func createMizuNamespace(ctx context.Context, kubernetesProvider *kubernetes.Provider) error { + _, err := kubernetesProvider.CreateNamespace(ctx, mizu.Config.MizuResourcesNamespace) + return err +} + +func createMizuApiServer(ctx context.Context, kubernetesProvider *kubernetes.Provider, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error { var err error - mizuServiceAccountExists = createRBACIfNecessary(ctx, kubernetesProvider) - _, err = kubernetesProvider.CreateMizuAggregatorPod(ctx, mizu.ResourcesNamespace, mizu.AggregatorPodName, tappingOptions.MizuImage, mizuServiceAccountExists, mizuApiFilteringOptions, tappingOptions.MaxEntriesDBSizeBytes) + state.mizuServiceAccountExists, err = createRBACIfNecessary(ctx, kubernetesProvider) if err != nil { - fmt.Printf("Error creating mizu collector pod: %v\n", err) - return err + mizu.Log.Warningf(uiUtils.Warning, fmt.Sprintf("Failed to ensure the resources required for IP resolving. Mizu will not resolve target IPs to names. error: %v", errormessage.FormatError(err))) } - 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: mizu.Config.MizuResourcesNamespace, + PodName: mizu.ApiServerPodName, + PodImage: mizu.Config.AgentImage, + ServiceAccountName: serviceAccountName, + IsNamespaceRestricted: mizu.Config.IsNsRestrictedMode(), + MizuApiFilteringOptions: mizuApiFilteringOptions, + MaxEntriesDBSizeBytes: mizu.Config.Tap.MaxEntriesDBSizeBytes(), + } + _, err = kubernetesProvider.CreateMizuApiServerPod(ctx, opts) if err != nil { - fmt.Printf("Error creating mizu collector service: %v\n", err) return err } + mizu.Log.Debugf("Successfully created API server pod: %s", mizu.ApiServerPodName) + + state.apiServerService, err = kubernetesProvider.CreateService(ctx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName, mizu.ApiServerPodName) + if err != nil { + return err + } + mizu.Log.Debugf("Successfully created service: %s", mizu.ApiServerPodName) return nil } -func getMizuApiFilteringOptions(tappingOptions *MizuTapOptions) (*shared.TrafficFilteringOptions, error) { +func getMizuApiFilteringOptions() (*shared.TrafficFilteringOptions, error) { var compiledRegexSlice []*shared.SerializableRegexp - if tappingOptions.PlainTextFilterRegexes != nil && len(tappingOptions.PlainTextFilterRegexes) > 0 { + if mizu.Config.Tap.PlainTextFilterRegexes != nil && len(mizu.Config.Tap.PlainTextFilterRegexes) > 0 { compiledRegexSlice = make([]*shared.SerializableRegexp, 0) - for _, regexStr := range tappingOptions.PlainTextFilterRegexes { + for _, regexStr := range mizu.Config.Tap.PlainTextFilterRegexes { compiledRegex, err := shared.CompileRegexToSerializableRegexp(regexStr) if err != nil { - fmt.Printf("Regex %s is invalid: %v", regexStr, err) return nil, err } compiledRegexSlice = append(compiledRegexSlice, compiledRegex) } } - return &shared.TrafficFilteringOptions{PlainTextMaskingRegexes: compiledRegexSlice, HideHealthChecks: tappingOptions.HideHealthChecks}, nil + return &shared.TrafficFilteringOptions{PlainTextMaskingRegexes: compiledRegexSlice, HideHealthChecks: mizu.Config.Tap.HideHealthChecks, DisableRedaction: mizu.Config.Tap.DisableRedaction}, nil } -func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, tappingOptions *MizuTapOptions) error { +func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string) error { if len(nodeToTappedPodIPMap) > 0 { + var serviceAccountName string + if state.mizuServiceAccountExists { + serviceAccountName = mizu.ServiceAccountName + } else { + serviceAccountName = "" + } + if err := kubernetesProvider.ApplyMizuTapperDaemonSet( ctx, - mizu.ResourcesNamespace, + mizu.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName, - tappingOptions.MizuImage, + mizu.Config.AgentImage, mizu.TapperPodName, - fmt.Sprintf("%s.%s.svc.cluster.local", aggregatorService.Name, aggregatorService.Namespace), + fmt.Sprintf("%s.%s.svc.cluster.local", state.apiServerService.Name, state.apiServerService.Namespace), nodeToTappedPodIPMap, - mizuServiceAccountExists, - tappingOptions.TapOutgoing, + serviceAccountName, + mizu.Config.Tap.TapOutgoing(), ); err != nil { - fmt.Printf("Error creating mizu tapper daemonset: %v\n", err) return err } + mizu.Log.Debugf("Successfully created %v tappers", len(nodeToTappedPodIPMap)) } else { - if err := kubernetesProvider.RemoveDaemonSet(ctx, mizu.ResourcesNamespace, mizu.TapperDaemonSetName); err != nil { - fmt.Printf("Error deleting mizu tapper daemonset: %v\n", err) + if err := kubernetesProvider.RemoveDaemonSet(ctx, mizu.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil { return err } } @@ -156,41 +243,138 @@ func updateMizuTappers(ctx context.Context, kubernetesProvider *kubernetes.Provi } func cleanUpMizuResources(kubernetesProvider *kubernetes.Provider) { - fmt.Printf("\nRemoving mizu resources\n") - removalCtx, _ := context.WithTimeout(context.Background(), 5*time.Second) - if err := kubernetesProvider.RemovePod(removalCtx, mizu.ResourcesNamespace, mizu.AggregatorPodName); err != nil { - fmt.Printf("Error removing Pod %s in namespace %s: %s (%v,%+v)\n", mizu.AggregatorPodName, mizu.ResourcesNamespace, err, err, err) + removalCtx, cancel := context.WithTimeout(context.Background(), cleanupTimeout) + defer cancel() + + if mizu.Config.DumpLogs { + mizuDir := mizu.GetMizuFolderPath() + filePath = path.Join(mizuDir, fmt.Sprintf("mizu_logs_%s.zip", time.Now().Format("2006_01_02__15_04_05"))) + if err := fsUtils.DumpLogs(kubernetesProvider, removalCtx, filePath); err != nil { + mizu.Log.Errorf("Failed dump logs %v", err) + } } - if err := kubernetesProvider.RemoveService(removalCtx, mizu.ResourcesNamespace, mizu.AggregatorPodName); err != nil { - fmt.Printf("Error removing Service %s in namespace %s: %s (%v,%+v)\n", mizu.AggregatorPodName, mizu.ResourcesNamespace, err, err, err) + + mizu.Log.Infof("\nRemoving mizu resources\n") + + if !mizu.Config.IsNsRestrictedMode() { + if err := kubernetesProvider.RemoveNamespace(removalCtx, mizu.Config.MizuResourcesNamespace); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Namespace %s: %v", mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + return + } + } else { + if err := kubernetesProvider.RemovePod(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Pod %s in namespace %s: %v", mizu.ApiServerPodName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + + if err := kubernetesProvider.RemoveService(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service %s in namespace %s: %v", mizu.ApiServerPodName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + + if err := kubernetesProvider.RemoveDaemonSet(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.TapperDaemonSetName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing DaemonSet %s in namespace %s: %v", mizu.TapperDaemonSetName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + + if !state.doNotRemoveConfigMap { + if err := kubernetesProvider.RemoveConfigMap(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ConfigMapName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing ConfigMap %s in namespace %s: %v", mizu.ConfigMapName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + } + } - if err := kubernetesProvider.RemoveDaemonSet(removalCtx, mizu.ResourcesNamespace, mizu.TapperDaemonSetName); err != nil { - fmt.Printf("Error removing DaemonSet %s in namespace %s: %s (%v,%+v)\n", mizu.TapperDaemonSetName, mizu.ResourcesNamespace, err, err, err) + + if state.mizuServiceAccountExists { + if !mizu.Config.IsNsRestrictedMode() { + if err := kubernetesProvider.RemoveNonNamespacedResources(removalCtx, mizu.ClusterRoleName, mizu.ClusterRoleBindingName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing non-namespaced resources: %v", errormessage.FormatError(err))) + return + } + } else { + if err := kubernetesProvider.RemoveServicAccount(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Service Account %s in namespace %s: %v", mizu.ServiceAccountName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + return + } + + if err := kubernetesProvider.RemoveRole(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.RoleName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing Role %s in namespace %s: %v", mizu.RoleName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + + if err := kubernetesProvider.RemoveRoleBinding(removalCtx, mizu.Config.MizuResourcesNamespace, mizu.RoleBindingName); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error removing RoleBinding %s in namespace %s: %v", mizu.RoleBindingName, mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + } + } + + if !mizu.Config.IsNsRestrictedMode() { + waitUntilNamespaceDeleted(removalCtx, cancel, kubernetesProvider) } } -func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, podRegex *regexp.Regexp, tappingOptions *MizuTapOptions) { - targetNamespace := getNamespace(tappingOptions, kubernetesProvider) +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) + }() - added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, targetNamespace), podRegex) + if err := kubernetesProvider.WaitUtilNamespaceDeleted(ctx, mizu.Config.MizuResourcesNamespace); err != nil { + switch { + case ctx.Err() == context.Canceled: + // Do nothing. User interrupted the wait. + case err == wait.ErrWaitTimeout: + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Timeout while removing Namespace %s", mizu.Config.MizuResourcesNamespace)) + default: + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error while waiting for Namespace %s to be deleted: %v", mizu.Config.MizuResourcesNamespace, errormessage.FormatError(err))) + } + } +} + +func reportTappedPods() { + mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort) + tappedPodsUrl := fmt.Sprintf("http://%s/status/tappedPods", mizuProxiedUrl) + + podInfos := make([]shared.PodInfo, 0) + for _, pod := range state.currentlyTappedPods { + podInfos = append(podInfos, shared.PodInfo{Name: pod.Name, Namespace: pod.Namespace}) + } + tapStatus := shared.TapStatus{Pods: podInfos} + + if jsonValue, err := json.Marshal(tapStatus); err != nil { + mizu.Log.Debugf("[ERROR] failed Marshal the tapped pods %v", err) + } else { + if response, err := http.Post(tappedPodsUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil { + mizu.Log.Debugf("[ERROR] failed sending to API server the tapped pods %v", err) + } else if response.StatusCode != 200 { + mizu.Log.Debugf("[ERROR] failed sending to API server the tapped pods, response status code %v", response.StatusCode) + } else { + mizu.Log.Debugf("Reported to server API about %d taped pods successfully", len(podInfos)) + } + } +} + +func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Provider, targetNamespaces []string, cancel context.CancelFunc) { + added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, targetNamespaces, mizu.Config.Tap.PodRegex()) restartTappers := func() { - if matchingPods, err := kubernetesProvider.GetAllPodsMatchingRegex(ctx, podRegex, targetNamespace); err != nil { - fmt.Printf("Error getting pods by regex: %s (%v,%+v)\n", err, err, err) - cancel() - } else { - currentlyTappedPods = matchingPods - } - - nodeToTappedPodIPMap, err := getNodeHostToTappedPodIpsMap(currentlyTappedPods) + err, changeFound := updateCurrentlyTappedPods(kubernetesProvider, ctx, targetNamespaces) if err != nil { - fmt.Printf("Error building node to ips map: %s (%v,%+v)\n", err, err, err) + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Failed to update currently tapped pods: %v", err)) cancel() } - if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap, tappingOptions); err != nil { - fmt.Printf("Error updating daemonset: %s (%v,%+v)\n", err, err, err) + if !changeFound { + mizu.Log.Debugf("Nothing changed update tappers not needed") + return + } + + reportTappedPods() + + nodeToTappedPodIPMap := getNodeHostToTappedPodIpsMap(state.currentlyTappedPods) + if err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error building node to ips map: %v", errormessage.FormatError(err))) + cancel() + } + if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap); err != nil { + mizu.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error updating daemonset: %v", errormessage.FormatError(err))) cancel() } } @@ -198,109 +382,187 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro for { select { - case newTarget := <-added: - fmt.Printf(mizu.Green, fmt.Sprintf("+%s\n", newTarget.Name)) - - case removedTarget := <-removed: - fmt.Printf(mizu.Red, fmt.Sprintf("-%s\n", removedTarget.Name)) + case pod := <-added: + mizu.Log.Debugf("Added matching pod %s, ns: %s", pod.Name, pod.Namespace) restartTappersDebouncer.SetOn() - - case modifiedTarget := <-modified: + case pod := <-removed: + mizu.Log.Debugf("Removed matching pod %s, ns: %s", pod.Name, pod.Namespace) + restartTappersDebouncer.SetOn() + case pod := <-modified: + mizu.Log.Debugf("Modified matching pod %s, ns: %s, phase: %s, ip: %s", pod.Name, pod.Namespace, pod.Status.Phase, pod.Status.PodIP) // Act only if the modified pod has already obtained an IP address. // After filtering for IPs, on a normal pod restart this includes the following events: // - Pod deletion // - Pod reaches start state // - Pod reaches ready state // Ready/unready transitions might also trigger this event. - if modifiedTarget.Status.PodIP != "" { + if pod.Status.PodIP != "" { restartTappersDebouncer.SetOn() } - case <-errorChan: + case err := <-errorChan: + mizu.Log.Debugf("Watching pods loop, got error %v, stopping `restart tappers debouncer`", err) + restartTappersDebouncer.Cancel() // TODO: Does this also perform cleanup? cancel() case <-ctx.Done(): + mizu.Log.Debugf("Watching pods loop, context done, stopping `restart tappers debouncer`") + restartTappersDebouncer.Cancel() return } } } -func portForwardApiPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc, tappingOptions *MizuTapOptions) { - podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.AggregatorPodName)) - added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider.GetPodWatcher(ctx, mizu.ResourcesNamespace), podExactRegex) +func updateCurrentlyTappedPods(kubernetesProvider *kubernetes.Provider, ctx context.Context, targetNamespaces []string) (error, bool) { + changeFound := false + if matchingPods, err := kubernetesProvider.ListAllRunningPodsMatchingRegex(ctx, mizu.Config.Tap.PodRegex(), targetNamespaces); err != nil { + return err, false + } else { + podsToTap := excludeMizuPods(matchingPods) + addedPods, removedPods := getPodArrayDiff(state.currentlyTappedPods, podsToTap) + for _, addedPod := range addedPods { + changeFound = true + mizu.Log.Infof(uiUtils.Green, fmt.Sprintf("+%s", addedPod.Name)) + } + for _, removedPod := range removedPods { + changeFound = true + mizu.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 createProxyToApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) { + podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", mizu.ApiServerPodName)) + added, modified, removed, errorChan := kubernetes.FilteredWatch(ctx, kubernetesProvider, []string{mizu.Config.MizuResourcesNamespace}, podExactRegex) isPodReady := false + timeAfter := time.After(25 * time.Second) for { select { + case <-ctx.Done(): + mizu.Log.Debugf("Watching API Server pod loop, ctx done") + return case <-added: + mizu.Log.Debugf("Watching API Server pod loop, added") continue case <-removed: - fmt.Printf("%s removed\n", mizu.AggregatorPodName) + mizu.Log.Infof("%s removed", mizu.ApiServerPodName) cancel() return case modifiedPod := <-modified: - if modifiedPod.Status.Phase == "Running" && !isPodReady { - isPodReady = true - go func() { - err := kubernetes.StartProxy(kubernetesProvider, tappingOptions.GuiPort, mizu.ResourcesNamespace, mizu.AggregatorPodName) - if err != nil { - fmt.Printf("Error occured while running k8s proxy %v\n", err) - cancel() - } - }() - mizuProxiedUrl := kubernetes.GetMizuCollectorProxiedHostAndPath(tappingOptions.GuiPort) - fmt.Printf("Mizu is available at http://%s\n", mizuProxiedUrl) - - time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready - if tappingOptions.Analysis { - urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(tappingOptions.AnalysisDestination), tappingOptions.SleepIntervalSec) - u, err := url.ParseRequestURI(urlPath) - - if err != nil { - log.Fatal(fmt.Sprintf("Failed parsing the URL %v\n", err)) - } - rlog.Debugf("Sending get request to %v\n", u.String()) - if response, err := http.Get(u.String()); err != nil || response.StatusCode != 200 { - fmt.Printf("error sending upload entries req, status code: %v, err: %v\n", response.StatusCode, err) - } else { - fmt.Printf(mizu.Purple, "Traffic is uploading to UP9 for further analsys") - fmt.Println() - } - } + if modifiedPod == nil { + mizu.Log.Debugf("Watching API Server pod loop, modifiedPod with nil") + continue } - - case <-time.After(25 * time.Second): + mizu.Log.Debugf("Watching API Server pod loop, modified: %v", modifiedPod.Status.Phase) + if modifiedPod.Status.Phase == core.PodRunning && !isPodReady { + isPodReady = true + go startProxyReportErrorIfAny(kubernetesProvider, cancel) + mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort)) + time.Sleep(time.Second * 5) // Waiting to be sure the proxy is ready + requestForAnalysis() + reportTappedPods() + } + case <-timeAfter: if !isPodReady { - fmt.Printf("error: %s pod was not ready in time", mizu.AggregatorPodName) + mizu.Log.Errorf(uiUtils.Error, "Mizu API server was not ready in time") cancel() } - case <-errorChan: + mizu.Log.Debugf("[ERROR] Agent creation, watching %v namespace", mizu.Config.MizuResourcesNamespace) cancel() - - case <-ctx.Done(): - return } } } -func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) bool { - mizuRBACExists, err := kubernetesProvider.DoesMizuRBACExist(ctx, mizu.ResourcesNamespace) +func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) { + err := kubernetes.StartProxy(kubernetesProvider, mizu.Config.Tap.GuiPort, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName) if err != nil { - fmt.Printf("warning: could not ensure mizu rbac resources exist %v\n", err) - return false + mizu.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() } - if !mizuRBACExists { - err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.ResourcesNamespace, mizu.RBACVersion) - if err != nil { - fmt.Printf("warning: could not create mizu rbac resources %v\n", err) - return false - } - } - return true } -func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) (map[string][]string, error) { +func requestForAnalysis() { + if !mizu.Config.Tap.Analysis { + return + } + + mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.Tap.GuiPort) + urlPath := fmt.Sprintf("http://%s/api/uploadEntries?dest=%s&interval=%v", mizuProxiedUrl, url.QueryEscape(mizu.Config.Tap.AnalysisDestination), mizu.Config.Tap.SleepIntervalSec) + u, parseErr := url.ParseRequestURI(urlPath) + if parseErr != nil { + mizu.Log.Fatal("Failed parsing the URL (consider changing the analysis dest URL), err: %v", parseErr) + } + + mizu.Log.Debugf("Sending get request to %v", u.String()) + if response, requestErr := http.Get(u.String()); requestErr != nil { + mizu.Log.Errorf("Failed to notify agent for analysis, err: %v", requestErr) + } else if response.StatusCode != 200 { + mizu.Log.Errorf("Failed to notify agent for analysis, status code: %v", response.StatusCode) + } else { + mizu.Log.Infof(uiUtils.Purple, "Traffic is uploading to UP9 for further analysis") + } +} + +func createRBACIfNecessary(ctx context.Context, kubernetesProvider *kubernetes.Provider) (bool, error) { + if !mizu.Config.IsNsRestrictedMode() { + err := kubernetesProvider.CreateMizuRBAC(ctx, mizu.Config.MizuResourcesNamespace, mizu.ServiceAccountName, mizu.ClusterRoleName, mizu.ClusterRoleBindingName, mizu.RBACVersion) + if err != nil { + return false, err + } + } else { + err := kubernetesProvider.CreateMizuRBACNamespaceRestricted(ctx, mizu.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) for _, pod := range tappedPods { existingList := nodeToTappedPodIPMap[pod.Spec.NodeName] @@ -310,7 +572,7 @@ func getNodeHostToTappedPodIpsMap(tappedPods []core.Pod) (map[string][]string, e 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) { @@ -326,35 +588,12 @@ func waitForFinish(ctx context.Context, cancel context.CancelFunc) { } } -func syncApiStatus(ctx context.Context, cancel context.CancelFunc, tappingOptions *MizuTapOptions) { - controlSocketStr := fmt.Sprintf("ws://%s/ws", kubernetes.GetMizuCollectorProxiedHostAndPath(tappingOptions.GuiPort)) - controlSocket, err := mizu.CreateControlSocket(controlSocketStr) - 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 { - rlog.Debugf("error Sending message via control socket %v, error: %s\n", controlSocketStr, 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 +func getNamespaces(kubernetesProvider *kubernetes.Provider) []string { + if mizu.Config.Tap.AllNamespaces { + return []string{mizu.K8sAllNamespaces} + } else if len(mizu.Config.Tap.Namespaces) > 0 { + return mizu.Config.Tap.Namespaces } else { - return kubernetesProvider.CurrentNamespace() + return []string{kubernetesProvider.CurrentNamespace()} } } diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 366b05da2..45fc6dc9a 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -1,32 +1,27 @@ package cmd import ( - "fmt" - "github.com/up9inc/mizu/cli/mizu" "strconv" "time" + "github.com/creasty/defaults" "github.com/spf13/cobra" + "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/mizu/configStructs" ) -type MizuVersionOptions struct { - DebugInfo bool -} - - -var mizuVersionOptions = &MizuVersionOptions{} - var versionCmd = &cobra.Command{ Use: "version", Short: "Print version info", RunE: func(cmd *cobra.Command, args []string) error { - if mizuVersionOptions.DebugInfo { + go mizu.ReportRun("version", mizu.Config.Version) + if mizu.Config.Version.DebugInfo { timeStampInt, _ := strconv.ParseInt(mizu.BuildTimestamp, 10, 0) - fmt.Printf("Version: %s \nBranch: %s (%s) \n", mizu.SemVer, mizu.Branch, mizu.GitCommitHash) - fmt.Printf("Build Time: %s (%s)\n", mizu.BuildTimestamp, time.Unix(timeStampInt, 0)) + mizu.Log.Infof("Version: %s \nBranch: %s (%s)", mizu.SemVer, mizu.Branch, mizu.GitCommitHash) + mizu.Log.Infof("Build Time: %s (%s)", mizu.BuildTimestamp, time.Unix(timeStampInt, 0)) } else { - fmt.Printf("Version: %s (%s)\n", mizu.SemVer, mizu.Branch) + mizu.Log.Infof("Version: %s (%s)", mizu.SemVer, mizu.Branch) } return nil }, @@ -35,6 +30,9 @@ var versionCmd = &cobra.Command{ func init() { rootCmd.AddCommand(versionCmd) - versionCmd.Flags().BoolVarP(&mizuVersionOptions.DebugInfo, "debug", "d", false, "Provide all information about version") + defaultVersionConfig := configStructs.VersionConfig{} + defaults.Set(&defaultVersionConfig) + + versionCmd.Flags().BoolP(configStructs.DebugInfoVersionName, "d", defaultVersionConfig.DebugInfo, "Provide all information about version") } diff --git a/cli/cmd/view.go b/cli/cmd/view.go index a49ebde75..8f9742e2a 100644 --- a/cli/cmd/view.go +++ b/cli/cmd/view.go @@ -1,34 +1,28 @@ package cmd import ( + "github.com/creasty/defaults" "github.com/spf13/cobra" "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/mizu/configStructs" ) -type MizuViewOptions struct { - GuiPort uint16 -} - -var mizuViewOptions = &MizuViewOptions{} - var viewCmd = &cobra.Command{ Use: "view", Short: "Open GUI in browser", RunE: func(cmd *cobra.Command, args []string) error { - if isCompatible, err := mizu.CheckVersionCompatibility(mizuFetchOptions.MizuPort); err != nil { - return err - } else if !isCompatible { - return nil - } - runMizuView(mizuViewOptions) + go mizu.ReportRun("view", mizu.Config.View) + runMizuView() return nil }, - } func init() { rootCmd.AddCommand(viewCmd) - viewCmd.Flags().Uint16VarP(&mizuViewOptions.GuiPort, "gui-port", "p", 8899, "Provide a custom port for the web interface webserver") + defaultViewConfig := configStructs.ViewConfig{} + defaults.Set(&defaultViewConfig) + viewCmd.Flags().Uint16P(configStructs.GuiPortViewName, "p", defaultViewConfig.GuiPort, "Provide a custom port for the web interface webserver") + viewCmd.Flags().StringP(configStructs.KubeConfigPathViewName, "k", defaultViewConfig.KubeConfigPath, "Path to kube-config file") } diff --git a/cli/cmd/viewRunner.go b/cli/cmd/viewRunner.go index 289ba5234..4b2b11a0d 100644 --- a/cli/cmd/viewRunner.go +++ b/cli/cmd/viewRunner.go @@ -8,32 +8,47 @@ import ( "net/http" ) -func runMizuView(mizuViewOptions *MizuViewOptions) { - kubernetesProvider := kubernetes.NewProvider("") +func runMizuView() { + kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.View.KubeConfigPath) + if err != nil { + mizu.Log.Error(err) + return + } ctx, cancel := context.WithCancel(context.Background()) defer cancel() - exists, err := kubernetesProvider.DoesServicesExist(ctx, mizu.ResourcesNamespace, mizu.AggregatorPodName) + exists, err := kubernetesProvider.DoesServicesExist(ctx, mizu.Config.MizuResourcesNamespace, mizu.ApiServerPodName) if err != nil { - panic(err) + mizu.Log.Errorf("Failed to found mizu service %v", err) + cancel() + return } if !exists { - fmt.Printf("The %s service not found\n", mizu.AggregatorPodName) + mizu.Log.Infof("%s service not found, you should run `mizu tap` command first", mizu.ApiServerPodName) + cancel() return } - mizuProxiedUrl := kubernetes.GetMizuCollectorProxiedHostAndPath(mizuViewOptions.GuiPort) + mizuProxiedUrl := kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort) _, err = http.Get(fmt.Sprintf("http://%s/", mizuProxiedUrl)) if err == nil { - fmt.Printf("Found a running service %s and open port %d\n", mizu.AggregatorPodName, mizuViewOptions.GuiPort) + mizu.Log.Infof("Found a running service %s and open port %d", mizu.ApiServerPodName, mizu.Config.View.GuiPort) return } - fmt.Printf("Found service %s, creating k8s proxy\n", mizu.AggregatorPodName) + mizu.Log.Debugf("Found service %s, creating k8s proxy", mizu.ApiServerPodName) - fmt.Printf("Mizu is available at http://%s\n", kubernetes.GetMizuCollectorProxiedHostAndPath(mizuViewOptions.GuiPort)) - err = kubernetes.StartProxy(kubernetesProvider, mizuViewOptions.GuiPort, mizu.ResourcesNamespace, mizu.AggregatorPodName) - if err != nil { - fmt.Printf("Error occured while running k8s proxy %v\n", err) + go startProxyReportErrorIfAny(kubernetesProvider, cancel) + + mizu.Log.Infof("Mizu is available at http://%s\n", kubernetes.GetMizuApiServerProxiedHostAndPath(mizu.Config.View.GuiPort)) + if isCompatible, err := mizu.CheckVersionCompatibility(mizu.Config.View.GuiPort); err != nil { + mizu.Log.Errorf("Failed to check versions compatibility %v", err) + cancel() + return + } else if !isCompatible { + cancel() + return } + + waitForFinish(ctx, cancel) } diff --git a/cli/errormessage/errormessage.go b/cli/errormessage/errormessage.go new file mode 100644 index 000000000..1268f835d --- /dev/null +++ b/cli/errormessage/errormessage.go @@ -0,0 +1,38 @@ +package errormessage + +import ( + "errors" + "fmt" + + "github.com/up9inc/mizu/cli/mizu" + + regexpsyntax "regexp/syntax" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" +) + +// formatError wraps error with a detailed message that is meant for the user. +// While the errors are meant to be displayed, they are not meant to be exported as classes outsite of CLI. +func FormatError(err error) error { + var errorNew error + if k8serrors.IsForbidden(err) { + errorNew = fmt.Errorf("insufficient permissions: %w. "+ + "supply the required permission or control Mizu's access to namespaces by setting %s "+ + "in the config file or setting the tapped namespace with --%s %s=", + err, + mizu.MizuResourcesNamespaceConfigName, + mizu.SetCommandName, + mizu.MizuResourcesNamespaceConfigName) + } else if syntaxError, isSyntaxError := asRegexSyntaxError(err); isSyntaxError { + errorNew = fmt.Errorf("regex %s is invalid: %w", syntaxError.Expr, err) + } else { + errorNew = err + } + + return errorNew +} + +func asRegexSyntaxError(err error) (*regexpsyntax.Error, bool) { + var syntaxError *regexpsyntax.Error + return syntaxError, errors.As(err, &syntaxError) +} diff --git a/cli/fsUtils/dirUtils.go b/cli/fsUtils/dirUtils.go new file mode 100644 index 000000000..716e1472c --- /dev/null +++ b/cli/fsUtils/dirUtils.go @@ -0,0 +1,26 @@ +package fsUtils + +import ( + "errors" + "fmt" + "os" +) + +func EnsureDir(dirName string) error { + err := os.Mkdir(dirName, 0700) + if err == nil { + return nil + } + if os.IsExist(err) { + // check that the existing path is a directory + info, err := os.Stat(dirName) + if err != nil { + return err + } + if !info.IsDir() { + return errors.New(fmt.Sprintf("path exists but is not a directory: %s", dirName)) + } + return nil + } + return err +} diff --git a/cli/fsUtils/mizuLogsUtils.go b/cli/fsUtils/mizuLogsUtils.go new file mode 100644 index 000000000..b48cc2aa6 --- /dev/null +++ b/cli/fsUtils/mizuLogsUtils.go @@ -0,0 +1,58 @@ +package fsUtils + +import ( + "archive/zip" + "context" + "fmt" + "github.com/up9inc/mizu/cli/kubernetes" + "github.com/up9inc/mizu/cli/mizu" + "os" + "regexp" +) + +func DumpLogs(provider *kubernetes.Provider, ctx context.Context, filePath string) error { + podExactRegex := regexp.MustCompile("^" + mizu.MizuResourcesPrefix) + pods, err := provider.ListAllPodsMatchingRegex(ctx, podExactRegex, []string{mizu.Config.MizuResourcesNamespace}) + if err != nil { + return err + } + + if len(pods) == 0 { + return fmt.Errorf("no mizu pods found in namespace %s", mizu.Config.MizuResourcesNamespace) + } + + newZipFile, err := os.Create(filePath) + if err != nil { + return err + } + defer newZipFile.Close() + zipWriter := zip.NewWriter(newZipFile) + defer zipWriter.Close() + + for _, pod := range pods { + logs, err := provider.GetPodLogs(pod.Namespace, pod.Name, ctx) + if err != nil { + mizu.Log.Errorf("Failed to get logs, %v", err) + continue + } else { + mizu.Log.Debugf("Successfully read log length %d for pod: %s.%s", len(logs), pod.Namespace, pod.Name) + } + if err := AddStrToZip(zipWriter, logs, fmt.Sprintf("%s.%s.log", pod.Namespace, pod.Name)); err != nil { + mizu.Log.Errorf("Failed write logs, %v", err) + } else { + mizu.Log.Infof("Successfully added log length %d from pod: %s.%s", len(logs), pod.Namespace, pod.Name) + } + } + if err := AddFileToZip(zipWriter, mizu.GetConfigFilePath()); err != nil { + mizu.Log.Debugf("Failed write file, %v", err) + } else { + mizu.Log.Infof("Successfully added file %s", mizu.GetConfigFilePath()) + } + if err := AddFileToZip(zipWriter, mizu.GetLogFilePath()); err != nil { + mizu.Log.Debugf("Failed write file, %v", err) + } else { + mizu.Log.Infof("Successfully added file %s", mizu.GetLogFilePath()) + } + mizu.Log.Infof("You can find the zip with all logs in %s\n", filePath) + return nil +} diff --git a/cli/fsUtils/zipUtils.go b/cli/fsUtils/zipUtils.go new file mode 100644 index 000000000..be64e644c --- /dev/null +++ b/cli/fsUtils/zipUtils.go @@ -0,0 +1,55 @@ +package fsUtils + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" +) + +func AddFileToZip(zipWriter *zip.Writer, filename string) error { + + fileToZip, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to open file %s, %w", filename, err) + } + defer fileToZip.Close() + + // Get the file information + info, err := fileToZip.Stat() + if err != nil { + return fmt.Errorf("failed to get file information %s, %w", filename, err) + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + // Using FileInfoHeader() above only uses the basename of the file. If we want + // to preserve the folder structure we can overwrite this with the full path. + header.Name = filepath.Base(filename) + + // Change to deflate to gain better compression + // see http://golang.org/pkg/archive/zip/#pkg-constants + header.Method = zip.Deflate + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return fmt.Errorf("failed to create header in zip for %s, %w", filename, err) + } + _, err = io.Copy(writer, fileToZip) + return err +} + +func AddStrToZip(writer *zip.Writer, logs string, fileName string) error { + if zipFile, err := writer.Create(fileName); err != nil { + return fmt.Errorf("couldn't create a log file inside zip for %s, %w", fileName, err) + } else { + if _, err = zipFile.Write([]byte(logs)); err != nil { + return fmt.Errorf("couldn't write logs to zip file: %s, %w", fileName, err) + } + } + return nil +} diff --git a/cli/go.mod b/cli/go.mod index 02730fa71..9879f012e 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,10 +3,14 @@ module github.com/up9inc/mizu/cli go 1.16 require ( + github.com/creasty/defaults v1.5.1 + github.com/google/go-github/v37 v37.0.0 github.com/gorilla/websocket v1.4.2 - github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 + github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/spf13/cobra v1.1.3 + github.com/spf13/pflag v1.0.5 github.com/up9inc/mizu/shared v0.0.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.21.2 k8s.io/apimachinery v0.21.2 k8s.io/client-go v0.21.2 diff --git a/cli/go.sum b/cli/go.sum index 9a79ea6a5..ae8e268fd 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -82,6 +82,8 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM= +github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= @@ -99,6 +101,7 @@ github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT 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/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= @@ -174,6 +177,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -203,11 +207,17 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v37 v37.0.0 h1:rCspN8/6kB1BAJWZfuafvHhyfIo5fkAulaP/3bOQ/tM= +github.com/google/go-github/v37 v37.0.0/go.mod h1:LM7in3NmXDrX58GbEHy7FtNLbI2JijX93RnMKvWG3m4= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -218,6 +228,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -245,6 +256,7 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -324,6 +336,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/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/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/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -331,6 +345,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -355,8 +370,6 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 h1:jkvpcEatpwuMF5O5LVxTnehj6YZ/aEZN4NWD/Xml4pI= -github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7/go.mod h1:KTrHyWpO1sevuXPZwyeZc72ddWRFqNSKDFl7uVWKpg0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -398,6 +411,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -679,8 +694,9 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/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-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -705,6 +721,7 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/kubectl v0.21.2 h1:9XPCetvOMDqrIZZXb1Ei+g8t6KrIp9ENJaysQjUuLiE= k8s.io/kubectl v0.21.2/go.mod h1:PgeUclpG8VVmmQIl8zpLar3IQEpFc9mrmvlwY3CK1xo= diff --git a/cli/goUtils/funcWrappers.go b/cli/goUtils/funcWrappers.go new file mode 100644 index 000000000..ad71b684b --- /dev/null +++ b/cli/goUtils/funcWrappers.go @@ -0,0 +1,25 @@ +package goUtils + +import ( + "github.com/up9inc/mizu/cli/mizu" + "reflect" + "runtime/debug" +) + +func HandleExcWrapper(fn interface{}, params ...interface{}) (result []reflect.Value) { + defer func() { + if panicMessage := recover(); panicMessage != nil { + stack := debug.Stack() + mizu.Log.Fatalf("Unhandled panic: %v\n stack: %s", panicMessage, stack) + } + }() + f := reflect.ValueOf(fn) + if f.Type().NumIn() != len(params) { + panic("incorrect number of parameters!") + } + inputs := make([]reflect.Value, len(params)) + for k, in := range params { + inputs[k] = reflect.ValueOf(in) + } + return f.Call(inputs) +} diff --git a/cli/kubernetes/provider.go b/cli/kubernetes/provider.go index 9a9409eaf..fcbb78502 100644 --- a/cli/kubernetes/provider.go +++ b/cli/kubernetes/provider.go @@ -1,21 +1,26 @@ package kubernetes import ( + "bytes" _ "bytes" "context" "encoding/json" "errors" "fmt" + "os" "path/filepath" "regexp" "strconv" + "github.com/up9inc/mizu/cli/mizu" "github.com/up9inc/mizu/shared" + "io" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" resource "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/watch" applyconfapp "k8s.io/client-go/applyconfigurations/apps/v1" @@ -27,8 +32,10 @@ import ( _ "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/cache" "k8s.io/client-go/tools/clientcmd" _ "k8s.io/client-go/tools/portforward" + watchtools "k8s.io/client-go/tools/watch" "k8s.io/client-go/util/homedir" ) @@ -40,15 +47,19 @@ type Provider struct { } const ( - serviceAccountName = "mizu-service-account" - fieldManagerName = "mizu-manager" + fieldManagerName = "mizu-manager" ) -func NewProvider(kubeConfigPath string) *Provider { +func NewProvider(kubeConfigPath string) (*Provider, error) { kubernetesConfig := loadKubernetesConfiguration(kubeConfigPath) restClientConfig, err := kubernetesConfig.ClientConfig() if err != nil { - panic(err.Error()) + if clientcmd.IsEmptyConfig(err) { + return nil, fmt.Errorf("Couldn't find the kube config file, or file is empty. Try adding '--kube-config='\n") + } + if clientcmd.IsConfigurationInvalid(err) { + return nil, fmt.Errorf("Invalid kube config file. Try using a different config with '--kube-config='\n") + } } clientSet := getClientSet(restClientConfig) @@ -56,7 +67,7 @@ func NewProvider(kubeConfigPath string) *Provider { clientSet: clientSet, kubernetesConfig: kubernetesConfig, clientConfig: *restClientConfig, - } + }, nil } func (provider *Provider) CurrentNamespace() string { @@ -64,6 +75,46 @@ func (provider *Provider) CurrentNamespace() string { return ns } +func (provider *Provider) WaitUtilNamespaceDeleted(ctx context.Context, name string) error { + fieldSelector := fmt.Sprintf("metadata.name=%s", name) + var limit int64 = 1 + lw := &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + options.FieldSelector = fieldSelector + options.Limit = limit + return provider.clientSet.CoreV1().Namespaces().List(ctx, options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + options.FieldSelector = fieldSelector + options.Limit = limit + return provider.clientSet.CoreV1().Namespaces().Watch(ctx, options) + }, + } + + var preconditionFunc watchtools.PreconditionFunc = func(store cache.Store) (bool, error) { + _, exists, err := store.Get(&core.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}) + if err != nil { + return false, err + } + if exists { + return false, nil + } + return true, nil + } + + conditionFunc := func(e watch.Event) (bool, error) { + if e.Type == watch.Deleted { + return true, nil + } + return false, nil + } + + obj := &core.Namespace{} + _, err := watchtools.UntilWithSync(ctx, lw, obj, preconditionFunc, conditionFunc) + + return err +} + func (provider *Provider) GetPodWatcher(ctx context.Context, namespace string) watch.Interface { watcher, err := provider.clientSet.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{Watch: true}) if err != nil { @@ -72,42 +123,76 @@ func (provider *Provider) GetPodWatcher(ctx context.Context, namespace string) w return watcher } -func (provider *Provider) CreateMizuAggregatorPod(ctx context.Context, namespace string, podName string, podImage string, linkServiceAccount bool, mizuApiFilteringOptions *shared.TrafficFilteringOptions, maxEntriesDBSizeBytes int64) (*core.Pod, error) { - marshaledFilteringOptions, err := json.Marshal(mizuApiFilteringOptions) +func (provider *Provider) CreateNamespace(ctx context.Context, name string) (*core.Namespace, error) { + namespaceSpec := &core.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + return provider.clientSet.CoreV1().Namespaces().Create(ctx, namespaceSpec, metav1.CreateOptions{}) +} + +type ApiServerOptions struct { + Namespace string + PodName string + PodImage string + ServiceAccountName string + IsNamespaceRestricted bool + MizuApiFilteringOptions *shared.TrafficFilteringOptions + MaxEntriesDBSizeBytes int64 +} + +func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, opts *ApiServerOptions) (*core.Pod, error) { + marshaledFilteringOptions, err := json.Marshal(opts.MizuApiFilteringOptions) if err != nil { return nil, err } + configMapVolumeName := &core.ConfigMapVolumeSource{} + configMapVolumeName.Name = mizu.ConfigMapName + configMapOptional := true + configMapVolumeName.Optional = &configMapOptional cpuLimit, err := resource.ParseQuantity("750m") if err != nil { - return nil, errors.New("invalid cpu limit for aggregator container") + return nil, errors.New(fmt.Sprintf("invalid cpu limit for %s container", opts.PodName)) } memLimit, err := resource.ParseQuantity("512Mi") if err != nil { - return nil, errors.New("invalid memory limit for aggregator container") + return nil, errors.New(fmt.Sprintf("invalid memory limit for %s container", opts.PodName)) } cpuRequests, err := resource.ParseQuantity("50m") if err != nil { - return nil, errors.New("invalid cpu request for aggregator container") + return nil, errors.New(fmt.Sprintf("invalid cpu request for %s container", opts.PodName)) } memRequests, err := resource.ParseQuantity("50Mi") if err != nil { - return nil, errors.New("invalid memory request for aggregator container") + return nil, errors.New(fmt.Sprintf("invalid memory request for %s container", opts.PodName)) + } + + command := []string{"./mizuagent", "--api-server"} + if opts.IsNamespaceRestricted { + command = append(command, "--namespace", opts.Namespace) } pod := &core.Pod{ ObjectMeta: metav1.ObjectMeta{ - Name: podName, - Namespace: namespace, - Labels: map[string]string{"app": podName}, + Name: opts.PodName, + Namespace: opts.Namespace, + Labels: map[string]string{"app": opts.PodName}, }, Spec: core.PodSpec{ Containers: []core.Container{ { - Name: podName, - Image: podImage, + Name: opts.PodName, + Image: opts.PodImage, ImagePullPolicy: core.PullAlways, - Command: []string{"./mizuagent", "--aggregator"}, + VolumeMounts: []core.VolumeMount{ + { + Name: mizu.ConfigMapName, + MountPath: shared.RulePolicyPath, + }, + }, + Command: command, Env: []core.EnvVar{ { Name: shared.HostModeEnvVar, @@ -118,31 +203,39 @@ func (provider *Provider) CreateMizuAggregatorPod(ctx context.Context, namespace Value: string(marshaledFilteringOptions), }, { - Name: shared.MaxEntriesDBSizeByteSEnvVar, - Value: strconv.FormatInt(maxEntriesDBSizeBytes, 10), + Name: shared.MaxEntriesDBSizeBytesEnvVar, + Value: strconv.FormatInt(opts.MaxEntriesDBSizeBytes, 10), }, }, Resources: core.ResourceRequirements{ Limits: core.ResourceList{ - "cpu": cpuLimit, + "cpu": cpuLimit, "memory": memLimit, }, Requests: core.ResourceList{ - "cpu": cpuRequests, + "cpu": cpuRequests, "memory": memRequests, }, }, }, }, + Volumes: []core.Volume{ + { + Name: mizu.ConfigMapName, + VolumeSource: core.VolumeSource{ + ConfigMap: configMapVolumeName, + }, + }, + }, DNSPolicy: core.DNSClusterFirstWithHostNet, TerminationGracePeriodSeconds: new(int64), }, } //define the service account only when it exists to prevent pod crash - if linkServiceAccount { - pod.Spec.ServiceAccountName = serviceAccountName + if opts.ServiceAccountName != "" { + pod.Spec.ServiceAccountName = opts.ServiceAccountName } - return provider.clientSet.CoreV1().Pods(namespace).Create(ctx, pod, metav1.CreateOptions{}) + return provider.clientSet.CoreV1().Pods(opts.Namespace).Create(ctx, pod, metav1.CreateOptions{}) } func (provider *Provider) CreateService(ctx context.Context, namespace string, serviceName string, appLabelValue string) (*core.Service, error) { @@ -160,9 +253,57 @@ func (provider *Provider) CreateService(ctx context.Context, namespace string, s return provider.clientSet.CoreV1().Services(namespace).Create(ctx, &service, metav1.CreateOptions{}) } -func (provider *Provider) DoesMizuRBACExist(ctx context.Context, namespace string) (bool, error) { +func (provider *Provider) DoesServiceAccountExist(ctx context.Context, namespace string, serviceAccountName string) (bool, error) { serviceAccount, err := provider.clientSet.CoreV1().ServiceAccounts(namespace).Get(ctx, serviceAccountName, metav1.GetOptions{}) + return provider.doesResourceExist(serviceAccount, err) +} +func (provider *Provider) DoesConfigMapExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesServicesExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesNamespaceExist(ctx context.Context, name string) (bool, error) { + resource, err := provider.clientSet.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesClusterRoleExist(ctx context.Context, name string) (bool, error) { + resource, err := provider.clientSet.RbacV1().ClusterRoles().Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesClusterRoleBindingExist(ctx context.Context, name string) (bool, error) { + resource, err := provider.clientSet.RbacV1().ClusterRoleBindings().Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesRoleExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.RbacV1().Roles(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesRoleBindingExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.RbacV1().RoleBindings(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesPodExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) DoesDaemonSetExist(ctx context.Context, namespace string, name string) (bool, error) { + resource, err := provider.clientSet.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) + return provider.doesResourceExist(resource, err) +} + +func (provider *Provider) doesResourceExist(resource interface{}, err error) (bool, error) { var statusError *k8serrors.StatusError if errors.As(err, &statusError) { // expected behavior when resource does not exist @@ -173,27 +314,10 @@ func (provider *Provider) DoesMizuRBACExist(ctx context.Context, namespace strin if err != nil { return false, err } - return serviceAccount != nil, nil + return resource != nil, nil } -func (provider *Provider) DoesServicesExist(ctx context.Context, namespace string, serviceName string) (bool, error) { - service, err := provider.clientSet.CoreV1().Services(namespace).Get(ctx, serviceName, metav1.GetOptions{}) - - var statusError *k8serrors.StatusError - if errors.As(err, &statusError) { - if statusError.ErrStatus.Reason == metav1.StatusReasonNotFound { - return false, nil - } - } - if err != nil { - return false, err - } - return service != nil, nil -} - -func (provider *Provider) CreateMizuRBAC(ctx context.Context, namespace string, version string) error { - clusterRoleName := "mizu-cluster-role" - +func (provider *Provider) CreateMizuRBAC(ctx context.Context, namespace string, serviceAccountName string, clusterRoleName string, clusterRoleBindingName string, version string) error { serviceAccount := &core.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: serviceAccountName, @@ -216,7 +340,7 @@ func (provider *Provider) CreateMizuRBAC(ctx context.Context, namespace string, } clusterRoleBinding := &rbac.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: "mizu-cluster-role-binding", + Name: clusterRoleBindingName, Labels: map[string]string{"mizu-cli-version": version}, }, RoleRef: rbac.RoleRef{ @@ -233,23 +357,148 @@ func (provider *Provider) CreateMizuRBAC(ctx context.Context, namespace string, }, } _, err := provider.clientSet.CoreV1().ServiceAccounts(namespace).Create(ctx, serviceAccount, metav1.CreateOptions{}) - if err != nil { + if err != nil && !k8serrors.IsAlreadyExists(err) { return err } _, err = provider.clientSet.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{}) - if err != nil { + if err != nil && !k8serrors.IsAlreadyExists(err) { return err } _, err = provider.clientSet.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{}) - if err != nil { + if err != nil && !k8serrors.IsAlreadyExists(err) { return err } return nil } +func (provider *Provider) CreateMizuRBACNamespaceRestricted(ctx context.Context, namespace string, serviceAccountName string, roleName string, roleBindingName string, version string) error { + serviceAccount := &core.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + Namespace: namespace, + Labels: map[string]string{"mizu-cli-version": version}, + }, + } + role := &rbac.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleName, + Labels: map[string]string{"mizu-cli-version": version}, + }, + Rules: []rbac.PolicyRule{ + { + APIGroups: []string{"", "extensions", "apps"}, + Resources: []string{"pods", "services", "endpoints"}, + Verbs: []string{"list", "get", "watch"}, + }, + }, + } + roleBinding := &rbac.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleBindingName, + Labels: map[string]string{"mizu-cli-version": version}, + }, + RoleRef: rbac.RoleRef{ + Name: roleName, + Kind: "Role", + APIGroup: "rbac.authorization.k8s.io", + }, + Subjects: []rbac.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: namespace, + }, + }, + } + _, err := provider.clientSet.CoreV1().ServiceAccounts(namespace).Create(ctx, serviceAccount, metav1.CreateOptions{}) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + _, err = provider.clientSet.RbacV1().Roles(namespace).Create(ctx, role, metav1.CreateOptions{}) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + _, err = provider.clientSet.RbacV1().RoleBindings(namespace).Create(ctx, roleBinding, metav1.CreateOptions{}) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + return nil +} + +func (provider *Provider) RemoveNamespace(ctx context.Context, name string) error { + if isFound, err := provider.DoesNamespaceExist(ctx, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{}) +} + +func (provider *Provider) RemoveNonNamespacedResources(ctx context.Context, clusterRoleName string, clusterRoleBindingName string) error { + if err := provider.RemoveClusterRole(ctx, clusterRoleName); err != nil { + return err + } + + if err := provider.RemoveClusterRoleBinding(ctx, clusterRoleBindingName); err != nil { + return err + } + + return nil +} + +func (provider *Provider) RemoveClusterRole(ctx context.Context, name string) error { + if isFound, err := provider.DoesClusterRoleExist(ctx, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.RbacV1().ClusterRoles().Delete(ctx, name, metav1.DeleteOptions{}) +} + +func (provider *Provider) RemoveClusterRoleBinding(ctx context.Context, name string) error { + if isFound, err := provider.DoesClusterRoleBindingExist(ctx, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.RbacV1().ClusterRoleBindings().Delete(ctx, name, metav1.DeleteOptions{}) +} + +func (provider *Provider) RemoveRoleBinding(ctx context.Context, namespace string, name string) error { + if isFound, err := provider.DoesRoleBindingExist(ctx, namespace, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.RbacV1().RoleBindings(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +func (provider *Provider) RemoveRole(ctx context.Context, namespace string, name string) error { + if isFound, err := provider.DoesRoleExist(ctx, namespace, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.RbacV1().Roles(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +func (provider *Provider) RemoveServicAccount(ctx context.Context, namespace string, name string) error { + if isFound, err := provider.DoesServiceAccountExist(ctx, namespace, name); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + func (provider *Provider) RemovePod(ctx context.Context, namespace string, podName string) error { - if isFound, err := provider.CheckPodExists(ctx, namespace, podName); - err != nil { + if isFound, err := provider.DoesPodExist(ctx, namespace, podName); err != nil { return err } else if !isFound { return nil @@ -258,9 +507,18 @@ func (provider *Provider) RemovePod(ctx context.Context, namespace string, podNa return provider.clientSet.CoreV1().Pods(namespace).Delete(ctx, podName, metav1.DeleteOptions{}) } +func (provider *Provider) RemoveConfigMap(ctx context.Context, namespace string, configMapName string) error { + if isFound, err := provider.DoesConfigMapExist(ctx, namespace, configMapName); err != nil { + return err + } else if !isFound { + return nil + } + + return provider.clientSet.CoreV1().ConfigMaps(namespace).Delete(ctx, configMapName, metav1.DeleteOptions{}) +} + func (provider *Provider) RemoveService(ctx context.Context, namespace string, serviceName string) error { - if isFound, err := provider.CheckServiceExists(ctx, namespace, serviceName); - err != nil { + if isFound, err := provider.DoesServicesExist(ctx, namespace, serviceName); err != nil { return err } else if !isFound { return nil @@ -270,8 +528,7 @@ func (provider *Provider) RemoveService(ctx context.Context, namespace string, s } func (provider *Provider) RemoveDaemonSet(ctx context.Context, namespace string, daemonSetName string) error { - if isFound, err := provider.CheckDaemonSetExists(ctx, namespace, daemonSetName); - err != nil { + if isFound, err := provider.DoesDaemonSetExist(ctx, namespace, daemonSetName); err != nil { return err } else if !isFound { return nil @@ -280,58 +537,33 @@ func (provider *Provider) RemoveDaemonSet(ctx context.Context, namespace string, return provider.clientSet.AppsV1().DaemonSets(namespace).Delete(ctx, daemonSetName, metav1.DeleteOptions{}) } -func (provider *Provider) CheckPodExists(ctx context.Context, namespace string, name string) (bool, error) { - listOptions := metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - Limit: 1, - } - resourceList, err := provider.clientSet.CoreV1().Pods(namespace).List(ctx, listOptions) - if err != nil { - return false, err +func (provider *Provider) CreateConfigMap(ctx context.Context, namespace string, configMapName string, data string) error { + if data == "" { + return nil } - if len(resourceList.Items) > 0 { - return true, nil + configMapData := make(map[string]string, 0) + configMapData[shared.RulePolicyFileName] = data + configMap := &core.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: configMapData, } - - return false, nil + if _, err := provider.clientSet.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}); err != nil { + return err + } + return nil } -func (provider *Provider) CheckServiceExists(ctx context.Context, namespace string, name string) (bool, error) { - listOptions := metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - Limit: 1, - } - resourceList, err := provider.clientSet.CoreV1().Services(namespace).List(ctx, listOptions) - if err != nil { - return false, err - } +func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeToTappedPodIPMap map[string][]string, serviceAccountName string, tapOutgoing bool) error { + mizu.Log.Debugf("Applying %d tapper deamonsets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeToTappedPodIPMap), namespace, daemonSetName, podImage, tapperPodName) - if len(resourceList.Items) > 0 { - return true, nil - } - - return false, nil -} - -func (provider *Provider) CheckDaemonSetExists(ctx context.Context, namespace string, name string) (bool, error) { - listOptions := metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - Limit: 1, - } - resourceList, err := provider.clientSet.AppsV1().DaemonSets(namespace).List(ctx, listOptions) - if err != nil { - return false, err - } - - if len(resourceList.Items) > 0 { - return true, nil - } - - return false, nil -} - -func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, aggregatorPodIp string, nodeToTappedPodIPMap map[string][]string, linkServiceAccount bool, tapOutgoing bool) error { if len(nodeToTappedPodIPMap) == 0 { return fmt.Errorf("Daemon set %s must tap at least 1 pod", daemonSetName) } @@ -346,18 +578,17 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac "-i", "any", "--tap", "--hardump", - "--aggregator-address", fmt.Sprintf("ws://%s/wsTapper", aggregatorPodIp), + "--api-server-address", fmt.Sprintf("ws://%s/wsTapper", apiServerPodIp), } if tapOutgoing { mizuCmd = append(mizuCmd, "--anydirection") } - privileged := true agentContainer := applyconfcore.Container() agentContainer.WithName(tapperPodName) agentContainer.WithImage(podImage) agentContainer.WithImagePullPolicy(core.PullAlways) - agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(privileged)) + agentContainer.WithSecurityContext(applyconfcore.SecurityContext().WithPrivileged(true)) agentContainer.WithCommand(mizuCmd...) agentContainer.WithEnv( applyconfcore.EnvVar().WithName(shared.HostModeEnvVar).WithValue("1"), @@ -372,26 +603,26 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac ) cpuLimit, err := resource.ParseQuantity("500m") if err != nil { - return errors.New("invalid cpu limit for tapper container") + return errors.New(fmt.Sprintf("invalid cpu limit for %s container", tapperPodName)) } memLimit, err := resource.ParseQuantity("1Gi") if err != nil { - return errors.New("invalid memory limit for tapper container") + return errors.New(fmt.Sprintf("invalid memory limit for %s container", tapperPodName)) } cpuRequests, err := resource.ParseQuantity("50m") if err != nil { - return errors.New("invalid cpu request for tapper container") + return errors.New(fmt.Sprintf("invalid cpu request for %s container", tapperPodName)) } memRequests, err := resource.ParseQuantity("50Mi") if err != nil { - return errors.New("invalid memory request for tapper container") + return errors.New(fmt.Sprintf("invalid memory request for %s container", tapperPodName)) } agentResourceLimits := core.ResourceList{ - "cpu": cpuLimit, + "cpu": cpuLimit, "memory": memLimit, } agentResourceRequests := core.ResourceList{ - "cpu": cpuRequests, + "cpu": cpuRequests, "memory": memRequests, } agentResources := applyconfcore.ResourceRequirements().WithRequests(agentResourceRequests).WithLimits(agentResourceLimits) @@ -425,7 +656,7 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac podSpec.WithHostNetwork(true) podSpec.WithDNSPolicy(core.DNSClusterFirstWithHostNet) podSpec.WithTerminationGracePeriodSeconds(0) - if linkServiceAccount { + if serviceAccountName != "" { podSpec.WithServiceAccountName(serviceAccountName) } podSpec.WithContainers(agentContainer) @@ -446,18 +677,55 @@ func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespac return err } -func (provider *Provider) GetAllPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespace string) ([]core.Pod, error) { - pods, err := provider.clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err +func (provider *Provider) ListAllPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespaces []string) ([]core.Pod, error) { + var pods []core.Pod + for _, namespace := range namespaces { + namespacePods, err := provider.clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get pods in ns: [%s], %w", namespace, err) + } + + pods = append(pods, namespacePods.Items...) } + matchingPods := make([]core.Pod, 0) - for _, pod := range pods.Items { + for _, pod := range pods { if regex.MatchString(pod.Name) { matchingPods = append(matchingPods, pod) } } - return matchingPods, err + return matchingPods, nil +} + +func (provider *Provider) ListAllRunningPodsMatchingRegex(ctx context.Context, regex *regexp.Regexp, namespaces []string) ([]core.Pod, error) { + pods, err := provider.ListAllPodsMatchingRegex(ctx, regex, namespaces) + if err != nil { + return nil, err + } + + matchingPods := make([]core.Pod, 0) + for _, pod := range pods { + if isPodRunning(&pod) { + matchingPods = append(matchingPods, pod) + } + } + return matchingPods, nil +} + +func (provider *Provider) GetPodLogs(namespace string, podName string, ctx context.Context) (string, error) { + podLogOpts := core.PodLogOptions{} + req := provider.clientSet.CoreV1().Pods(namespace).GetLogs(podName, &podLogOpts) + podLogs, err := req.Stream(ctx) + if err != nil { + return "", fmt.Errorf("error opening log stream on ns: %s, pod: %s, %w", namespace, podName, err) + } + defer podLogs.Close() + buf := new(bytes.Buffer) + if _, err = io.Copy(buf, podLogs); err != nil { + return "", fmt.Errorf("error copy information from podLogs to buf, ns: %s, pod: %s, %w", namespace, podName, err) + } + str := buf.String() + return str, nil } func getClientSet(config *restclient.Config) *kubernetes.Clientset { @@ -469,11 +737,16 @@ func getClientSet(config *restclient.Config) *kubernetes.Clientset { } func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig { + if kubeConfigPath == "" { + kubeConfigPath = os.Getenv("KUBECONFIG") + } + if kubeConfigPath == "" { home := homedir.HomeDir() kubeConfigPath = filepath.Join(home, ".kube", "config") } + mizu.Log.Debugf("Using kube config %s", kubeConfigPath) configPathList := filepath.SplitList(kubeConfigPath) configLoadingRules := &clientcmd.ClientConfigLoadingRules{} if len(configPathList) <= 1 { @@ -489,3 +762,7 @@ func loadKubernetesConfiguration(kubeConfigPath string) clientcmd.ClientConfig { }, ) } + +func isPodRunning(pod *core.Pod) bool { + return pod.Status.Phase == core.PodRunning +} diff --git a/cli/kubernetes/proxy.go b/cli/kubernetes/proxy.go index 77904ddaa..5ca2eab16 100644 --- a/cli/kubernetes/proxy.go +++ b/cli/kubernetes/proxy.go @@ -2,6 +2,7 @@ package kubernetes import ( "fmt" + "github.com/up9inc/mizu/cli/mizu" "k8s.io/kubectl/pkg/proxy" "net" "net/http" @@ -13,6 +14,7 @@ const k8sProxyApiPrefix = "/" const mizuServicePort = 80 func StartProxy(kubernetesProvider *Provider, mizuPort uint16, mizuNamespace string, mizuServiceName string) error { + mizu.Log.Debugf("Starting proxy. namespace: [%v], service name: [%s], port: [%v]", mizuNamespace, mizuServiceName, mizuPort) filter := &proxy.FilterServer{ AcceptPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathAcceptRE), RejectPaths: proxy.MakeRegexpArrayOrDie(proxy.DefaultPathRejectRE), @@ -40,24 +42,24 @@ func StartProxy(kubernetesProvider *Provider, mizuPort uint16, mizuNamespace str return server.Serve(l) } -func getMizuCollectorProxiedHostAndPath(mizuNamespace string, mizuServiceName string) string { +func getMizuApiServerProxiedHostAndPath(mizuNamespace string, mizuServiceName string) string { return fmt.Sprintf("/api/v1/namespaces/%s/services/%s:%d/proxy/", mizuNamespace, mizuServiceName, mizuServicePort) } -func GetMizuCollectorProxiedHostAndPath(mizuPort uint16) string { +func GetMizuApiServerProxiedHostAndPath(mizuPort uint16) string { return fmt.Sprintf("localhost:%d/mizu", mizuPort) } func getRerouteHttpHandlerMizuAPI(proxyHandler http.Handler, mizuNamespace string, mizuServiceName string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Path = strings.Replace(r.URL.Path, "/mizu/", getMizuCollectorProxiedHostAndPath(mizuNamespace, mizuServiceName), 1) + r.URL.Path = strings.Replace(r.URL.Path, "/mizu/", getMizuApiServerProxiedHostAndPath(mizuNamespace, mizuServiceName), 1) proxyHandler.ServeHTTP(w, r) }) } func getRerouteHttpHandlerMizuStatic(proxyHandler http.Handler, mizuNamespace string, mizuServiceName string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.URL.Path = strings.Replace(r.URL.Path, "/static/", fmt.Sprintf("%s/static/", getMizuCollectorProxiedHostAndPath(mizuNamespace, mizuServiceName)), 1) + r.URL.Path = strings.Replace(r.URL.Path, "/static/", fmt.Sprintf("%s/static/", getMizuApiServerProxiedHostAndPath(mizuNamespace, mizuServiceName)), 1) proxyHandler.ServeHTTP(w, r) }) } diff --git a/cli/kubernetes/watch.go b/cli/kubernetes/watch.go index 9d914d4e5..7916feef6 100644 --- a/cli/kubernetes/watch.go +++ b/cli/kubernetes/watch.go @@ -4,49 +4,64 @@ import ( "context" "errors" "regexp" + "sync" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/watch" ) -func FilteredWatch(ctx context.Context, watcher watch.Interface, podFilter *regexp.Regexp) (chan *corev1.Pod, chan *corev1.Pod, chan *corev1.Pod, chan error) { +func FilteredWatch(ctx context.Context, kubernetesProvider *Provider, targetNamespaces []string, podFilter *regexp.Regexp) (chan *corev1.Pod, chan *corev1.Pod, chan *corev1.Pod, chan error) { addedChan := make(chan *corev1.Pod) modifiedChan := make(chan *corev1.Pod) removedChan := make(chan *corev1.Pod) errorChan := make(chan error) - go func() { - for { - select { - case e := <-watcher.ResultChan(): - if e.Object == nil { - errorChan <- errors.New("kubernetes pod watch failed") + var wg sync.WaitGroup + + for _, targetNamespace := range targetNamespaces { + wg.Add(1) + + go func(targetNamespace string) { + defer wg.Done() + watcher := kubernetesProvider.GetPodWatcher(ctx, targetNamespace) + + for { + select { + case e := <-watcher.ResultChan(): + if e.Object == nil { + errorChan <- errors.New("kubernetes pod watch failed") + return + } + + pod := e.Object.(*corev1.Pod) + + if !podFilter.MatchString(pod.Name) { + continue + } + + switch e.Type { + case watch.Added: + addedChan <- pod + case watch.Modified: + modifiedChan <- pod + case watch.Deleted: + removedChan <- pod + } + case <-ctx.Done(): + watcher.Stop() return } - - pod := e.Object.(*corev1.Pod) - - if !podFilter.MatchString(pod.Name) { - continue - } - - switch e.Type { - case watch.Added: - addedChan <- pod - case watch.Modified: - modifiedChan <- pod - case watch.Deleted: - removedChan <- pod - } - case <-ctx.Done(): - watcher.Stop() - close(addedChan) - close(modifiedChan) - close(removedChan) - close(errorChan) - return } - } + }(targetNamespace) + } + + go func() { + <-ctx.Done() + wg.Wait() + close(addedChan) + close(modifiedChan) + close(removedChan) + close(errorChan) }() return addedChan, modifiedChan, removedChan, errorChan diff --git a/cli/mizu.go b/cli/mizu.go index 7caa61604..6dc698567 100644 --- a/cli/mizu.go +++ b/cli/mizu.go @@ -1,7 +1,10 @@ package main -import "github.com/up9inc/mizu/cli/cmd" +import ( + "github.com/up9inc/mizu/cli/cmd" + "github.com/up9inc/mizu/cli/goUtils" +) func main() { - cmd.Execute() + goUtils.HandleExcWrapper(cmd.Execute) } diff --git a/cli/mizu/config.go b/cli/mizu/config.go new file mode 100644 index 000000000..2c2712af5 --- /dev/null +++ b/cli/mizu/config.go @@ -0,0 +1,279 @@ +package mizu + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path" + "reflect" + "strconv" + "strings" + + "github.com/creasty/defaults" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/up9inc/mizu/cli/mizu/configStructs" + "github.com/up9inc/mizu/cli/uiUtils" + "gopkg.in/yaml.v3" +) + +const ( + Separator = "=" + SetCommandName = "set" +) + +var allowedSetFlags = []string{ + AgentImageConfigName, + MizuResourcesNamespaceConfigName, + TelemetryConfigName, + DumpLogsConfigName, + KubeConfigPathName, + configStructs.AnalysisDestinationTapName, + configStructs.SleepIntervalSecTapName, +} + +var Config = ConfigStruct{} + +func (config *ConfigStruct) Validate() error { + if config.IsNsRestrictedMode() { + if config.Tap.AllNamespaces || len(config.Tap.Namespaces) != 1 || config.Tap.Namespaces[0] != config.MizuResourcesNamespace { + return fmt.Errorf("Not supported mode. Mizu can't resolve IPs in other namespaces when running in namespace restricted mode.\n"+ + "You can use the same namespace for --%s and --%s", configStructs.NamespacesTapName, MizuResourcesNamespaceConfigName) + } + } + + return nil +} + +func InitConfig(cmd *cobra.Command) error { + if err := defaults.Set(&Config); err != nil { + return err + } + + if err := mergeConfigFile(); err != nil { + return fmt.Errorf("invalid config %w\n"+ + "you can regenerate the file using `mizu config -r` or just remove it %v", err, GetConfigFilePath()) + } + + cmd.Flags().Visit(initFlag) + + finalConfigPrettified, _ := uiUtils.PrettyJson(Config) + Log.Debugf("Init config finished\n Final config: %v", finalConfigPrettified) + + return nil +} + +func GetConfigWithDefaults() (string, error) { + defaultConf := ConfigStruct{} + if err := defaults.Set(&defaultConf); err != nil { + return "", err + } + return uiUtils.PrettyYaml(defaultConf) +} + +func GetConfigFilePath() string { + return path.Join(GetMizuFolderPath(), "config.yaml") +} + +func mergeConfigFile() error { + reader, openErr := os.Open(GetConfigFilePath()) + if openErr != nil { + return nil + } + + buf, readErr := ioutil.ReadAll(reader) + if readErr != nil { + return readErr + } + + if err := yaml.Unmarshal(buf, &Config); err != nil { + return err + } + Log.Debugf("Found config file, merged to default options") + + return nil +} + +func initFlag(f *pflag.Flag) { + configElem := reflect.ValueOf(&Config).Elem() + + sliceValue, isSliceValue := f.Value.(pflag.SliceValue) + if !isSliceValue { + mergeFlagValue(configElem, f.Name, f.Value.String()) + return + } + + if f.Name == SetCommandName { + mergeSetFlag(sliceValue.GetSlice()) + return + } + + mergeFlagValues(configElem, f.Name, sliceValue.GetSlice()) +} + +func mergeSetFlag(setValues []string) { + configElem := reflect.ValueOf(&Config).Elem() + + for _, setValue := range setValues { + if !strings.Contains(setValue, Separator) { + Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s (set argument format: =)", setValue)) + } + + split := strings.SplitN(setValue, Separator, 2) + if len(split) != 2 { + Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s (set argument format: =)", setValue)) + } + + argumentKey, argumentValue := split[0], split[1] + + if !Contains(allowedSetFlags, argumentKey) { + Log.Warningf(uiUtils.Warning, fmt.Sprintf("Ignoring set argument %s, flag name must be one of the following: \"%s\"", setValue, strings.Join(allowedSetFlags, "\", \""))) + } + + mergeFlagValue(configElem, argumentKey, argumentValue) + } +} + +func mergeFlagValue(currentElem reflect.Value, flagKey string, flagValue string) { + for i := 0; i < currentElem.NumField(); i++ { + currentField := currentElem.Type().Field(i) + currentFieldByName := currentElem.FieldByName(currentField.Name) + + if currentField.Type.Kind() == reflect.Struct { + mergeFlagValue(currentFieldByName, flagKey, flagValue) + continue + } + + if currentField.Tag.Get("yaml") != flagKey { + continue + } + + flagValueKind := currentField.Type.Kind() + + parsedValue, err := getParsedValue(flagValueKind, flagValue) + if err != nil { + Log.Warningf(uiUtils.Red, fmt.Sprintf("Invalid value %v for flag name %s, expected %s", flagValue, flagKey, flagValueKind)) + return + } + + currentFieldByName.Set(parsedValue) + } +} + +func mergeFlagValues(currentElem reflect.Value, flagKey string, flagValues []string) { + for i := 0; i < currentElem.NumField(); i++ { + currentField := currentElem.Type().Field(i) + currentFieldByName := currentElem.FieldByName(currentField.Name) + + if currentField.Type.Kind() == reflect.Struct { + mergeFlagValues(currentFieldByName, flagKey, flagValues) + continue + } + + if currentField.Tag.Get("yaml") != flagKey { + continue + } + + flagValueKind := currentField.Type.Elem().Kind() + + parsedValues := reflect.MakeSlice(reflect.SliceOf(currentField.Type.Elem()), 0, 0) + for _, flagValue := range flagValues { + parsedValue, err := getParsedValue(flagValueKind, flagValue) + if err != nil { + Log.Warningf(uiUtils.Red, fmt.Sprintf("Invalid value %v for flag name %s, expected %s", flagValue, flagKey, flagValueKind)) + return + } + + parsedValues = reflect.Append(parsedValues, parsedValue) + } + + currentFieldByName.Set(parsedValues) + } +} + +func getParsedValue(kind reflect.Kind, value string) (reflect.Value, error) { + switch kind { + case reflect.String: + return reflect.ValueOf(value), nil + case reflect.Bool: + boolArgumentValue, err := strconv.ParseBool(value) + if err != nil { + break + } + + return reflect.ValueOf(boolArgumentValue), nil + case reflect.Int: + intArgumentValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + break + } + + return reflect.ValueOf(int(intArgumentValue)), nil + case reflect.Int8: + intArgumentValue, err := strconv.ParseInt(value, 10, 8) + if err != nil { + break + } + + return reflect.ValueOf(int8(intArgumentValue)), nil + case reflect.Int16: + intArgumentValue, err := strconv.ParseInt(value, 10, 16) + if err != nil { + break + } + + return reflect.ValueOf(int16(intArgumentValue)), nil + case reflect.Int32: + intArgumentValue, err := strconv.ParseInt(value, 10, 32) + if err != nil { + break + } + + return reflect.ValueOf(int32(intArgumentValue)), nil + case reflect.Int64: + intArgumentValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + break + } + + return reflect.ValueOf(intArgumentValue), nil + case reflect.Uint: + uintArgumentValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + break + } + + return reflect.ValueOf(uint(uintArgumentValue)), nil + case reflect.Uint8: + uintArgumentValue, err := strconv.ParseUint(value, 10, 8) + if err != nil { + break + } + + return reflect.ValueOf(uint8(uintArgumentValue)), nil + case reflect.Uint16: + uintArgumentValue, err := strconv.ParseUint(value, 10, 16) + if err != nil { + break + } + + return reflect.ValueOf(uint16(uintArgumentValue)), nil + case reflect.Uint32: + uintArgumentValue, err := strconv.ParseUint(value, 10, 32) + if err != nil { + break + } + + return reflect.ValueOf(uint32(uintArgumentValue)), nil + case reflect.Uint64: + uintArgumentValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + break + } + + return reflect.ValueOf(uintArgumentValue), nil + } + + return reflect.ValueOf(nil), errors.New("value to parse does not match type") +} diff --git a/cli/mizu/configStruct.go b/cli/mizu/configStruct.go new file mode 100644 index 000000000..24ee0d49e --- /dev/null +++ b/cli/mizu/configStruct.go @@ -0,0 +1,35 @@ +package mizu + +import ( + "fmt" + + "github.com/up9inc/mizu/cli/mizu/configStructs" +) + +const ( + AgentImageConfigName = "agent-image" + MizuResourcesNamespaceConfigName = "mizu-resources-namespace" + TelemetryConfigName = "telemetry" + DumpLogsConfigName = "dump-logs" + KubeConfigPathName = "kube-config-path" +) + +type ConfigStruct struct { + Tap configStructs.TapConfig `yaml:"tap"` + Fetch configStructs.FetchConfig `yaml:"fetch"` + Version configStructs.VersionConfig `yaml:"version"` + View configStructs.ViewConfig `yaml:"view"` + AgentImage string `yaml:"agent-image"` + MizuResourcesNamespace string `yaml:"mizu-resources-namespace" default:"mizu"` + Telemetry bool `yaml:"telemetry" default:"true"` + DumpLogs bool `yaml:"dump-logs" default:"false"` + KubeConfigPath string `yaml:"kube-config-path" default:""` +} + +func (config *ConfigStruct) SetDefaults() { + config.AgentImage = fmt.Sprintf("gcr.io/up9-docker-hub/mizu/%s:%s", Branch, SemVer) +} + +func (config *ConfigStruct) IsNsRestrictedMode() bool { + return config.MizuResourcesNamespace != "mizu" // Notice "mizu" string must match the default MizuResourcesNamespace +} diff --git a/cli/mizu/configStructs/fetchConfig.go b/cli/mizu/configStructs/fetchConfig.go new file mode 100644 index 000000000..e84fba8b6 --- /dev/null +++ b/cli/mizu/configStructs/fetchConfig.go @@ -0,0 +1,15 @@ +package configStructs + +const ( + DirectoryFetchName = "directory" + FromTimestampFetchName = "from" + ToTimestampFetchName = "to" + GuiPortFetchName = "gui-port" +) + +type FetchConfig struct { + Directory string `yaml:"directory" default:"."` + FromTimestamp int `yaml:"from" default:"0"` + ToTimestamp int `yaml:"to" default:"0"` + GuiPort uint16 `yaml:"gui-port" default:"8899"` +} diff --git a/cli/mizu/configStructs/tapConfig.go b/cli/mizu/configStructs/tapConfig.go new file mode 100644 index 000000000..67582e467 --- /dev/null +++ b/cli/mizu/configStructs/tapConfig.go @@ -0,0 +1,81 @@ +package configStructs + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/up9inc/mizu/shared/units" +) + +const ( + AnalysisDestinationTapName = "dest" + SleepIntervalSecTapName = "upload-interval" + GuiPortTapName = "gui-port" + NamespacesTapName = "namespaces" + AnalysisTapName = "analysis" + AllNamespacesTapName = "all-namespaces" + PlainTextFilterRegexesTapName = "regex-masking" + HideHealthChecksTapName = "hide-healthchecks" + DisableRedactionTapName = "no-redact" + HumanMaxEntriesDBSizeTapName = "max-entries-db-size" + DirectionTapName = "direction" + DryRunTapName = "dry-run" + EnforcePolicyFile = "test-rules" +) + +type TapConfig struct { + AnalysisDestination string `yaml:"dest" default:"up9.app"` + SleepIntervalSec int `yaml:"upload-interval" default:"10"` + PodRegexStr string `yaml:"regex" default:".*"` + GuiPort uint16 `yaml:"gui-port" default:"8899"` + Namespaces []string `yaml:"namespaces"` + Analysis bool `yaml:"analysis" default:"false"` + AllNamespaces bool `yaml:"all-namespaces" default:"false"` + PlainTextFilterRegexes []string `yaml:"regex-masking"` + HideHealthChecks bool `yaml:"hide-healthchecks" default:"false"` + DisableRedaction bool `yaml:"no-redact" default:"false"` + HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"` + Direction string `yaml:"direction" default:"in"` + DryRun bool `yaml:"dry-run" default:"false"` + EnforcePolicyFile string `yaml:"test-rules"` +} + +func (config *TapConfig) PodRegex() *regexp.Regexp { + podRegex, _ := regexp.Compile(config.PodRegexStr) + return podRegex +} + +func (config *TapConfig) TapOutgoing() bool { + directionLowerCase := strings.ToLower(config.Direction) + if directionLowerCase == "any" { + return true + } + + return false +} + +func (config *TapConfig) MaxEntriesDBSizeBytes() int64 { + maxEntriesDBSizeBytes, _ := units.HumanReadableToBytes(config.HumanMaxEntriesDBSize) + return maxEntriesDBSizeBytes +} + +func (config *TapConfig) Validate() error { + _, compileErr := regexp.Compile(config.PodRegexStr) + if compileErr != nil { + return errors.New(fmt.Sprintf("%s is not a valid regex %s", config.PodRegexStr, compileErr)) + } + + _, parseHumanDataSizeErr := units.HumanReadableToBytes(config.HumanMaxEntriesDBSize) + if parseHumanDataSizeErr != nil { + return errors.New(fmt.Sprintf("Could not parse --%s value %s", HumanMaxEntriesDBSizeTapName, config.HumanMaxEntriesDBSize)) + } + + directionLowerCase := strings.ToLower(config.Direction) + if directionLowerCase != "any" && directionLowerCase != "in" { + return errors.New(fmt.Sprintf("%s is not a valid value for flag --%s. Acceptable values are in/any.", config.Direction, DirectionTapName)) + } + + return nil +} diff --git a/cli/mizu/configStructs/versionConfig.go b/cli/mizu/configStructs/versionConfig.go new file mode 100644 index 000000000..fd09a4747 --- /dev/null +++ b/cli/mizu/configStructs/versionConfig.go @@ -0,0 +1,9 @@ +package configStructs + +const ( + DebugInfoVersionName = "debug" +) + +type VersionConfig struct { + DebugInfo bool `yaml:"debug" default:"false"` +} diff --git a/cli/mizu/configStructs/viewConfig.go b/cli/mizu/configStructs/viewConfig.go new file mode 100644 index 000000000..6f26ca9dc --- /dev/null +++ b/cli/mizu/configStructs/viewConfig.go @@ -0,0 +1,11 @@ +package configStructs + +const ( + GuiPortViewName = "gui-port" + KubeConfigPathViewName = "kube-config" +) + +type ViewConfig struct { + GuiPort uint16 `yaml:"gui-port" default:"8899"` + KubeConfigPath string `yaml:"kube-config"` +} diff --git a/cli/mizu/consts.go b/cli/mizu/consts.go index 1f960ead2..c90c78179 100644 --- a/cli/mizu/consts.go +++ b/cli/mizu/consts.go @@ -1,5 +1,10 @@ package mizu +import ( + "os" + "path" +) + var ( SemVer = "0.0.1" Branch = "develop" @@ -9,20 +14,23 @@ var ( ) const ( - ResourcesNamespace = "default" - TapperDaemonSetName = "mizu-tapper-daemon-set" - AggregatorPodName = "mizu-collector" - TapperPodName = "mizu-tapper" - K8sAllNamespaces = "" + MizuResourcesPrefix = "mizu-" + ApiServerPodName = MizuResourcesPrefix + "api-server" + ClusterRoleBindingName = MizuResourcesPrefix + "cluster-role-binding" + ClusterRoleName = MizuResourcesPrefix + "cluster-role" + K8sAllNamespaces = "" + RoleBindingName = MizuResourcesPrefix + "role-binding" + RoleName = MizuResourcesPrefix + "role" + ServiceAccountName = MizuResourcesPrefix + "service-account" + TapperDaemonSetName = MizuResourcesPrefix + "tapper-daemon-set" + TapperPodName = MizuResourcesPrefix + "tapper" + ConfigMapName = MizuResourcesPrefix + "policy" ) -const ( - Black = "\033[1;30m%s\033[0m" - Red = "\033[1;31m%s\033[0m" - Green = "\033[1;32m%s\033[0m" - Yellow = "\033[1;33m%s\033[0m" - Purple = "\033[1;34m%s\033[0m" - Magenta = "\033[1;35m%s\033[0m" - Teal = "\033[1;36m%s\033[0m" - White = "\033[1;37m%s\033[0m" -) +func GetMizuFolderPath() string { + home, homeDirErr := os.UserHomeDir() + if homeDirErr != nil { + return "" + } + return path.Join(home, ".mizu") +} diff --git a/cli/mizu/logger.go b/cli/mizu/logger.go new file mode 100644 index 000000000..251922263 --- /dev/null +++ b/cli/mizu/logger.go @@ -0,0 +1,38 @@ +package mizu + +import ( + "github.com/op/go-logging" + "os" + "path" +) + +var Log = logging.MustGetLogger("mizu_cli") + +var format = logging.MustStringFormatter( + `%{time} %{level:.5s} ▶ %{pid} %{shortfile} %{shortfunc} ▶ %{message}`, +) + +func GetLogFilePath() string { + return path.Join(GetMizuFolderPath(), "mizu_cli.log") +} + +func InitLogger() { + logPath := GetLogFilePath() + f, err := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + Log.Infof("Failed to open mizu log file: %v, err %v", logPath, err) + } + + fileLog := logging.NewLogBackend(f, "", 0) + consoleLog := logging.NewLogBackend(os.Stderr, "", 0) + + backend2Formatter := logging.NewBackendFormatter(fileLog, format) + + backend1Leveled := logging.AddModuleLevel(consoleLog) + backend1Leveled.SetLevel(logging.INFO, "") + + logging.SetBackend(backend1Leveled, backend2Formatter) + + Log.Debugf("\n\n\n") + Log.Debugf("Running mizu version %v", SemVer) +} diff --git a/cli/mizu/sliceUtils.go b/cli/mizu/sliceUtils.go new file mode 100644 index 000000000..551e12603 --- /dev/null +++ b/cli/mizu/sliceUtils.go @@ -0,0 +1,11 @@ +package mizu + +func Contains(slice []string, containsValue string) bool { + for _, sliceValue := range slice { + if sliceValue == containsValue { + return true + } + } + + return false +} diff --git a/cli/mizu/telemetry.go b/cli/mizu/telemetry.go new file mode 100644 index 000000000..e36956cd9 --- /dev/null +++ b/cli/mizu/telemetry.go @@ -0,0 +1,36 @@ +package mizu + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +const telemetryUrl = "https://us-east4-up9-prod.cloudfunctions.net/mizu-telemetry" + +func ReportRun(cmd string, args interface{}) { + if !Config.Telemetry { + Log.Debugf("not reporting due to config value") + return + } + + argsBytes, _ := json.Marshal(args) + argsMap := map[string]string{ + "telemetry_type": "execution", + "cmd": cmd, + "args": string(argsBytes), + "component": "mizu_cli", + "BuildTimestamp": BuildTimestamp, + "Branch": Branch, + "version": SemVer} + argsMap["message"] = fmt.Sprintf("mizu %v - %v", argsMap["cmd"], string(argsBytes)) + + jsonValue, _ := json.Marshal(argsMap) + + if resp, err := http.Post(telemetryUrl, "application/json", bytes.NewBuffer(jsonValue)); err != nil { + Log.Debugf("error sending telemetry err: %v, response %v", err, resp) + } else { + Log.Debugf("Successfully reported telemetry") + } +} diff --git a/cli/mizu/versionCheck.go b/cli/mizu/versionCheck.go index 3211f687d..74a5c84e3 100644 --- a/cli/mizu/versionCheck.go +++ b/cli/mizu/versionCheck.go @@ -1,14 +1,19 @@ package mizu import ( + "context" "encoding/json" "fmt" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/shared/semver" + "io/ioutil" "net/http" "net/url" -) + "time" + "github.com/google/go-github/v37/github" + "github.com/up9inc/mizu/cli/uiUtils" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/shared/semver" +) func getApiVersion(port uint16) (string, error) { versionUrl, _ := url.Parse(fmt.Sprintf("http://localhost:%d/mizu/metadata/version", port)) @@ -41,6 +46,48 @@ func CheckVersionCompatibility(port uint16) (bool, error) { return true, nil } - fmt.Printf(Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)\n", SemVer, apiSemVer)) + Log.Errorf(uiUtils.Red, fmt.Sprintf("cli version (%s) is not compatible with api version (%s)", SemVer, apiSemVer)) return false, nil } + +func CheckNewerVersion() { + Log.Debugf("Checking for newer version...") + start := time.Now() + client := github.NewClient(nil) + latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "up9inc", "mizu") + if err != nil { + Log.Debugf("[ERROR] Failed to get latest release") + return + } + + versionFileUrl := "" + for _, asset := range latestRelease.Assets { + if *asset.Name == "version.txt" { + versionFileUrl = *asset.BrowserDownloadURL + break + } + } + if versionFileUrl == "" { + Log.Debugf("[ERROR] Version file not found in the latest release") + return + } + + res, err := http.Get(versionFileUrl) + if err != nil { + Log.Debugf("[ERROR] Failed to get the version file %v", err) + return + } + + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + Log.Debugf("[ERROR] Failed to read the version file -> %v", err) + return + } + gitHubVersion := string(data) + gitHubVersion = gitHubVersion[:len(gitHubVersion)-1] + Log.Debugf("Finished version validation, took %v", time.Since(start)) + if SemVer < gitHubVersion { + Log.Infof(uiUtils.Yellow, fmt.Sprintf("Update available! %v -> %v (%v)", SemVer, gitHubVersion, *latestRelease.HTMLURL)) + } +} diff --git a/cli/uiUtils/colors.go b/cli/uiUtils/colors.go new file mode 100644 index 000000000..c7b0887c4 --- /dev/null +++ b/cli/uiUtils/colors.go @@ -0,0 +1,15 @@ +package uiUtils + + +const ( + Black = "\033[1;30m%s\033[0m" + Red = "\033[1;31m%s\033[0m" + Green = "\033[1;32m%s\033[0m" + Yellow = "\033[1;33m%s\033[0m" + Purple = "\033[1;34m%s\033[0m" + Magenta = "\033[1;35m%s\033[0m" + Teal = "\033[1;36m%s\033[0m" + White = "\033[1;37m%s\033[0m" + Error = Red + Warning = Yellow +) diff --git a/cli/uiUtils/confirmation.go b/cli/uiUtils/confirmation.go index 06b912431..afa57a1bd 100644 --- a/cli/uiUtils/confirmation.go +++ b/cli/uiUtils/confirmation.go @@ -3,7 +3,6 @@ package uiUtils import ( "bufio" "fmt" - "github.com/up9inc/mizu/cli/mizu" "log" "os" "strings" @@ -12,7 +11,7 @@ import ( func AskForConfirmation(s string) bool { reader := bufio.NewReader(os.Stdin) - fmt.Printf(mizu.Magenta, s) + fmt.Printf(Magenta, s) response, err := reader.ReadString('\n') if err != nil { diff --git a/cli/uiUtils/prettyString.go b/cli/uiUtils/prettyString.go new file mode 100644 index 000000000..561ca52ee --- /dev/null +++ b/cli/uiUtils/prettyString.go @@ -0,0 +1,36 @@ +package uiUtils + +import ( + "bytes" + "encoding/json" + "gopkg.in/yaml.v3" +) + +const ( + empty = "" + tab = "\t" +) + +func PrettyJson(data interface{}) (string, error) { + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent(empty, tab) + + err := encoder.Encode(data) + if err != nil { + return empty, err + } + return buffer.String(), nil +} + +func PrettyYaml(data interface{}) (string, error) { + buffer := new(bytes.Buffer) + encoder := yaml.NewEncoder(buffer) + encoder.SetIndent(0) + + err := encoder.Encode(data) + if err != nil { + return empty, err + } + return buffer.String(), nil +} diff --git a/debug.Dockerfile b/debug.Dockerfile index d0dc3102b..f3145d218 100644 --- a/debug.Dockerfile +++ b/debug.Dockerfile @@ -1,4 +1,4 @@ -# creates image in which mizu api is remotely debuggable using delve +# creates image in which mizu agent is remotely debuggable using delve FROM node:14-slim AS site-build WORKDIR /app/ui-build @@ -14,18 +14,21 @@ ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64 RUN apk add libpcap-dev gcc g++ make -# Move to api working directory (/api-build). -WORKDIR /app/api-build +# Move to agent working directory (/agent-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 tap/go.mod tap/go.mod ../tap/ + RUN go mod download # 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 -# Copy and build api code +# Copy and build agent code COPY shared ../shared -COPY api . +COPY tap ../tap +COPY agent . RUN go build -gcflags="all=-N -l" -o mizuagent . @@ -35,10 +38,11 @@ RUN apk add bash libpcap-dev tcpdump WORKDIR /app # 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=site-build ["/app/ui-build/build", "site"] # install remote debugging tool RUN go get github.com/go-delve/delve/cmd/dlv -CMD ["sh", "-c", "dlv --headless=true --listen=:2345 --log --api-version=2 --accept-multiclient exec ./mizuagent -- --aggregator"] +ENTRYPOINT "/app/mizuagent" +#CMD ["sh", "-c", "dlv --headless=true --listen=:2345 --log --api-version=2 --accept-multiclient exec ./mizuagent -- --api-server"] diff --git a/examples/roles/permissions-all-namespaces-without-ip-resolution.yaml b/examples/roles/permissions-all-namespaces-without-ip-resolution.yaml new file mode 100644 index 000000000..c4e809ac3 --- /dev/null +++ b/examples/roles/permissions-all-namespaces-without-ip-resolution.yaml @@ -0,0 +1,35 @@ +# This example shows the roles required for a user to be able to use Mizu in all namespaces with IP resolution disabled. +# (Traffic will be recorded, but Mizu will not translate IP addresses to names) +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-clusterrole +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services"] + verbs: ["create", "delete"] +- apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["create", "patch", "delete"] +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-clusterrolebindings +subjects: +- kind: User + name: user1 + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: mizu-runner-clusterrole + apiGroup: rbac.authorization.k8s.io diff --git a/examples/roles/permissions-all-namespaces.yaml b/examples/roles/permissions-all-namespaces.yaml new file mode 100644 index 000000000..ff1060df0 --- /dev/null +++ b/examples/roles/permissions-all-namespaces.yaml @@ -0,0 +1,58 @@ +# This example shows the roles required for a user to be able to use Mizu in all namespaces. +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-clusterrole +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["create", "patch", "delete"] +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get"] +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterrolebindings"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] + verbs: ["get", "create", "delete"] +- apiGroups: ["apps", "extensions"] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: ["apps", "extensions"] + resources: ["services"] + verbs: ["get", "list", "watch"] +- apiGroups: ["", "apps", "extensions"] + resources: ["endpoints"] + verbs: ["get", "list", "watch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-clusterrolebindings +subjects: +- kind: User + name: user1 + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: ClusterRole + name: mizu-runner-clusterrole + apiGroup: rbac.authorization.k8s.io diff --git a/examples/roles/permissions-ns-with-validation.yaml b/examples/roles/permissions-ns-with-validation.yaml new file mode 100644 index 000000000..e2d6863ec --- /dev/null +++ b/examples/roles/permissions-ns-with-validation.yaml @@ -0,0 +1,54 @@ +# This example shows the roles required for a user to be able to use Mizu in a single namespace. +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-role + namespace: user1 +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["get", "create", "patch", "delete"] +- apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get"] +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "create", "delete"] +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] + verbs: ["get", "create", "delete"] +- apiGroups: ["apps", "extensions"] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: ["apps", "extensions"] + resources: ["services"] + verbs: ["get", "list", "watch"] +- apiGroups: ["", "apps", "extensions"] + resources: ["endpoints"] + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-rolebindings + namespace: user1 +subjects: +- kind: User + name: user1 + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: mizu-runner-role + apiGroup: rbac.authorization.k8s.io diff --git a/examples/roles/permissions-ns-without-ip-resolution.yaml b/examples/roles/permissions-ns-without-ip-resolution.yaml new file mode 100644 index 000000000..4293f8979 --- /dev/null +++ b/examples/roles/permissions-ns-without-ip-resolution.yaml @@ -0,0 +1,33 @@ +# This example shows the roles required for a user to be able to use Mizu in a single namespace with IP resolution disabled. +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-role + namespace: user1 +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "create", "delete"] +- apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["get", "create", "patch", "delete"] +- apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-rolebindings + namespace: user1 +subjects: +- kind: User + name: user1 + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: mizu-runner-role + apiGroup: rbac.authorization.k8s.io diff --git a/examples/roles/permissions-ns.yaml b/examples/roles/permissions-ns.yaml new file mode 100644 index 000000000..da60a02e1 --- /dev/null +++ b/examples/roles/permissions-ns.yaml @@ -0,0 +1,51 @@ +# This example shows the roles required for a user to be able to use Mizu in a single namespace. +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-role + namespace: user1 +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: [""] + resources: ["services"] + verbs: ["get", "list", "watch", "create", "delete"] +- apiGroups: ["apps"] + resources: ["daemonsets"] + verbs: ["get", "create", "patch", "delete"] +- apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get"] +- apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles"] + verbs: ["get", "create", "delete"] +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["rolebindings"] + verbs: ["get", "create", "delete"] +- apiGroups: ["apps", "extensions"] + resources: ["pods"] + verbs: ["get", "list", "watch"] +- apiGroups: ["apps", "extensions"] + resources: ["services"] + verbs: ["get", "list", "watch"] +- apiGroups: ["", "apps", "extensions"] + resources: ["endpoints"] + verbs: ["get", "list", "watch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: mizu-runner-rolebindings + namespace: user1 +subjects: +- kind: User + name: user1 + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: mizu-runner-role + apiGroup: rbac.authorization.k8s.io diff --git a/shared/consts.go b/shared/consts.go index 0d452910d..71cafdff2 100644 --- a/shared/consts.go +++ b/shared/consts.go @@ -5,5 +5,7 @@ const ( HostModeEnvVar = "HOST_MODE" NodeNameEnvVar = "NODE_NAME" TappedAddressesPerNodeDictEnvVar = "TAPPED_ADDRESSES_PER_HOST" - MaxEntriesDBSizeByteSEnvVar = "MAX_ENTRIES_DB_BYTES" + MaxEntriesDBSizeBytesEnvVar = "MAX_ENTRIES_DB_BYTES" + RulePolicyPath = "/app/enforce-policy/" + RulePolicyFileName = "enforce-policy.yaml" ) diff --git a/shared/debounce/debounce.go b/shared/debounce/debounce.go index 7772591ef..74e15e2c6 100644 --- a/shared/debounce/debounce.go +++ b/shared/debounce/debounce.go @@ -1,6 +1,7 @@ package debounce import ( + "fmt" "time" ) @@ -13,9 +14,10 @@ func NewDebouncer(timeout time.Duration, callback func()) *Debouncer { type Debouncer struct { callback func() - running bool - timeout time.Duration - timer *time.Timer + running bool + canceled bool + timeout time.Duration + timer *time.Timer } func (d *Debouncer) setTimeout(timeout time.Duration) { @@ -25,18 +27,28 @@ func (d *Debouncer) setTimeout(timeout time.Duration) { func (d *Debouncer) setCallback(callback func()) { callbackWrapped := func() { - callback() + if !d.canceled { + callback() + } d.running = false } d.callback = callbackWrapped } -func (d *Debouncer) SetOn() { +func (d *Debouncer) Cancel() { + d.canceled = true +} + +func (d *Debouncer) SetOn() error { + if d.canceled { + return fmt.Errorf("debouncer cancelled") + } if d.running == true { - return + return nil } d.running = true d.timer = time.AfterFunc(d.timeout, d.callback) + return nil } diff --git a/shared/go.mod b/shared/go.mod index 66e5165d6..157d3e5fa 100644 --- a/shared/go.mod +++ b/shared/go.mod @@ -3,7 +3,8 @@ module github.com/up9inc/mizu/shared go 1.16 require ( + github.com/google/martian v2.1.0+incompatible // indirect github.com/gorilla/websocket v1.4.2 + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/docker/go-units v0.4.0 ) - diff --git a/shared/go.sum b/shared/go.sum index b46c3a514..498bce1d3 100644 --- a/shared/go.sum +++ b/shared/go.sum @@ -1,4 +1,8 @@ +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 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/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= diff --git a/shared/models.go b/shared/models.go index 14c1b3ce9..2a70fa212 100644 --- a/shared/models.go +++ b/shared/models.go @@ -1,5 +1,13 @@ package shared +import ( + "fmt" + "io/ioutil" + "strings" + + yaml "gopkg.in/yaml.v3" +) + type WebSocketMessageType string const ( @@ -7,6 +15,7 @@ const ( WebSocketMessageTypeTappedEntry WebSocketMessageType = "tappedEntry" WebSocketMessageTypeUpdateStatus WebSocketMessageType = "status" WebSocketMessageTypeAnalyzeStatus WebSocketMessageType = "analyzeStatus" + WebsocketMessageTypeOutboundLink WebSocketMessageType = "outboundLink" ) type WebSocketMessageMetadata struct { @@ -31,7 +40,8 @@ type WebSocketStatusMessage struct { } type TapStatus struct { - Pods []PodInfo `json:"pods"` + Pods []PodInfo `json:"pods"` + TLSLinks []TLSLinkInfo `json:"tlsLinks"` } type PodInfo struct { @@ -39,6 +49,13 @@ type PodInfo struct { Name string `json:"name"` } +type TLSLinkInfo struct { + SourceIP string `json:"sourceIP"` + DestinationAddress string `json:"destinationAddress"` + ResolvedDestinationName string `json:"resolvedDestinationName"` + ResolvedSourceName string `json:"resolvedSourceName"` +} + func CreateWebSocketStatusMessage(tappingStatus TapStatus) WebSocketStatusMessage { return WebSocketStatusMessage{ WebSocketMessageMetadata: &WebSocketMessageMetadata{ @@ -60,9 +77,82 @@ func CreateWebSocketMessageTypeAnalyzeStatus(analyzeStatus AnalyzeStatus) WebSoc type TrafficFilteringOptions struct { PlainTextMaskingRegexes []*SerializableRegexp HideHealthChecks bool + DisableRedaction bool } type VersionResponse struct { SemVer string `json:"semver"` } +type RulesPolicy struct { + Rules []RulePolicy `yaml:"rules"` +} + +type RulePolicy struct { + Type string `yaml:"type"` + Service string `yaml:"service"` + Path string `yaml:"path"` + Method string `yaml:"method"` + Key string `yaml:"key"` + Value string `yaml:"value"` + Latency int64 `yaml:"latency"` + Name string `yaml:"name"` +} + +func (r *RulePolicy) validateType() bool { + permitedTypes := []string{"json", "header", "latency"} + _, found := Find(permitedTypes, r.Type) + if !found { + fmt.Printf("\nRule with name %s will be ignored. Err: only json, header and latency types are supported on rule definition.\n", r.Name) + } + if strings.ToLower(r.Type) == "latency" { + if r.Latency == 0 { + fmt.Printf("\nRule with name %s will be ignored. Err: when type=latency, the field Latency should be specified and have a value >= 1\n\n", r.Name) + found = false + } + } + return found +} + +func (rules *RulesPolicy) ValidateRulesPolicy() []int { + invalidIndex := make([]int, 0) + for i := range rules.Rules { + validated := rules.Rules[i].validateType() + if !validated { + invalidIndex = append(invalidIndex, i) + } + } + return invalidIndex +} + +func (rules *RulesPolicy) RemoveRule(idx int) { + rules.Rules = append(rules.Rules[:idx], rules.Rules[idx+1:]...) +} + +func Find(slice []string, val string) (int, bool) { + for i, item := range slice { + if item == val { + return i, true + } + } + return -1, false +} + +func DecodeEnforcePolicy(path string) (RulesPolicy, error) { + content, err := ioutil.ReadFile(path) + enforcePolicy := RulesPolicy{} + if err != nil { + return enforcePolicy, err + } + err = yaml.Unmarshal([]byte(content), &enforcePolicy) + if err != nil { + return enforcePolicy, err + } + invalidIndex := enforcePolicy.ValidateRulesPolicy() + if len(invalidIndex) != 0 { + for i := range invalidIndex { + enforcePolicy.RemoveRule(invalidIndex[i]) + } + } + return enforcePolicy, nil +} diff --git a/tap/cleaner.go b/tap/cleaner.go index 157a74eb0..96972fc9e 100644 --- a/tap/cleaner.go +++ b/tap/cleaner.go @@ -1,6 +1,7 @@ package tap import ( + "github.com/romana/rlog" "sync" "time" @@ -20,19 +21,21 @@ type Cleaner struct { cleanPeriod time.Duration connectionTimeout time.Duration stats CleanerStats - statsMutex sync.Mutex + statsMutex sync.Mutex } func (cl *Cleaner) clean() { startCleanTime := time.Now() cl.assemblerMutex.Lock() + rlog.Debugf("Assembler Stats before cleaning %s", cl.assembler.Dump()) flushed, closed := cl.assembler.FlushCloseOlderThan(startCleanTime.Add(-cl.connectionTimeout)) cl.assemblerMutex.Unlock() deleted := cl.matcher.deleteOlderThan(startCleanTime.Add(-cl.connectionTimeout)) cl.statsMutex.Lock() + rlog.Debugf("Assembler Stats after cleaning %s", cl.assembler.Dump()) cl.stats.flushed += flushed cl.stats.closed += closed cl.stats.deleted += deleted @@ -55,7 +58,7 @@ func (cl *Cleaner) dumpStats() CleanerStats { stats := CleanerStats{ flushed: cl.stats.flushed, - closed : cl.stats.closed, + closed: cl.stats.closed, deleted: cl.stats.deleted, } diff --git a/tap/go.mod b/tap/go.mod index db13e354b..48057e605 100644 --- a/tap/go.mod +++ b/tap/go.mod @@ -8,4 +8,5 @@ require ( github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 golang.org/x/net v0.0.0-20210421230115-4e50805a0758 + github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 ) diff --git a/tap/go.sum b/tap/go.sum index 4530f201f..c7e7a2c54 100644 --- a/tap/go.sum +++ b/tap/go.sum @@ -1,3 +1,5 @@ +github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4 h1:NJOOlc6ZJjix0A1rAU+nxruZtR8KboG1848yqpIUo4M= +github.com/bradleyfalzon/tlsx v0.0.0-20170624122154-28fd0e59bac4/go.mod h1:DQPxZS994Ld1Y8uwnJT+dRL04XPD0cElP/pHH/zEBHM= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= diff --git a/tap/har_writer.go b/tap/har_writer.go index 25ff4f5f9..a20dbfb99 100644 --- a/tap/har_writer.go +++ b/tap/har_writer.go @@ -43,7 +43,7 @@ func openNewHarFile(filename string) *HarFile { } type HarFile struct { - file *os.File + file *os.File entryCount int } @@ -105,13 +105,13 @@ func NewEntry(request *http.Request, requestTime time.Time, response *http.Respo harEntry := har.Entry{ StartedDateTime: time.Now().UTC(), - Time: totalTime, - Request: harRequest, - Response: harResponse, - Cache: &har.Cache{}, + Time: totalTime, + Request: harRequest, + Response: harResponse, + Cache: &har.Cache{}, Timings: &har.Timings{ - Send: -1, - Wait: -1, + Send: -1, + Wait: -1, Receive: totalTime, }, } @@ -155,14 +155,14 @@ func (f *HarFile) Close() { } } -func (f*HarFile) writeHeader() { +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 { log.Panicf("Failed to write header to output file: %s (%v,%+v)", err, err, err) } } -func (f*HarFile) writeTrailer() { +func (f *HarFile) writeTrailer() { trailer := []byte("]}}") if _, err := f.file.Write(trailer); err != nil { log.Panicf("Failed to write trailer to output file: %s (%v,%+v)", err, err, err) @@ -172,26 +172,27 @@ func (f*HarFile) writeTrailer() { 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), + MaxEntries: maxEntries, + PairChan: make(chan *PairChanItem), + OutChan: make(chan *OutputChannelItem, 1000), + currentFile: nil, + done: make(chan bool), } } type OutputChannelItem struct { - HarEntry *har.Entry - ConnectionInfo *ConnectionInfo + HarEntry *har.Entry + ConnectionInfo *ConnectionInfo + ValidationRulesChecker string } type HarWriter struct { OutputDirPath string - MaxEntries int - PairChan chan *PairChanItem - OutChan chan *OutputChannelItem - currentFile *HarFile - done chan bool + 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, connectionInfo *ConnectionInfo) { @@ -240,7 +241,7 @@ func (hw *HarWriter) Start() { hw.closeFile() } hw.done <- true - } () + }() } func (hw *HarWriter) Stop() { diff --git a/tap/http_reader.go b/tap/http_reader.go index e38885acd..6f059afaa 100644 --- a/tap/http_reader.go +++ b/tap/http_reader.go @@ -5,21 +5,25 @@ import ( "bytes" "encoding/hex" "fmt" + "github.com/bradleyfalzon/tlsx" "io" "io/ioutil" "net/http" + "strconv" "sync" "time" ) +const checkTLSPacketAmount = 100 + type httpReaderDataMsg struct { bytes []byte timestamp time.Time } type tcpID struct { - srcIP string - dstIP string + srcIP string + dstIP string srcPort string dstPort string } @@ -42,28 +46,44 @@ func (tid *tcpID) String() string { * Implements io.Reader interface (Read) */ type httpReader struct { - ident string - tcpID tcpID - isClient bool - isHTTP2 bool - isOutgoing 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 + ident string + tcpID tcpID + isClient bool + isHTTP2 bool + isOutgoing 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 + packetsSeen uint + outboundLinkWriter *OutboundLinkWriter } 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 len(h.data) > 0 { + h.packetsSeen += 1 + } + if h.packetsSeen < checkTLSPacketAmount && len(msg.bytes) > 5 { // packets with less than 5 bytes cause tlsx to panic + clientHello := tlsx.ClientHello{} + err := clientHello.Unmarshall(msg.bytes) + if err == nil { + fmt.Printf("Detected TLS client hello with SNI %s\n", clientHello.SNI) + numericPort, _ := strconv.Atoi(h.tcpID.dstPort) + h.outboundLinkWriter.WriteOutboundLink(h.tcpID.srcIP, h.tcpID.dstIP, numericPort, clientHello.SNI, TLSProtocol) + } + } } if !ok || len(h.data) == 0 { return 0, io.EOF diff --git a/tap/outboundlinks.go b/tap/outboundlinks.go index 0cb60bbd9..6e8a194b7 100644 --- a/tap/outboundlinks.go +++ b/tap/outboundlinks.go @@ -1,9 +1,17 @@ package tap +type OutboundLinkProtocol string + +const ( + TLSProtocol OutboundLinkProtocol = "tls" +) + type OutboundLink struct { Src string DstIP string DstPort int + SuggestedResolvedName string + SuggestedProtocol OutboundLinkProtocol } func NewOutboundLinkWriter() *OutboundLinkWriter { @@ -16,11 +24,13 @@ type OutboundLinkWriter struct { OutChan chan *OutboundLink } -func (olw *OutboundLinkWriter) WriteOutboundLink(src string, DstIP string, DstPort int) { +func (olw *OutboundLinkWriter) WriteOutboundLink(src string, DstIP string, DstPort int, SuggestedResolvedName string, SuggestedProtocol OutboundLinkProtocol) { olw.OutChan <- &OutboundLink{ Src: src, DstIP: DstIP, DstPort: DstPort, + SuggestedResolvedName: SuggestedResolvedName, + SuggestedProtocol: SuggestedProtocol, } } diff --git a/tap/passive_tapper.go b/tap/passive_tapper.go index 0625c666d..79274029f 100644 --- a/tap/passive_tapper.go +++ b/tap/passive_tapper.go @@ -33,10 +33,10 @@ import ( const AppPortsEnvVar = "APP_PORTS" 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 maxHTTP2DataLenDefault = 1 * 1024 * 1024 // 1MB const cleanPeriod = time.Second * 10 -var remoteOnlyOutboundPorts = []int { 80, 443 } + +var remoteOnlyOutboundPorts = []int{80, 443} func parseAppPorts(appPortsList string) []int { ports := make([]int, 0) @@ -51,15 +51,15 @@ func parseAppPorts(appPortsList string) []int { return ports } -var maxcount = flag.Int("c", -1, "Only grab this many packets, then exit") +var maxcount = flag.Int64("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 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") @@ -69,7 +69,7 @@ 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 hexdump = flag.Bool("dump", false, "Dump HTTP request/response as hex") // global var hexdumppkt = flag.Bool("dumppkt", false, "Dump packet as hex") // capture @@ -88,7 +88,7 @@ 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 reqResMatcher = createResponseRequestMatcher() // global var statsTracker = StatsTracker{} // global @@ -118,8 +118,8 @@ var outputLevel int var errorsMap map[string]uint var errorsMapMutex sync.Mutex var nErrors uint -var ownIps []string // global -var hostMode bool // global +var ownIps []string // global +var hostMode bool // global /* minOutputLevel: Error will be printed only if outputLevel is above this value * t: key for errorsMap (counting errors) @@ -175,6 +175,10 @@ type Context struct { CaptureInfo gopacket.CaptureInfo } +func GetStats() AppStats { + return statsTracker.appStats +} + func (c *Context) GetCaptureInfo() gopacket.CaptureInfo { return c.CaptureInfo } @@ -197,6 +201,37 @@ func StartPassiveTapper(opts *TapOpts) (<-chan *OutputChannelItem, <-chan *Outbo return nil, outboundLinkWriter.OutChan } +func startMemoryProfiler() { + dirname := "/app/pprof" + rlog.Info("Profiling is on, results will be written to %s", dirname) + go func() { + if _, err := os.Stat(dirname); os.IsNotExist(err) { + if err := os.Mkdir(dirname, 0777); err != nil { + log.Fatal("could not create directory for profile: ", err) + } + } + + for true { + t := time.Now() + + filename := fmt.Sprintf("%s/%s__mem.prof", dirname, t.Format("15_04_05")) + + rlog.Info("Writing memory profile to %s\n", filename) + + f, err := os.Create(filename) + if err != nil { + log.Fatal("could not create memory profile: ", err) + } + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatal("could not write memory profile: ", err) + } + _ = f.Close() + time.Sleep(time.Minute) + } + }() +} + func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWriter) { log.SetFlags(log.LstdFlags | log.LUTC | log.Lshortfile) @@ -305,19 +340,23 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr source.Lazy = *lazy source.NoCopy = true rlog.Info("Starting to read packets") - count := 0 - bytes := int64(0) - start := time.Now() + statsTracker.setStartTime(time.Now()) defragger := ip4defrag.NewIPv4Defragmenter() streamFactory := &tcpStreamFactory{ - doHTTP: !*nohttp, - harWriter: harWriter, + doHTTP: !*nohttp, + harWriter: harWriter, outbountLinkWriter: outboundLinkWriter, - } streamPool := reassembly.NewStreamPool(streamFactory) assembler := reassembly.NewAssembler(streamPool) + + maxBufferedPagesTotal := GetMaxBufferedPagesPerConnection() + maxBufferedPagesPerConnection := GetMaxBufferedPagesTotal() + rlog.Infof("Assembler options: maxBufferedPagesTotal=%d, maxBufferedPagesPerConnection=%d", maxBufferedPagesTotal, maxBufferedPagesPerConnection) + assembler.AssemblerOptions.MaxBufferedPagesTotal = maxBufferedPagesTotal + assembler.AssemblerOptions.MaxBufferedPagesPerConnection = maxBufferedPagesPerConnection + var assemblerMutex sync.Mutex signalChan := make(chan os.Signal, 1) @@ -325,10 +364,10 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr staleConnectionTimeout := time.Second * time.Duration(*staleTimeoutSeconds) cleaner := Cleaner{ - assembler: assembler, - assemblerMutex: &assemblerMutex, - matcher: &reqResMatcher, - cleanPeriod: cleanPeriod, + assembler: assembler, + assemblerMutex: &assemblerMutex, + matcher: &reqResMatcher, + cleanPeriod: cleanPeriod, connectionTimeout: staleConnectionTimeout, } cleaner.start() @@ -346,9 +385,9 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr errorsSummery := fmt.Sprintf("%v", errorsMap) errorsMapMutex.Unlock() log.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v) - Errors Summary: %s", - count, - bytes, - time.Since(start), + statsTracker.appStats.TotalPacketsCount, + statsTracker.appStats.TotalProcessedBytes, + time.Since(statsTracker.appStats.StartTime), nErrors, errorMapLen, errorsSummery, @@ -366,22 +405,26 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr // Since the last print cleanStats := cleaner.dumpStats() - appStats := statsTracker.dumpStats() + matchedMessages := statsTracker.dumpStats() log.Printf( "flushed connections %d, closed connections: %d, deleted messages: %d, matched messages: %d", cleanStats.flushed, cleanStats.closed, cleanStats.deleted, - appStats.matchedMessages, + matchedMessages, ) } }() + if GetMemoryProfilingEnabled() { + startMemoryProfiler() + } + for packet := range source.Packets() { - count++ - rlog.Debugf("PACKET #%d", count) + packetsCount := statsTracker.incPacketsCount() + rlog.Debugf("PACKET #%d", packetsCount) data := packet.Data() - bytes += int64(len(data)) + statsTracker.updateProcessedSize(int64(len(data))) if *hexdumppkt { rlog.Debugf("Packet content (%d/0x%x) - %s", len(data), len(data), hex.Dump(data)) } @@ -432,12 +475,17 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr assemblerMutex.Unlock() } - done := *maxcount > 0 && count >= *maxcount + done := *maxcount > 0 && statsTracker.appStats.TotalPacketsCount >= *maxcount if done { errorsMapMutex.Lock() errorMapLen := len(errorsMap) errorsMapMutex.Unlock() - log.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)", count, bytes, time.Since(start), nErrors, errorMapLen) + log.Printf("Processed %v packets (%v bytes) in %v (errors: %v, errTypes:%v)", + statsTracker.appStats.TotalPacketsCount, + statsTracker.appStats.TotalProcessedBytes, + time.Since(statsTracker.appStats.StartTime), + nErrors, + errorMapLen) } select { case <-signalChan: @@ -494,4 +542,5 @@ func startPassiveTapper(harWriter *HarWriter, outboundLinkWriter *OutboundLinkWr for e := range errorsMap { log.Printf(" %s:\t\t%d", e, errorsMap[e]) } + log.Printf("AppStats: %v", GetStats()) } diff --git a/tap/settings.go b/tap/settings.go index cf89dd345..7c8636239 100644 --- a/tap/settings.go +++ b/tap/settings.go @@ -1,5 +1,18 @@ package tap +import ( + "os" + "strconv" +) + +const ( + MemoryProfilingEnabledEnvVarName = "MEMORY_PROFILING_ENABLED" + MaxBufferedPagesTotalEnvVarName = "MAX_BUFFERED_PAGES_TOTAL" + MaxBufferedPagesPerConnectionEnvVarName = "MAX_BUFFERED_PAGES_PER_CONNECTION" + MaxBufferedPagesTotalDefaultValue = 5000 + MaxBufferedPagesPerConnectionDefaultValue = 5000 +) + type globalSettings struct { filterPorts []int filterAuthorities []string @@ -29,3 +42,23 @@ func GetFilterIPs() []string { copy(addresses, gSettings.filterAuthorities) return addresses } + +func GetMaxBufferedPagesTotal() int { + valueFromEnv, err := strconv.Atoi(os.Getenv(MaxBufferedPagesTotalEnvVarName)) + if err != nil { + return MaxBufferedPagesTotalDefaultValue + } + return valueFromEnv +} + +func GetMaxBufferedPagesPerConnection() int { + valueFromEnv, err := strconv.Atoi(os.Getenv(MaxBufferedPagesPerConnectionEnvVarName)) + if err != nil { + return MaxBufferedPagesPerConnectionDefaultValue + } + return valueFromEnv +} + +func GetMemoryProfilingEnabled() bool { + return os.Getenv(MemoryProfilingEnabledEnvVarName) == "1" +} diff --git a/tap/stats_tracker.go b/tap/stats_tracker.go index ecee29202..c659e150c 100644 --- a/tap/stats_tracker.go +++ b/tap/stats_tracker.go @@ -2,34 +2,54 @@ package tap import ( "sync" + "time" ) type AppStats struct { - matchedMessages int + StartTime time.Time `json:"startTime"` + MatchedMessages int `json:"matchedMessages"` + TotalPacketsCount int64 `json:"totalPacketsCount"` + TotalProcessedBytes int64 `json:"totalProcessedBytes"` + TotalMatchedMessages int64 `json:"totalMatchedMessages"` } type StatsTracker struct { - stats AppStats - statsMutex sync.Mutex + appStats AppStats + matchedMessagesMutex sync.Mutex + totalPacketsCountMutex sync.Mutex + totalProcessedSizeMutex sync.Mutex } func (st *StatsTracker) incMatchedMessages() { - st.statsMutex.Lock() - st.stats.matchedMessages++ - st.statsMutex.Unlock() + st.matchedMessagesMutex.Lock() + st.appStats.MatchedMessages++ + st.appStats.TotalMatchedMessages++ + st.matchedMessagesMutex.Unlock() } -func (st *StatsTracker) dumpStats() AppStats { - st.statsMutex.Lock() - - stats := AppStats{ - matchedMessages: st.stats.matchedMessages, - } - - st.stats.matchedMessages = 0 - - st.statsMutex.Unlock() - - return stats +func (st *StatsTracker) incPacketsCount() int64 { + st.totalPacketsCountMutex.Lock() + st.appStats.TotalPacketsCount++ + currentPacketsCount := st.appStats.TotalPacketsCount + st.totalPacketsCountMutex.Unlock() + return currentPacketsCount } +func (st *StatsTracker) updateProcessedSize(size int64) { + st.totalProcessedSizeMutex.Lock() + st.appStats.TotalProcessedBytes += size + st.totalProcessedSizeMutex.Unlock() +} + +func (st *StatsTracker) setStartTime(startTime time.Time) { + st.appStats.StartTime = startTime +} + +func (st *StatsTracker) dumpStats() int { + st.matchedMessagesMutex.Lock() + matchedMessages := st.appStats.MatchedMessages + st.appStats.MatchedMessages = 0 + st.matchedMessagesMutex.Unlock() + + return matchedMessages +} diff --git a/tap/tcp_stream_factory.go b/tap/tcp_stream_factory.go index 2acb2ed9a..c03600584 100644 --- a/tap/tcp_stream_factory.go +++ b/tap/tcp_stream_factory.go @@ -33,7 +33,7 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T dstPort := int(tcp.DstPort) if factory.shouldNotifyOnOutboundLink(dstIp, dstPort) { - factory.outbountLinkWriter.WriteOutboundLink(net.Src().String(), dstIp, dstPort) + factory.outbountLinkWriter.WriteOutboundLink(net.Src().String(), dstIp, dstPort, "", "") } props := factory.getStreamProps(srcIp, dstIp, dstPort) isHTTP := props.isTapTarget @@ -57,11 +57,12 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T srcPort: transport.Src().String(), dstPort: transport.Dst().String(), }, - hexdump: *hexdump, - parent: stream, - isClient: true, - isOutgoing: props.isOutgoing, - harWriter: factory.harWriter, + hexdump: *hexdump, + parent: stream, + isClient: true, + isOutgoing: props.isOutgoing, + harWriter: factory.harWriter, + outboundLinkWriter: factory.outbountLinkWriter, } stream.server = httpReader{ msgQueue: make(chan httpReaderDataMsg), @@ -72,10 +73,11 @@ func (factory *tcpStreamFactory) New(net, transport gopacket.Flow, tcp *layers.T srcPort: transport.Dst().String(), dstPort: transport.Src().String(), }, - hexdump: *hexdump, - parent: stream, - isOutgoing: props.isOutgoing, - harWriter: factory.harWriter, + hexdump: *hexdump, + parent: stream, + isOutgoing: props.isOutgoing, + harWriter: factory.harWriter, + outboundLinkWriter: factory.outbountLinkWriter, } factory.wg.Add(2) // Start reading from channels stream.client.bytes and stream.server.bytes @@ -131,6 +133,5 @@ func (factory *tcpStreamFactory) shouldNotifyOnOutboundLink(dstIP string, dstPor type streamProps struct { isTapTarget bool - isOutgoing bool + isOutgoing bool } - diff --git a/ui/package-lock.json b/ui/package-lock.json index 759012d82..9ce39e95f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@material-ui/core": "^4.11.3", + "@material-ui/lab": "^4.0.0-alpha.60", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^12.8.3", @@ -16,6 +17,7 @@ "@types/node": "^12.20.10", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", + "axios": "^0.21.1", "node-sass": "^5.0.0", "numeral": "^2.0.6", "protobuf-decoder": "^0.1.0", @@ -66,6 +68,10 @@ }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, "node_modules/@babel/core/node_modules/semver": { @@ -112,6 +118,9 @@ "@babel/helper-validator-option": "^7.12.17", "browserslist": "^4.14.5", "semver": "^6.3.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { @@ -132,6 +141,9 @@ "@babel/helper-optimise-call-expression": "^7.12.13", "@babel/helper-replace-supers": "^7.13.0", "@babel/helper-split-export-declaration": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { @@ -141,6 +153,9 @@ "dependencies": { "@babel/helper-annotate-as-pure": "^7.12.13", "regexpu-core": "^4.7.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-define-polyfill-provider": { @@ -156,6 +171,9 @@ "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { @@ -350,6 +368,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.13.12" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, "node_modules/@babel/plugin-proposal-async-generator-functions": { @@ -360,6 +381,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-remap-async-to-generator": "^7.13.0", "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-class-properties": { @@ -369,6 +393,9 @@ "dependencies": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-decorators": { @@ -379,6 +406,9 @@ "@babel/helper-create-class-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-decorators": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-dynamic-import": { @@ -388,6 +418,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-export-namespace-from": { @@ -397,6 +430,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.12.13", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-json-strings": { @@ -406,6 +442,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-logical-assignment-operators": { @@ -415,6 +454,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { @@ -424,6 +466,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-numeric-separator": { @@ -433,6 +478,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.12.13", "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-object-rest-spread": { @@ -445,6 +493,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-optional-catch-binding": { @@ -454,6 +505,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-optional-chaining": { @@ -464,6 +518,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-private-methods": { @@ -473,6 +530,9 @@ "dependencies": { "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-proposal-unicode-property-regex": { @@ -485,6 +545,9 @@ }, "engines": { "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-async-generators": { @@ -493,6 +556,9 @@ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-bigint": { @@ -501,6 +567,9 @@ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-class-properties": { @@ -509,6 +578,9 @@ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-decorators": { @@ -517,6 +589,9 @@ "integrity": "sha512-Rw6aIXGuqDLr6/LoBBYE57nKOzQpz/aDkKlMqEwH+Vp0MXbG6H/TfRjaY343LKxzAKAMXIHsQ8JzaZKuDZ9MwA==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-dynamic-import": { @@ -525,6 +600,9 @@ "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-export-namespace-from": { @@ -533,6 +611,9 @@ "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-flow": { @@ -541,6 +622,9 @@ "integrity": "sha512-J/RYxnlSLXZLVR7wTRsozxKT8qbsx1mNKJzXEEjQ0Kjx1ZACcyHgbanNWNCFtc36IzuWhYWPpvJFFoexoOWFmA==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-import-meta": { @@ -549,6 +633,9 @@ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-json-strings": { @@ -557,6 +644,9 @@ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-jsx": { @@ -565,6 +655,9 @@ "integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { @@ -573,6 +666,9 @@ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { @@ -581,6 +677,9 @@ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-numeric-separator": { @@ -589,6 +688,9 @@ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-object-rest-spread": { @@ -597,6 +699,9 @@ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { @@ -605,6 +710,9 @@ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-optional-chaining": { @@ -613,6 +721,9 @@ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-top-level-await": { @@ -621,6 +732,9 @@ "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-typescript": { @@ -629,6 +743,9 @@ "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-arrow-functions": { @@ -637,6 +754,9 @@ "integrity": "sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-async-to-generator": { @@ -647,6 +767,9 @@ "@babel/helper-module-imports": "^7.12.13", "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-remap-async-to-generator": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { @@ -655,6 +778,9 @@ "integrity": "sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-block-scoping": { @@ -663,6 +789,9 @@ "integrity": "sha512-ad3PHUxGnfWF4Efd3qFuznEtZKoBp0spS+DgqzVzRPV7urEBvPLue3y2j80w4Jf2YLzZHj8TOv/Lmvdmh3b2xg==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-classes": { @@ -677,6 +806,9 @@ "@babel/helper-replace-supers": "^7.13.0", "@babel/helper-split-export-declaration": "^7.12.13", "globals": "^11.1.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-computed-properties": { @@ -685,6 +817,9 @@ "integrity": "sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-destructuring": { @@ -693,6 +828,9 @@ "integrity": "sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-dotall-regex": { @@ -702,6 +840,9 @@ "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.12.13", "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-duplicate-keys": { @@ -710,6 +851,9 @@ "integrity": "sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { @@ -719,6 +863,9 @@ "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.12.13", "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-flow-strip-types": { @@ -728,6 +875,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-flow": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-for-of": { @@ -736,6 +886,9 @@ "integrity": "sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-function-name": { @@ -745,6 +898,9 @@ "dependencies": { "@babel/helper-function-name": "^7.12.13", "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-literals": { @@ -753,6 +909,9 @@ "integrity": "sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-member-expression-literals": { @@ -761,6 +920,9 @@ "integrity": "sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-amd": { @@ -771,6 +933,9 @@ "@babel/helper-module-transforms": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0", "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-commonjs": { @@ -782,6 +947,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-simple-access": "^7.12.13", "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-systemjs": { @@ -794,6 +962,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-validator-identifier": "^7.12.11", "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-umd": { @@ -803,6 +974,9 @@ "dependencies": { "@babel/helper-module-transforms": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { @@ -811,6 +985,9 @@ "integrity": "sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA==", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-new-target": { @@ -819,6 +996,9 @@ "integrity": "sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-object-super": { @@ -828,6 +1008,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.12.13", "@babel/helper-replace-supers": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-parameters": { @@ -836,6 +1019,9 @@ "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-property-literals": { @@ -844,6 +1030,9 @@ "integrity": "sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-constant-elements": { @@ -852,6 +1041,9 @@ "integrity": "sha512-SNJU53VM/SjQL0bZhyU+f4kJQz7bQQajnrZRSaU21hruG/NWY41AEM9AWXeXX90pYr/C2yAmTgI6yW3LlLrAUQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-display-name": { @@ -860,6 +1052,9 @@ "integrity": "sha512-MprESJzI9O5VnJZrL7gg1MpdqmiFcUv41Jc7SahxYsNP2kDkFqClxxTZq+1Qv4AFCamm+GXMRDQINNn+qrxmiA==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx": { @@ -872,6 +1067,9 @@ "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-jsx": "^7.12.13", "@babel/types": "^7.13.12" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-development": { @@ -880,6 +1078,9 @@ "integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==", "dependencies": { "@babel/plugin-transform-react-jsx": "^7.12.17" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-self": { @@ -888,6 +1089,9 @@ "integrity": "sha512-FXYw98TTJ125GVCCkFLZXlZ1qGcsYqNQhVBQcZjyrwf8FEUtVfKIoidnO8S0q+KBQpDYNTmiGo1gn67Vti04lQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-source": { @@ -896,6 +1100,9 @@ "integrity": "sha512-O5JJi6fyfih0WfDgIJXksSPhGP/G0fQpfxYy87sDc+1sFmsCS6wr3aAn+whbzkhbjtq4VMqLRaSzR6IsshIC0Q==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { @@ -905,6 +1112,9 @@ "dependencies": { "@babel/helper-annotate-as-pure": "^7.10.4", "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-regenerator": { @@ -913,6 +1123,9 @@ "integrity": "sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ==", "dependencies": { "regenerator-transform": "^0.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-reserved-words": { @@ -921,6 +1134,9 @@ "integrity": "sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-runtime": { @@ -932,6 +1148,9 @@ "@babel/helper-plugin-utils": "^7.10.4", "resolve": "^1.8.1", "semver": "^5.5.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { @@ -948,6 +1167,9 @@ "integrity": "sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-spread": { @@ -957,6 +1179,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-sticky-regex": { @@ -965,6 +1190,9 @@ "integrity": "sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-template-literals": { @@ -973,6 +1201,9 @@ "integrity": "sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==", "dependencies": { "@babel/helper-plugin-utils": "^7.13.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-typeof-symbol": { @@ -981,6 +1212,9 @@ "integrity": "sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-typescript": { @@ -991,6 +1225,9 @@ "@babel/helper-create-class-features-plugin": "^7.13.0", "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-typescript": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-escapes": { @@ -999,6 +1236,9 @@ "integrity": "sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw==", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-regex": { @@ -1008,6 +1248,9 @@ "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.12.13", "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-env": { @@ -1084,6 +1327,9 @@ "babel-plugin-polyfill-regenerator": "^0.2.0", "core-js-compat": "^3.9.0", "semver": "^6.3.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-env/node_modules/semver": { @@ -1104,6 +1350,9 @@ "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-react": { @@ -1117,6 +1366,9 @@ "@babel/plugin-transform-react-jsx": "^7.13.12", "@babel/plugin-transform-react-jsx-development": "^7.12.17", "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-typescript": { @@ -1126,6 +1378,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-transform-typescript": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { @@ -1245,6 +1500,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/eslintrc/node_modules/ignore": { @@ -1258,22 +1516,26 @@ "node_modules/@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==" + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "deprecated": "Moved to 'npm install @sideway/address'" }, "node_modules/@hapi/bourne": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", - "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==" + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "deprecated": "This version has been deprecated and is no longer supported or maintained" }, "node_modules/@hapi/hoek": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==" + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "deprecated": "This version has been deprecated and is no longer supported or maintained" }, "node_modules/@hapi/joi": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "deprecated": "Switch to 'npm install joi'", "dependencies": { "@hapi/address": "2.x.x", "@hapi/bourne": "1.x.x", @@ -1285,6 +1547,7 @@ "version": "3.1.6", "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", "dependencies": { "@hapi/hoek": "^8.3.0" } @@ -1353,6 +1616,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/console/node_modules/chalk": { @@ -1365,6 +1631,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jest/console/node_modules/color-convert": { @@ -1449,6 +1718,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/core/node_modules/chalk": { @@ -1461,6 +1733,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jest/core/node_modules/color-convert": { @@ -1530,6 +1805,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@jest/core/node_modules/read-pkg/node_modules/type-fest": { @@ -1640,6 +1918,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/reporters/node_modules/chalk": { @@ -1652,6 +1933,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jest/reporters/node_modules/color-convert": { @@ -1721,6 +2005,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@jest/reporters/node_modules/read-pkg/node_modules/type-fest": { @@ -1834,6 +2121,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/transform/node_modules/chalk": { @@ -1846,6 +2136,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jest/transform/node_modules/color-convert": { @@ -1915,6 +2208,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/types/node_modules/chalk": { @@ -1927,6 +2223,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@jest/types/node_modules/color-convert": { @@ -1965,14 +2264,14 @@ } }, "node_modules/@material-ui/core": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", - "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.2.tgz", + "integrity": "sha512-Q1npB8V73IC+eV2X6as+g71MpEGQwqKHUI2iujY62npk35V8nMx/bUXAHjv5kKG1BZ8s8XUWoG6s/VkjYPjjQA==", "dependencies": { "@babel/runtime": "^7.4.4", - "@material-ui/styles": "^4.11.3", - "@material-ui/system": "^4.11.3", - "@material-ui/types": "^5.1.0", + "@material-ui/styles": "^4.11.4", + "@material-ui/system": "^4.12.1", + "@material-ui/types": "5.1.0", "@material-ui/utils": "^4.11.2", "@types/react-transition-group": "^4.2.0", "clsx": "^1.0.4", @@ -1984,16 +2283,56 @@ }, "engines": { "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/lab": { + "version": "4.0.0-alpha.60", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz", + "integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.12.1", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@material-ui/styles": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz", - "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", + "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", "dependencies": { "@babel/runtime": "^7.4.4", "@emotion/hash": "^0.8.0", - "@material-ui/types": "^5.1.0", + "@material-ui/types": "5.1.0", "@material-ui/utils": "^4.11.2", "clsx": "^1.0.4", "csstype": "^2.5.2", @@ -2010,6 +2349,20 @@ }, "engines": { "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@material-ui/styles/node_modules/csstype": { @@ -2018,9 +2371,9 @@ "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" }, "node_modules/@material-ui/system": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", - "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", + "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/utils": "^4.11.2", @@ -2029,6 +2382,20 @@ }, "engines": { "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/@material-ui/system/node_modules/csstype": { @@ -2039,7 +2406,15 @@ "node_modules/@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } }, "node_modules/@material-ui/utils": { "version": "4.11.2", @@ -2052,6 +2427,10 @@ }, "engines": { "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -2123,6 +2502,36 @@ }, "engines": { "node": ">= 10.x" + }, + "peerDependencies": { + "@types/webpack": "4.x", + "react-refresh": ">=0.8.3 <0.10.0", + "sockjs-client": "^1.4.0", + "type-fest": "^0.13.1", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { @@ -2146,6 +2555,9 @@ }, "engines": { "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" } }, "node_modules/@rollup/plugin-replace": { @@ -2155,6 +2567,9 @@ "dependencies": { "@rollup/pluginutils": "^3.1.0", "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" } }, "node_modules/@rollup/pluginutils": { @@ -2168,6 +2583,9 @@ }, "engines": { "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" } }, "node_modules/@rollup/pluginutils/node_modules/@types/estree": { @@ -2206,6 +2624,10 @@ "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { @@ -2214,6 +2636,10 @@ "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { @@ -2222,6 +2648,10 @@ "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { @@ -2230,6 +2660,10 @@ "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-svg-dynamic-title": { @@ -2238,6 +2672,10 @@ "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-svg-em-dimensions": { @@ -2246,6 +2684,10 @@ "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-transform-react-native-svg": { @@ -2254,6 +2696,10 @@ "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-plugin-transform-svg-component": { @@ -2262,6 +2708,10 @@ "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/babel-preset": { @@ -2280,6 +2730,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/core": { @@ -2293,6 +2747,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/hast-util-to-babel-ast": { @@ -2304,6 +2762,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/plugin-jsx": { @@ -2318,6 +2780,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/plugin-svgo": { @@ -2331,6 +2797,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/webpack": { @@ -2349,6 +2819,10 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@testing-library/dom": { @@ -2378,6 +2852,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@testing-library/dom/node_modules/chalk": { @@ -2390,6 +2867,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@testing-library/dom/node_modules/color-convert": { @@ -2456,6 +2936,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@testing-library/jest-dom/node_modules/chalk": { @@ -2542,6 +3025,10 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" } }, "node_modules/@testing-library/user-event": { @@ -2554,6 +3041,9 @@ "engines": { "node": ">=10", "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, "node_modules/@types/anymatch": { @@ -2873,6 +3363,19 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/experimental-utils": { @@ -2889,6 +3392,13 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" } }, "node_modules/@typescript-eslint/parser": { @@ -2903,6 +3413,18 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/scope-manager": { @@ -2915,6 +3437,10 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/types": { @@ -2923,6 +3449,10 @@ "integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==", "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { @@ -2940,6 +3470,15 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { @@ -2952,6 +3491,10 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@webassemblyjs/ast": { @@ -3166,7 +3709,10 @@ "node_modules/acorn-jsx": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==" + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, "node_modules/acorn-walk": { "version": "7.2.0", @@ -3217,17 +3763,27 @@ "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ajv-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "peerDependencies": { + "ajv": ">=5.0.0" + } }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } }, "node_modules/alphanum-sort": { "version": "1.0.2", @@ -3259,6 +3815,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-escapes/node_modules/type-fest": { @@ -3267,6 +3826,9 @@ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-html": { @@ -3422,6 +3984,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-union": { @@ -3459,6 +4024,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array.prototype.flatmap": { @@ -3473,6 +4041,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/arrify": { @@ -3628,6 +4199,10 @@ }, "bin": { "autoprefixer": "bin/autoprefixer" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" } }, "node_modules/aws-sign2": { @@ -3651,6 +4226,14 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -3660,6 +4243,7 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -3670,6 +4254,9 @@ }, "engines": { "node": ">=6" + }, + "peerDependencies": { + "eslint": ">= 4.12.1" } }, "node_modules/babel-eslint/node_modules/eslint-visitor-keys": { @@ -3707,6 +4294,9 @@ }, "engines": { "node": ">= 10.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/babel-jest/node_modules/ansi-styles": { @@ -3718,6 +4308,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/babel-jest/node_modules/chalk": { @@ -3730,6 +4323,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/babel-jest/node_modules/color-convert": { @@ -3780,6 +4376,10 @@ }, "engines": { "node": ">= 6.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" } }, "node_modules/babel-loader/node_modules/json5": { @@ -3871,7 +4471,10 @@ "node_modules/babel-plugin-named-asset-import": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", - "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==" + "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", + "peerDependencies": { + "@babel/core": "^7.1.0" + } }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.2.0", @@ -3881,6 +4484,9 @@ "@babel/compat-data": "^7.13.11", "@babel/helper-define-polyfill-provider": "^0.2.0", "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { @@ -3898,6 +4504,9 @@ "dependencies": { "@babel/helper-define-polyfill-provider": "^0.2.0", "core-js-compat": "^3.9.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-polyfill-regenerator": { @@ -3906,6 +4515,9 @@ "integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-plugin-syntax-object-rest-spread": { @@ -3944,6 +4556,9 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/babel-preset-jest": { @@ -3956,6 +4571,9 @@ }, "engines": { "node": ">= 10.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/babel-preset-react-app": { @@ -3987,6 +4605,9 @@ "dependencies": { "@babel/helper-create-class-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { @@ -3996,6 +4617,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-numeric-separator": { @@ -4005,6 +4629,9 @@ "dependencies": { "@babel/helper-plugin-utils": "^7.10.4", "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-optional-chaining": { @@ -4015,6 +4642,9 @@ "@babel/helper-plugin-utils": "^7.10.4", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/plugin-transform-react-display-name": { @@ -4023,6 +4653,9 @@ "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/preset-env": { @@ -4096,6 +4729,9 @@ "@babel/types": "^7.12.1", "core-js-compat": "^3.6.2", "semver": "^5.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/preset-react": { @@ -4110,6 +4746,9 @@ "@babel/plugin-transform-react-jsx-self": "^7.12.1", "@babel/plugin-transform-react-jsx-source": "^7.12.1", "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/babel-preset-react-app/node_modules/@babel/runtime": { @@ -4140,7 +4779,9 @@ "node_modules/babel-runtime/node_modules/core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "hasInstallScript": true }, "node_modules/babel-runtime/node_modules/regenerator-runtime": { "version": "0.11.1", @@ -4226,7 +4867,21 @@ "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/batch": { "version": "0.6.1", @@ -4450,7 +5105,21 @@ "node_modules/browserify-sign/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/browserify-zlib": { "version": "0.2.0", @@ -4476,6 +5145,10 @@ }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" } }, "node_modules/bser": { @@ -4517,6 +5190,9 @@ "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/builtin-status-codes": { @@ -4596,6 +5272,9 @@ "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/caller-callsite": { @@ -4656,6 +5335,9 @@ "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/camelcase-keys": { @@ -4742,17 +5424,29 @@ "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==" + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/character-reference-invalid": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/check-types": { "version": "11.1.2", @@ -4767,7 +5461,6 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -4776,6 +5469,9 @@ }, "engines": { "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" } }, "node_modules/chownr": { @@ -4991,7 +5687,11 @@ "node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/commander": { "version": "4.1.1", @@ -5228,7 +5928,12 @@ "node_modules/core-js": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.10.2.tgz", - "integrity": "sha512-W+2oVYeNghuBr3yTzZFQ5rfmjZtYB/Ubg87R5YOmlGrIb+Uw9f7qjUbhsj+/EkXhcV7eOD3jiM4+sgraX3FZUw==" + "integrity": "sha512-W+2oVYeNghuBr3yTzZFQ5rfmjZtYB/Ubg87R5YOmlGrIb+Uw9f7qjUbhsj+/EkXhcV7eOD3jiM4+sgraX3FZUw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, "node_modules/core-js-compat": { "version": "3.10.2", @@ -5237,6 +5942,10 @@ "dependencies": { "browserslist": "^4.16.4", "semver": "7.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, "node_modules/core-js-compat/node_modules/semver": { @@ -5250,7 +5959,12 @@ "node_modules/core-js-pure": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.10.2.tgz", - "integrity": "sha512-uu18pVHQ21n4mzfuSlCXpucu5VKsck3j2m5fjrBOBqqdgWAxwdCgUuGWj6cDDPN1zLj/qtiqKvBMxWgDeeu49Q==" + "integrity": "sha512-uu18pVHQ21n4mzfuSlCXpucu5VKsck3j2m5fjrBOBqqdgWAxwdCgUuGWj6cDDPN1zLj/qtiqKvBMxWgDeeu49Q==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } }, "node_modules/core-util-is": { "version": "1.0.2", @@ -5467,6 +6181,13 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.27.0 || ^5.0.0" } }, "node_modules/css-prefers-color-scheme": { @@ -5534,6 +6255,9 @@ "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", "engines": { "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, "node_modules/css.escape": { @@ -5825,6 +6549,11 @@ }, "engines": { "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/decamelize": { @@ -5864,6 +6593,9 @@ "object-is": "^1.0.1", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/deep-is": { @@ -6203,7 +6935,13 @@ "node_modules/dom-serializer/node_modules/domelementtype": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, "node_modules/domain-browser": { "version": "1.2.0", @@ -6349,6 +7087,7 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "hasInstallScript": true, "engines": { "node": ">=0.10.0" } @@ -6383,6 +7122,9 @@ "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, "node_modules/emoji-regex": { @@ -6475,7 +7217,10 @@ "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/env-paths": { "version": "2.2.1", @@ -6536,6 +7281,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es-to-primitive": { @@ -6549,6 +7297,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/es5-ext": { @@ -6734,6 +7485,9 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-config-react-app": { @@ -6745,6 +7499,27 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^4.0.0", + "@typescript-eslint/parser": "^4.0.0", + "babel-eslint": "^10.0.0", + "eslint": "^7.5.0", + "eslint-plugin-flowtype": "^5.2.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-jest": "^24.0.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.8", + "eslint-plugin-testing-library": "^3.9.0" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + }, + "eslint-plugin-testing-library": { + "optional": true + } } }, "node_modules/eslint-import-resolver-node": { @@ -6876,6 +7651,9 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.1.0" } }, "node_modules/eslint-plugin-import": { @@ -6899,6 +7677,9 @@ }, "engines": { "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -6935,6 +7716,15 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": ">= 4", + "eslint": ">=5" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } } }, "node_modules/eslint-plugin-jsx-a11y": { @@ -6956,6 +7746,9 @@ }, "engines": { "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { @@ -6983,6 +7776,9 @@ }, "engines": { "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -6991,6 +7787,9 @@ "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", "engines": { "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -7011,6 +7810,9 @@ "dependencies": { "is-core-module": "^2.2.0", "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/eslint-plugin-testing-library": { @@ -7023,6 +7825,9 @@ "engines": { "node": "^10.12.0 || >=12.0.0", "npm": ">=6" + }, + "peerDependencies": { + "eslint": "^5 || ^6 || ^7" } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/experimental-utils": { @@ -7038,6 +7843,13 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { @@ -7046,6 +7858,10 @@ "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { @@ -7064,6 +7880,15 @@ }, "engines": { "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { @@ -7075,6 +7900,10 @@ }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/eslint-plugin-testing-library/node_modules/eslint-visitor-keys": { @@ -7106,6 +7935,9 @@ }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { @@ -7138,6 +7970,14 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^7.0.0", + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/eslint-webpack-plugin/node_modules/schema-utils": { @@ -7151,6 +7991,10 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/eslint/node_modules/@babel/code-frame": { @@ -7170,6 +8014,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/eslint/node_modules/chalk": { @@ -7182,6 +8029,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/eslint/node_modules/color-convert": { @@ -7222,6 +8072,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/has-flag": { @@ -7284,6 +8137,9 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint/node_modules/which": { @@ -7540,6 +8396,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/expect/node_modules/color-convert": { @@ -7793,6 +8652,10 @@ "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", "dependencies": { "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/faye-websocket": { @@ -7840,6 +8703,13 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/file-loader/node_modules/schema-utils": { @@ -7853,6 +8723,10 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/file-uri-to-path": { @@ -7992,8 +8866,19 @@ "version": "1.13.3", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, "node_modules/for-in": { @@ -8297,6 +9182,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -8408,6 +9294,9 @@ "function-bind": "^1.1.1", "has": "^1.0.3", "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-own-enumerable-property-symbols": { @@ -8472,6 +9361,9 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -8531,6 +9423,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globule": { @@ -8595,6 +9490,7 @@ "version": "5.1.5", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -8641,7 +9537,10 @@ "node_modules/has-bigints": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/has-flag": { "version": "3.0.0", @@ -8657,6 +9556,9 @@ "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-unicode": { @@ -8738,7 +9640,21 @@ "node_modules/hash-base/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/hash.js": { "version": "1.1.7", @@ -8752,7 +9668,11 @@ "node_modules/hast-util-parse-selector": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, "node_modules/hastscript": { "version": "6.0.0", @@ -8764,6 +9684,10 @@ "hast-util-parse-selector": "^2.0.0", "property-information": "^5.0.0", "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/he": { @@ -8919,6 +9843,9 @@ }, "engines": { "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/html-webpack-plugin/node_modules/json5": { @@ -9210,7 +10137,21 @@ "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/iferr": { "version": "0.1.5", @@ -9228,7 +10169,11 @@ "node_modules/immer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" + "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, "node_modules/import-cwd": { "version": "2.1.0", @@ -9251,6 +10196,9 @@ }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/import-from": { @@ -9306,14 +10254,6 @@ "node": ">=0.8.19" } }, - "node_modules/indefinite-observable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", - "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", - "dependencies": { - "symbol-observable": "1.2.0" - } - }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -9430,7 +10370,11 @@ "node_modules/is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/is-alphanumerical": { "version": "1.0.4", @@ -9439,6 +10383,10 @@ "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/is-arguments": { @@ -9450,6 +10398,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-arrayish": { @@ -9460,7 +10411,10 @@ "node_modules/is-bigint": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", - "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==" + "integrity": "sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -9483,6 +10437,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-buffer": { @@ -9496,6 +10453,9 @@ "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-ci": { @@ -9528,6 +10488,9 @@ "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", "dependencies": { "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-descriptor": { @@ -9558,12 +10521,19 @@ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-decimal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/is-descriptor": { "version": "0.1.6", @@ -9603,6 +10573,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-extendable": { @@ -9627,6 +10600,9 @@ "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", "engines": { "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-fullwidth-code-point": { @@ -9659,7 +10635,11 @@ "node_modules/is-hexadecimal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/is-in-browser": { "version": "1.1.3", @@ -9677,6 +10657,9 @@ "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-number": { @@ -9693,6 +10676,9 @@ "integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-obj": { @@ -9767,6 +10753,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-regexp": { @@ -9804,6 +10793,9 @@ "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-symbol": { @@ -9815,6 +10807,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-typedarray": { @@ -9929,6 +10924,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/istanbul-lib-report/node_modules/semver": { @@ -10042,6 +11040,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/jest-changed-files/node_modules/get-stream": { @@ -10053,6 +11054,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-changed-files/node_modules/is-stream": { @@ -10155,6 +11159,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-circus/node_modules/chalk": { @@ -10167,6 +11174,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-circus/node_modules/color-convert": { @@ -10230,6 +11240,14 @@ }, "engines": { "node": ">= 10.14.2" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } } }, "node_modules/jest-config/node_modules/ansi-styles": { @@ -10241,6 +11259,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-config/node_modules/chalk": { @@ -10253,6 +11274,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-config/node_modules/color-convert": { @@ -10322,6 +11346,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-config/node_modules/read-pkg/node_modules/type-fest": { @@ -10366,6 +11393,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-diff/node_modules/chalk": { @@ -10378,6 +11408,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-diff/node_modules/color-convert": { @@ -10450,6 +11483,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-each/node_modules/chalk": { @@ -10462,6 +11498,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-each/node_modules/color-convert": { @@ -10603,6 +11642,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-jasmine2/node_modules/chalk": { @@ -10615,6 +11657,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-jasmine2/node_modules/color-convert": { @@ -10687,6 +11732,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-matcher-utils/node_modules/chalk": { @@ -10699,6 +11747,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-matcher-utils/node_modules/color-convert": { @@ -10764,6 +11815,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-message-util/node_modules/chalk": { @@ -10776,6 +11830,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-message-util/node_modules/color-convert": { @@ -10831,6 +11888,14 @@ "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", "engines": { "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, "node_modules/jest-regex-util": { @@ -10881,6 +11946,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-resolve/node_modules/chalk": { @@ -10893,6 +11961,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-resolve/node_modules/color-convert": { @@ -10944,6 +12015,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-resolve/node_modules/read-pkg/node_modules/type-fest": { @@ -11004,6 +12078,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-runner/node_modules/chalk": { @@ -11016,6 +12093,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-runner/node_modules/color-convert": { @@ -11085,6 +12165,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-runner/node_modules/read-pkg/node_modules/type-fest": { @@ -11155,6 +12238,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-runtime/node_modules/chalk": { @@ -11167,6 +12253,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-runtime/node_modules/color-convert": { @@ -11236,6 +12325,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-runtime/node_modules/read-pkg/node_modules/type-fest": { @@ -11312,6 +12404,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-snapshot/node_modules/chalk": { @@ -11324,6 +12419,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-snapshot/node_modules/color-convert": { @@ -11393,6 +12491,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-snapshot/node_modules/read-pkg/node_modules/type-fest": { @@ -11439,6 +12540,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-util/node_modules/chalk": { @@ -11451,6 +12555,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-util/node_modules/color-convert": { @@ -11513,6 +12620,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-validate/node_modules/chalk": { @@ -11525,6 +12635,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-validate/node_modules/color-convert": { @@ -11577,6 +12690,9 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "jest": "^26.0.0" } }, "node_modules/jest-watch-typeahead/node_modules/ansi-styles": { @@ -11588,6 +12704,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-watch-typeahead/node_modules/chalk": { @@ -11600,6 +12719,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-watch-typeahead/node_modules/color-convert": { @@ -11663,6 +12785,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-watcher/node_modules/chalk": { @@ -11675,6 +12800,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest-watcher/node_modules/color-convert": { @@ -11753,6 +12881,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest/node_modules/chalk": { @@ -11765,6 +12896,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/jest/node_modules/color-convert": { @@ -11889,6 +13023,14 @@ }, "engines": { "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, "node_modules/jsdom/node_modules/acorn": { @@ -11967,8 +13109,10 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "graceful-fs": "^4.1.6", "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, "node_modules/jsprim": { @@ -11986,82 +13130,85 @@ } }, "node_modules/jss": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss/-/jss-10.6.0.tgz", - "integrity": "sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.7.1.tgz", + "integrity": "sha512-5QN8JSVZR6cxpZNeGfzIjqPEP+ZJwJJfZbXmeABNdxiExyO+eJJDy6WDtqTf8SDKnbL5kZllEpAP71E/Lt7PXg==", "dependencies": { "@babel/runtime": "^7.3.1", "csstype": "^3.0.2", - "indefinite-observable": "^2.0.1", "is-in-browser": "^1.1.3", "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" } }, "node_modules/jss-plugin-camel-case": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz", - "integrity": "sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.7.1.tgz", + "integrity": "sha512-+ioIyWvmAfgDCWXsQcW1NMnLBvRinOVFkSYJUgewQ6TynOcSj5F1bSU23B7z0p1iqK0PPHIU62xY1iNJD33WGA==", "dependencies": { "@babel/runtime": "^7.3.1", "hyphenate-style-name": "^1.0.3", - "jss": "10.6.0" + "jss": "10.7.1" } }, "node_modules/jss-plugin-default-unit": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz", - "integrity": "sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.7.1.tgz", + "integrity": "sha512-tW+dfYVNARBQb/ONzBwd8uyImigyzMiAEDai+AbH5rcHg5h3TtqhAkxx06iuZiT/dZUiFdSKlbe3q9jZGAPIwA==", "dependencies": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "node_modules/jss-plugin-global": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz", - "integrity": "sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.7.1.tgz", + "integrity": "sha512-FbxCnu44IkK/bw8X3CwZKmcAnJqjAb9LujlAc/aP0bMSdVa3/MugKQRyeQSu00uGL44feJJDoeXXiHOakBr/Zw==", "dependencies": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "node_modules/jss-plugin-nested": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz", - "integrity": "sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.7.1.tgz", + "integrity": "sha512-RNbICk7FlYKaJyv9tkMl7s6FFfeLA3ubNIFKvPqaWtADK0KUaPsPXVYBkAu4x1ItgsWx67xvReMrkcKA0jSXfA==", "dependencies": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0", + "jss": "10.7.1", "tiny-warning": "^1.0.2" } }, "node_modules/jss-plugin-props-sort": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz", - "integrity": "sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.7.1.tgz", + "integrity": "sha512-eyd5FhA+J0QrpqXxO7YNF/HMSXXl4pB0EmUdY4vSJI4QG22F59vQ6AHtP6fSwhmBdQ98Qd9gjfO+RMxcE39P1A==", "dependencies": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "node_modules/jss-plugin-rule-value-function": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz", - "integrity": "sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.7.1.tgz", + "integrity": "sha512-fGAAImlbaHD3fXAHI3ooX6aRESOl5iBt3LjpVjxs9II5u9tzam7pqFUmgTcrip9VpRqYHn8J3gA7kCtm8xKwHg==", "dependencies": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0", + "jss": "10.7.1", "tiny-warning": "^1.0.2" } }, "node_modules/jss-plugin-vendor-prefixer": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz", - "integrity": "sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.7.1.tgz", + "integrity": "sha512-1UHFmBn7hZNsHXTkLLOL8abRl8vi+D1EVzWD4WmLFj55vawHZfnH1oEz6TUf5Y61XHv0smdHabdXds6BgOXe3A==", "dependencies": { "@babel/runtime": "^7.3.1", "css-vendor": "^2.0.8", - "jss": "10.6.0" + "jss": "10.7.1" } }, "node_modules/jsx-ast-utils": { @@ -12280,6 +13427,10 @@ "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", "engines": { "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" } }, "node_modules/loose-envify": { @@ -12325,6 +13476,10 @@ "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/lru-cache": { @@ -12744,6 +13899,13 @@ }, "engines": { "node": ">= 6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.4.0 || ^5.0.0" } }, "node_modules/mini-css-extract-plugin/node_modules/json5": { @@ -13205,6 +14367,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-5.0.0.tgz", "integrity": "sha512-opNgmlu83ZCF792U281Ry7tak9IbVC+AKnXGovcQ8LG8wFaJv6cLnRlc6DIHlmNxWEexB5bZxi9SZ9JyUuOYjw==", + "hasInstallScript": true, "dependencies": { "async-foreach": "^0.1.3", "chalk": "^1.1.1", @@ -13507,7 +14670,10 @@ "node_modules/object-inspect": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", - "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" + "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/object-is": { "version": "1.1.5", @@ -13519,6 +14685,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-keys": { @@ -13552,6 +14721,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.entries": { @@ -13580,6 +14752,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.getownpropertydescriptors": { @@ -13593,6 +14768,9 @@ }, "engines": { "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object.pick": { @@ -13618,6 +14796,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/obuf": { @@ -13661,6 +14842,9 @@ }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/open": { @@ -13673,6 +14857,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/opn": { @@ -13701,6 +14888,9 @@ "dependencies": { "cssnano": "^4.1.10", "last-call-webpack-plugin": "^3.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" } }, "node_modules/optionator": { @@ -13738,6 +14928,9 @@ "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-finally": { @@ -13757,6 +14950,9 @@ }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { @@ -13779,6 +14975,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-retry": { @@ -13885,6 +15084,10 @@ "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/parse-json": { @@ -13899,6 +15102,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parse5": { @@ -14019,6 +15225,9 @@ "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", "engines": { "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pify": { @@ -14221,6 +15430,10 @@ }, "engines": { "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-attribute-case-insensitive": { @@ -14241,6 +15454,9 @@ }, "engines": { "node": ">=8.0.0" + }, + "peerDependencies": { + "browserslist": "^4" } }, "node_modules/postcss-calc": { @@ -14607,6 +15823,10 @@ }, "engines": { "node": ">= 4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-load-config/node_modules/cosmiconfig": { @@ -15279,6 +16499,10 @@ }, "engines": { "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-safe-parser/node_modules/postcss": { @@ -15292,6 +16516,10 @@ }, "engines": { "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-safe-parser/node_modules/source-map": { @@ -15422,6 +16650,9 @@ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pretty-error": { @@ -15456,6 +16687,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/pretty-format/node_modules/color-convert": { @@ -15549,6 +16783,10 @@ "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", "dependencies": { "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/protobuf-decoder": { @@ -15665,6 +16903,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "engines": { "node": ">=0.4.x" } @@ -15685,7 +16924,21 @@ "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/raf": { "version": "3.4.1", @@ -15777,6 +17030,9 @@ "dependencies": { "copy-to-clipboard": "^3", "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/react-dev-utils": { @@ -15836,6 +17092,10 @@ }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" } }, "node_modules/react-dev-utils/node_modules/cross-spawn": { @@ -15873,6 +17133,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-dev-utils/node_modules/path-key": { @@ -15924,6 +17187,9 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" } }, "node_modules/react-error-overlay": { @@ -16016,6 +17282,15 @@ }, "optionalDependencies": { "fsevents": "^2.1.3" + }, + "peerDependencies": { + "react": ">= 16", + "typescript": "^3.2.1 || ^4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/react-scrollable-feed": { @@ -16025,6 +17300,11 @@ "engines": { "node": ">=8", "npm": ">=5" + }, + "peerDependencies": { + "prop-types": "^15.7.2", + "react": "^15.0.0 || ^16.0.0", + "react-dom": "^15.0.0 || ^16.0.0" } }, "node_modules/react-syntax-highlighter": { @@ -16037,6 +17317,9 @@ "lowlight": "^1.17.0", "prismjs": "^1.22.0", "refractor": "^3.2.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" } }, "node_modules/react-transition-group": { @@ -16048,6 +17331,10 @@ "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/read-pkg": { @@ -16211,6 +17498,10 @@ "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.23.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/regenerate": { @@ -16269,6 +17560,9 @@ }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/regexpp": { @@ -16277,6 +17571,9 @@ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" } }, "node_modules/regexpu-core": { @@ -16394,6 +17691,7 @@ "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -16429,12 +17727,16 @@ }, "engines": { "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" } }, "node_modules/request-promise-native": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", "dependencies": { "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", @@ -16442,6 +17744,9 @@ }, "engines": { "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" } }, "node_modules/request-promise-native/node_modules/tough-cookie": { @@ -16472,6 +17777,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "bin": { "uuid": "bin/uuid" } @@ -16509,6 +17815,9 @@ "dependencies": { "is-core-module": "^2.0.0", "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-cwd": { @@ -16541,7 +17850,8 @@ "node_modules/resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "deprecated": "https://github.com/lydell/resolve-url#deprecated" }, "node_modules/resolve-url-loader": { "version": "3.1.2", @@ -16698,6 +18008,9 @@ }, "bin": { "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ripemd160": { @@ -16726,9 +18039,14 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-4.4.0.tgz", "integrity": "sha512-Lek/TYp1+7g7I+uMfJnnSJ7YWoD58ajo6Oarhlex7lvUce+RCKRuGRSgztDO3/MF/PuGKmUL5iTHKf208UNszw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-babel.", "dependencies": { "@babel/helper-module-imports": "^7.0.0", "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "@babel/core": "7 || ^7.0.0-rc.2", + "rollup": ">=0.60.0 <3" } }, "node_modules/rollup-plugin-terser": { @@ -16741,6 +18059,9 @@ "rollup-pluginutils": "^2.8.2", "serialize-javascript": "^4.0.0", "terser": "^4.6.2" + }, + "peerDependencies": { + "rollup": ">=0.66.0 <3" } }, "node_modules/rollup-plugin-terser/node_modules/jest-worker": { @@ -16799,6 +18120,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { "queue-microtask": "^1.2.2" } @@ -16833,6 +18168,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", "dependencies": { "@cnakazawa/watch": "^1.0.3", "anymatch": "^2.0.0", @@ -16998,6 +18334,9 @@ "lodash": "^4.0.0", "scss-tokenizer": "^0.2.3", "yargs": "^13.3.2" + }, + "bin": { + "sassgraph": "bin/sassgraph" } }, "node_modules/sass-graph/node_modules/ansi-regex": { @@ -17157,6 +18496,27 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0", + "sass": "^1.3.0", + "webpack": "^4.36.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } } }, "node_modules/sass-loader/node_modules/schema-utils": { @@ -17170,6 +18530,10 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/sax": { @@ -17208,6 +18572,10 @@ }, "engines": { "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/scss-tokenizer": { @@ -17467,6 +18835,9 @@ "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/signal-exit": { @@ -17511,6 +18882,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { @@ -17522,6 +18896,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/slice-ansi/node_modules/color-convert": { @@ -17709,6 +19086,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "bin": { "uuid": "bin/uuid" } @@ -17779,7 +19157,11 @@ "node_modules/space-separated-tokens": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/spdx-correct": { "version": "3.1.1", @@ -17868,6 +19250,11 @@ "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, "engines": { "node": ">=0.10.0" } @@ -18079,7 +19466,21 @@ "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/string-length": { "version": "4.0.2", @@ -18123,6 +19524,9 @@ "internal-slot": "^1.0.3", "regexp.prototype.flags": "^1.3.1", "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimend": { @@ -18132,6 +19536,9 @@ "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { @@ -18141,6 +19548,9 @@ "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/stringify-object": { @@ -18228,6 +19638,9 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/style-loader": { @@ -18240,6 +19653,13 @@ }, "engines": { "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/stylehacks": { @@ -18341,14 +19761,6 @@ "node": ">=4.0.0" } }, - "node_modules/symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -18382,6 +19794,10 @@ "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/table/node_modules/json-schema-traverse": { @@ -18463,6 +19879,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/terser": { @@ -18498,6 +19917,13 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/terser-webpack-plugin/node_modules/commander": { @@ -18516,6 +19942,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" } }, "node_modules/terser-webpack-plugin/node_modules/make-dir": { @@ -18527,6 +19956,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/terser-webpack-plugin/node_modules/p-limit": { @@ -18538,6 +19970,9 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { @@ -18562,6 +19997,10 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser-webpack-plugin/node_modules/semver": { @@ -18840,6 +20279,11 @@ "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", "engines": { "node": ">=6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/tsconfig-paths": { @@ -18878,6 +20322,9 @@ }, "engines": { "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "node_modules/tty-browserify": { @@ -18979,6 +20426,9 @@ "has-bigints": "^1.0.1", "has-symbols": "^1.0.2", "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -19153,7 +20603,8 @@ "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "deprecated": "Please see https://github.com/lydell/urix#deprecated" }, "node_modules/url": { "version": "0.11.0", @@ -19175,6 +20626,19 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } } }, "node_modules/url-loader/node_modules/schema-utils": { @@ -19188,6 +20652,10 @@ }, "engines": { "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/url-parse": { @@ -19208,6 +20676,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "engines": { "node": ">=0.4.x" } @@ -19242,6 +20711,9 @@ "es-abstract": "^1.17.2", "has-symbols": "^1.0.1", "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/util/node_modules/inherits": { @@ -19317,7 +20789,11 @@ "node_modules/vendors": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==" + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/verror": { "version": "1.10.0", @@ -19454,12 +20930,12 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", "optional": true, "dependencies": { "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", - "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", @@ -19468,6 +20944,9 @@ "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" } }, "node_modules/watchpack-chokidar2/node_modules/fill-range": { @@ -19501,6 +20980,8 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -19701,6 +21182,18 @@ }, "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + }, + "webpack-command": { + "optional": true + } } }, "node_modules/webpack-dev-middleware": { @@ -19716,6 +21209,9 @@ }, "engines": { "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, "node_modules/webpack-dev-middleware/node_modules/mime": { @@ -19773,6 +21269,14 @@ }, "engines": { "node": ">= 6.11.5" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, "node_modules/webpack-dev-server/node_modules/ansi-regex": { @@ -19854,6 +21358,7 @@ "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", "dependencies": { "anymatch": "^2.0.0", "async-each": "^1.0.1", @@ -19945,6 +21450,8 @@ "version": "1.2.13", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -20326,6 +21833,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "bin": { "uuid": "bin/uuid" } @@ -20342,6 +21850,9 @@ }, "engines": { "node": ">=6.11.5" + }, + "peerDependencies": { + "webpack": "2 || 3 || 4" } }, "node_modules/webpack-manifest-plugin/node_modules/fs-extra": { @@ -20361,7 +21872,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dependencies": { + "optionalDependencies": { "graceful-fs": "^4.1.6" } }, @@ -20646,6 +22157,9 @@ }, "engines": { "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" } }, "node_modules/webpack/node_modules/to-regex-range": { @@ -20738,6 +22252,9 @@ "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-module": { @@ -20879,7 +22396,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dependencies": { + "optionalDependencies": { "graceful-fs": "^4.1.6" } }, @@ -21000,6 +22517,9 @@ }, "engines": { "node": ">=8.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" } }, "node_modules/workbox-window": { @@ -21048,6 +22568,9 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/wrap-ansi/node_modules/color-convert": { @@ -21088,6 +22611,18 @@ "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "engines": { "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/xml-name-validator": { @@ -21173,6 +22708,9 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } }, @@ -22943,14 +24481,14 @@ } }, "@material-ui/core": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.3.tgz", - "integrity": "sha512-Adt40rGW6Uds+cAyk3pVgcErpzU/qxc7KBR94jFHBYretU4AtWZltYcNsbeMn9tXL86jjVL1kuGcIHsgLgFGRw==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.2.tgz", + "integrity": "sha512-Q1npB8V73IC+eV2X6as+g71MpEGQwqKHUI2iujY62npk35V8nMx/bUXAHjv5kKG1BZ8s8XUWoG6s/VkjYPjjQA==", "requires": { "@babel/runtime": "^7.4.4", - "@material-ui/styles": "^4.11.3", - "@material-ui/system": "^4.11.3", - "@material-ui/types": "^5.1.0", + "@material-ui/styles": "^4.11.4", + "@material-ui/system": "^4.12.1", + "@material-ui/types": "5.1.0", "@material-ui/utils": "^4.11.2", "@types/react-transition-group": "^4.2.0", "clsx": "^1.0.4", @@ -22961,14 +24499,26 @@ "react-transition-group": "^4.4.0" } }, + "@material-ui/lab": { + "version": "4.0.0-alpha.60", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz", + "integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + } + }, "@material-ui/styles": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.3.tgz", - "integrity": "sha512-HzVzCG+PpgUGMUYEJ2rTEmQYeonGh41BYfILNFb/1ueqma+p1meSdu4RX6NjxYBMhf7k+jgfHFTTz+L1SXL/Zg==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", + "integrity": "sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==", "requires": { "@babel/runtime": "^7.4.4", "@emotion/hash": "^0.8.0", - "@material-ui/types": "^5.1.0", + "@material-ui/types": "5.1.0", "@material-ui/utils": "^4.11.2", "clsx": "^1.0.4", "csstype": "^2.5.2", @@ -22992,9 +24542,9 @@ } }, "@material-ui/system": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", - "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.1.tgz", + "integrity": "sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==", "requires": { "@babel/runtime": "^7.4.4", "@material-ui/utils": "^4.11.2", @@ -23012,7 +24562,8 @@ "@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "requires": {} }, "@material-ui/utils": { "version": "4.11.2", @@ -23993,7 +25544,8 @@ "acorn-jsx": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==" + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -24037,12 +25589,14 @@ "ajv-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -24381,6 +25935,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.1.4.tgz", "integrity": "sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -24564,7 +26126,8 @@ "babel-plugin-named-asset-import": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", - "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==" + "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", + "requires": {} }, "babel-plugin-polyfill-corejs2": { "version": "0.2.0", @@ -27374,7 +28937,8 @@ "eslint-plugin-react-hooks": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", - "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==" + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "requires": {} }, "eslint-plugin-testing-library": { "version": "3.10.2", @@ -29161,14 +30725,6 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, - "indefinite-observable": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", - "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", - "requires": { - "symbol-observable": "1.2.0" - } - }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -30410,7 +31966,8 @@ "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==" + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "requires": {} }, "jest-regex-util": { "version": "26.0.0", @@ -31257,6 +32814,23 @@ "universalify": "^2.0.0" } }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -31269,82 +32843,81 @@ } }, "jss": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss/-/jss-10.6.0.tgz", - "integrity": "sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.7.1.tgz", + "integrity": "sha512-5QN8JSVZR6cxpZNeGfzIjqPEP+ZJwJJfZbXmeABNdxiExyO+eJJDy6WDtqTf8SDKnbL5kZllEpAP71E/Lt7PXg==", "requires": { "@babel/runtime": "^7.3.1", "csstype": "^3.0.2", - "indefinite-observable": "^2.0.1", "is-in-browser": "^1.1.3", "tiny-warning": "^1.0.2" } }, "jss-plugin-camel-case": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz", - "integrity": "sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.7.1.tgz", + "integrity": "sha512-+ioIyWvmAfgDCWXsQcW1NMnLBvRinOVFkSYJUgewQ6TynOcSj5F1bSU23B7z0p1iqK0PPHIU62xY1iNJD33WGA==", "requires": { "@babel/runtime": "^7.3.1", "hyphenate-style-name": "^1.0.3", - "jss": "10.6.0" + "jss": "10.7.1" } }, "jss-plugin-default-unit": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz", - "integrity": "sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.7.1.tgz", + "integrity": "sha512-tW+dfYVNARBQb/ONzBwd8uyImigyzMiAEDai+AbH5rcHg5h3TtqhAkxx06iuZiT/dZUiFdSKlbe3q9jZGAPIwA==", "requires": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "jss-plugin-global": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz", - "integrity": "sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.7.1.tgz", + "integrity": "sha512-FbxCnu44IkK/bw8X3CwZKmcAnJqjAb9LujlAc/aP0bMSdVa3/MugKQRyeQSu00uGL44feJJDoeXXiHOakBr/Zw==", "requires": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "jss-plugin-nested": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz", - "integrity": "sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.7.1.tgz", + "integrity": "sha512-RNbICk7FlYKaJyv9tkMl7s6FFfeLA3ubNIFKvPqaWtADK0KUaPsPXVYBkAu4x1ItgsWx67xvReMrkcKA0jSXfA==", "requires": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0", + "jss": "10.7.1", "tiny-warning": "^1.0.2" } }, "jss-plugin-props-sort": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz", - "integrity": "sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.7.1.tgz", + "integrity": "sha512-eyd5FhA+J0QrpqXxO7YNF/HMSXXl4pB0EmUdY4vSJI4QG22F59vQ6AHtP6fSwhmBdQ98Qd9gjfO+RMxcE39P1A==", "requires": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0" + "jss": "10.7.1" } }, "jss-plugin-rule-value-function": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz", - "integrity": "sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.7.1.tgz", + "integrity": "sha512-fGAAImlbaHD3fXAHI3ooX6aRESOl5iBt3LjpVjxs9II5u9tzam7pqFUmgTcrip9VpRqYHn8J3gA7kCtm8xKwHg==", "requires": { "@babel/runtime": "^7.3.1", - "jss": "10.6.0", + "jss": "10.7.1", "tiny-warning": "^1.0.2" } }, "jss-plugin-vendor-prefixer": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz", - "integrity": "sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.7.1.tgz", + "integrity": "sha512-1UHFmBn7hZNsHXTkLLOL8abRl8vi+D1EVzWD4WmLFj55vawHZfnH1oEz6TUf5Y61XHv0smdHabdXds6BgOXe3A==", "requires": { "@babel/runtime": "^7.3.1", "css-vendor": "^2.0.8", - "jss": "10.6.0" + "jss": "10.7.1" } }, "jsx-ast-utils": { @@ -34601,7 +36174,8 @@ "react-scrollable-feed": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/react-scrollable-feed/-/react-scrollable-feed-1.3.0.tgz", - "integrity": "sha512-ZoaWcrYlzNoGHNuYy//Wkz9jtFiy+pb8VvHzRVmthktU4cJAN4//aYzEIATyT+amAROL8qpZnoWUZJypDtmvfg==" + "integrity": "sha512-ZoaWcrYlzNoGHNuYy//Wkz9jtFiy+pb8VvHzRVmthktU4cJAN4//aYzEIATyT+amAROL8qpZnoWUZJypDtmvfg==", + "requires": {} }, "react-syntax-highlighter": { "version": "15.4.3", @@ -36204,6 +37778,69 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -36574,11 +38211,6 @@ "util.promisify": "~1.0.0" } }, - "symbol-observable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" - }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -37095,6 +38727,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", @@ -38874,7 +40511,8 @@ "ws": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/ui/package.json b/ui/package.json index 1b393ee13..b21551068 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@material-ui/core": "^4.11.3", + "@material-ui/lab": "^4.0.0-alpha.60", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^12.8.3", @@ -11,6 +12,8 @@ "@types/node": "^12.20.10", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", + "jsonpath": "^1.1.1", + "axios": "^0.21.1", "node-sass": "^5.0.0", "numeral": "^2.0.6", "protobuf-decoder": "^0.1.0", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 162e8c8b5..c30d09083 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,10 +1,12 @@ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import './App.sass'; import logo from './components/assets/Mizu-logo.svg'; -import {Button} from "@material-ui/core"; +import {Button, Snackbar} from "@material-ui/core"; import {HarPage} from "./components/HarPage"; import Tooltip from "./components/Tooltip"; import {makeStyles} from "@material-ui/core/styles"; +import MuiAlert from '@material-ui/lab/Alert'; +import Api from "./helpers/api"; const useStyles = makeStyles(() => ({ @@ -15,11 +17,37 @@ const useStyles = makeStyles(() => ({ }, })); +const api = new Api(); + const App = () => { const classes = useStyles(); const [analyzeStatus, setAnalyzeStatus] = useState(null); + const [showTLSWarning, setShowTLSWarning] = useState(false); + const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false); + const [addressesWithTLS, setAddressesWithTLS] = useState(new Set()); + + useEffect(() => { + (async () => { + const recentTLSLinks = await api.getRecentTLSLinks(); + + if (recentTLSLinks?.length > 0) { + setAddressesWithTLS(new Set([...addressesWithTLS, ...recentTLSLinks])); + setShowTLSWarning(true); + } + + })(); + }, []); + + const onTLSDetected = (destAddress: string) => { + addressesWithTLS.add(destAddress); + setAddressesWithTLS(new Set(addressesWithTLS)); + + if (!userDismissedTLSWarning) { + setShowTLSWarning(true); + } + }; const analysisMessage = analyzeStatus?.isRemoteReady ? @@ -88,7 +116,12 @@ const App = () => { } - + + + setUserDismissedTLSWarning(true)} severity="warning"> + Mizu is detecting TLS traffic{addressesWithTLS.size ? ` (directed to ${Array.from(addressesWithTLS).join(", ")})` : ''}, this type of traffic will not be displayed. + + ); } diff --git a/ui/src/components/HarEntriesList.tsx b/ui/src/components/HarEntriesList.tsx index f446d135d..cea3c2cef 100644 --- a/ui/src/components/HarEntriesList.tsx +++ b/ui/src/components/HarEntriesList.tsx @@ -4,6 +4,7 @@ import styles from './style/HarEntriesList.module.sass'; import spinner from './assets/spinner.svg'; import ScrollableFeed from "react-scrollable-feed"; import {StatusType} from "./HarFilters"; +import Api from "../helpers/api"; interface HarEntriesListProps { entries: any[]; @@ -25,6 +26,8 @@ enum FetchOperator { GT = "gt" } +const api = new Api(); + export const HarEntriesList: React.FC = ({entries, setEntries, focusedEntryId, setFocusedEntryId, connectionOpen, noMoreDataTop, setNoMoreDataTop, noMoreDataBottom, setNoMoreDataBottom, methodsFilter, statusFilter, pathFilter}) => { const [loadMoreTop, setLoadMoreTop] = useState(false); @@ -54,14 +57,9 @@ export const HarEntriesList: React.FC = ({entries, setEntri return entries.filter(filterEntries); },[entries, filterEntries]) - const fetchData = async (operator, timestamp) => { - const response = await fetch(`http://localhost:8899/api/entries?limit=50&operator=${operator}×tamp=${timestamp}`); - return await response.json(); - } - const getOldEntries = useCallback(async () => { setIsLoadingTop(true); - const data = await fetchData(FetchOperator.LT, entries[0].timestamp); + const data = await api.fetchEntries(FetchOperator.LT, entries[0].timestamp); setLoadMoreTop(false); let scrollTo; @@ -89,7 +87,7 @@ export const HarEntriesList: React.FC = ({entries, setEntri }, [loadMoreTop, connectionOpen, noMoreDataTop, getOldEntries]); const getNewEntries = async () => { - const data = await fetchData(FetchOperator.GT, entries[entries.length - 1].timestamp); + const data = await api.fetchEntries(FetchOperator.GT, entries[entries.length - 1].timestamp); let scrollTo; if(data.length === 0) { setNoMoreDataBottom(true); diff --git a/ui/src/components/HarEntry.tsx b/ui/src/components/HarEntry.tsx index 368ac92cb..f51a0bcf0 100644 --- a/ui/src/components/HarEntry.tsx +++ b/ui/src/components/HarEntry.tsx @@ -19,6 +19,13 @@ interface HAREntry { isCurrentRevision?: boolean; timestamp: Date; isOutgoing?: boolean; + latency: number; + rules: Rules; +} + +interface Rules { + status: boolean; + latency: number } interface HAREntryProps { @@ -48,9 +55,16 @@ export const HarEntry: React.FC = ({entry, setFocusedEntryId, isS break; } } - + let backgroundColor = ""; + if ('latency' in entry.rules) { + if (entry.rules.latency !== -1) { + backgroundColor = entry.rules.latency >= entry.latency ? styles.ruleSuccessRow : styles.ruleFailureRow + } else { + backgroundColor = entry.rules.status ? styles.ruleSuccessRow : styles.ruleFailureRow + } + } return <> -
setFocusedEntryId(entry.id)}> +
setFocusedEntryId(entry.id)}> {entry.statusCode &&
} diff --git a/ui/src/components/HarEntryDetailed.tsx b/ui/src/components/HarEntryDetailed.tsx index c8ee78f3b..8e86d140f 100644 --- a/ui/src/components/HarEntryDetailed.tsx +++ b/ui/src/components/HarEntryDetailed.tsx @@ -29,7 +29,7 @@ const HarEntryTitle: React.FC = ({har}) => { const classes = useStyles(); const {log: {entries}} = har; - const {response, request, timings: {receive}} = entries[0]; + const {response, request, timings: {receive}} = entries[0].entry; const {status, statusText, bodySize} = response; @@ -40,9 +40,10 @@ const HarEntryTitle: React.FC = ({har}) => {
-
{formatSize(bodySize)}
-
{status} {statusText}
-
{Math.round(receive)}ms
+
{formatSize(bodySize)}
+
{status} {statusText}
+
{Math.round(receive)}ms
+
{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied
; }; diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass b/ui/src/components/HarEntryViewer/HAREntrySections.module.sass index 8e56935cf..50bb0cc19 100644 --- a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass +++ b/ui/src/components/HarEntryViewer/HAREntrySections.module.sass @@ -40,6 +40,27 @@ width: 1% max-width: 15rem + .rulesTitleSuccess + color: #0C0B1A + + .rulesMatchedSuccess + background: #E8FFF1 + padding: 5px + border-radius: 4px + color: #219653 + font-style: normal + font-size: 0.7rem + font-weight: 600 + + .rulesMatchedFailure + background: #FFE9EF + padding: 5px + border-radius: 4px + color: #DB2156 + font-style: normal + font-size: 0.7rem + font-weight: 600 + .dataValue color: $blue-gray margin: 0 @@ -66,7 +87,6 @@ border-top: 1px solid $light-blue-color padding: 1rem background: none - table width: 100% tr td:first-child diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.tsx b/ui/src/components/HarEntryViewer/HAREntrySections.tsx index c73c08bb0..088dd3f51 100644 --- a/ui/src/components/HarEntryViewer/HAREntrySections.tsx +++ b/ui/src/components/HarEntryViewer/HAREntrySections.tsx @@ -5,6 +5,7 @@ import CollapsibleContainer from "../CollapsibleContainer"; import FancyTextDisplay from "../FancyTextDisplay"; import Checkbox from "../Checkbox"; import ProtobufDecoder from "protobuf-decoder"; +var jp = require('jsonpath'); interface HAREntryViewLineProps { label: string; @@ -144,3 +145,122 @@ export const HAREntryTableSection: React.FC = ({title, arr } } + + + +interface HAREntryPolicySectionProps { + service: string, + title: string, + response: any, + latency?: number, + arrayToIterate: any[], +} + + +interface HAREntryPolicySectionCollapsibleTitleProps { + label: string; + matched: string; + isExpanded: boolean; +} + +const HAREntryPolicySectionCollapsibleTitle: React.FC = ({label, matched, isExpanded}) => { + return
+ + {isExpanded ? '-' : '+'} + + + + {label} + {matched} + + +
+} + +interface HAREntryPolicySectionContainerProps { + label: string; + matched: string; + children?: any; +} + +export const HAREntryPolicySectionContainer: React.FC = ({label, matched, children}) => { + const [expanded, setExpanded] = useState(false); + return setExpanded(!expanded)} + title={} + > + {children} + +} + +export const HAREntryTablePolicySection: React.FC = ({service, title, response, latency, arrayToIterate}) => { + const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {}; + return + { + arrayToIterate && arrayToIterate.length > 0 ? + <> + + + + {arrayToIterate.map(({rule, matched}, index) => { + + + return ( + = latency : true)? "Success" : "Failure"}> + { + + <> + { + rule.Key != "" ? + + : null + } + { + rule.Latency != "" ? + + : null + } + { + rule.Method != "" ? + + : null + } + { + rule.Path != "" ? + + : null + } + { + rule.Service != "" ? + + : null + } + { + rule.Type != "" ? + + : null + } + { + rule.Value != "" ? + + : null + } + + } + + + + ) + } + ) + } + +
Key:{rule.Key}
Latency: {rule.Latency}
Method: {rule.Method}
Path: {rule.Path}
Service: {service}
Type: {rule.Type}
Value: {rule.Value}
+
+ + : + } +
+} \ No newline at end of file diff --git a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx b/ui/src/components/HarEntryViewer/HAREntryViewer.tsx index 801d5cb2d..e0450e1e7 100644 --- a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx +++ b/ui/src/components/HarEntryViewer/HAREntryViewer.tsx @@ -1,19 +1,22 @@ import React, {useState} from 'react'; import styles from './HAREntryViewer.module.sass'; import Tabs from "../Tabs"; -import {HAREntryTableSection, HAREntryBodySection} from "./HAREntrySections"; +import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections"; const MIME_TYPE_KEY = 'mimeType'; -const HAREntryDisplay: React.FC = ({entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => { - const {request, response} = entry; - +const HAREntryDisplay: React.FC = ({har, entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => { + const {request, response, timings: {receive}} = entry; + const rulesMatched = har.log.entries[0].rulesMatched const TABS = [ {tab: 'request'}, { tab: 'response', badge: <>{isResponseMocked && MOCK} }, + { + tab: 'Rules', + }, ]; const [currentTab, setCurrentTab] = useState(TABS[0].tab); @@ -43,6 +46,9 @@ const HAREntryDisplay: React.FC = ({entry, isCollapsed: initialIsCollapsed, } + {currentTab === TABS[2].tab && + + }
} ; } @@ -58,7 +64,7 @@ const HAREntryViewer: React.FC = ({harObject, className, isResponseMocked const {log: {entries}} = harObject; const isCollapsed = entries.length > 1; return
- {Object.keys(entries).map((entry: any, index) => )} + {Object.keys(entries).map((entry: any, index) => )}
}; diff --git a/ui/src/components/HarPage.tsx b/ui/src/components/HarPage.tsx index f9d32fdc5..bfa7dbfb3 100644 --- a/ui/src/components/HarPage.tsx +++ b/ui/src/components/HarPage.tsx @@ -9,6 +9,7 @@ import playIcon from './assets/run.svg'; import pauseIcon from './assets/pause.svg'; import variables from './style/variables.module.scss'; import {StatusBar} from "./StatusBar"; +import Api, {MizuWebsocketURL} from "../helpers/api"; const useLayoutStyles = makeStyles(() => ({ details: { @@ -37,25 +38,12 @@ enum ConnectionStatus { interface HarPageProps { setAnalyzeStatus: (status: any) => void; + onTLSDetected: (destAddress: string) => void; } -const mizuAPIPathPrefix = "/mizu"; +const api = new Api(); - -// When working locally (with npm run start) we need to change the PORT -const getMizuApiUrl = () => { - return `${window.location.origin}${mizuAPIPathPrefix}`; -}; - -const getMizuWebsocketUrl = () => { - return `ws://${window.location.host}${mizuAPIPathPrefix}/ws`; -} - - -const mizuApiUrl = getMizuApiUrl(); -const mizuWebsocketUrl = getMizuWebsocketUrl(); - -export const HarPage: React.FC = ({setAnalyzeStatus}) => { +export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected}) => { const classes = useLayoutStyles(); @@ -75,7 +63,7 @@ export const HarPage: React.FC = ({setAnalyzeStatus}) => { const ws = useRef(null); const openWebSocket = () => { - ws.current = new WebSocket(mizuWebsocketUrl); + ws.current = new WebSocket(MizuWebsocketURL); ws.current.onopen = () => setConnection(ConnectionStatus.Connected); ws.current.onclose = () => setConnection(ConnectionStatus.Closed); } @@ -84,7 +72,6 @@ export const HarPage: React.FC = ({setAnalyzeStatus}) => { ws.current.onmessage = e => { if (!e?.data) return; const message = JSON.parse(e.data); - switch (message.messageType) { case "entry": const entry = message.data @@ -106,6 +93,9 @@ export const HarPage: React.FC = ({setAnalyzeStatus}) => { case "analyzeStatus": setAnalyzeStatus(message.analyzeStatus); break + case "outboundLink": + onTLSDetected(message.Data.DstIP); + break; default: console.error(`unsupported websocket message type, Got: ${message.messageType}`) } @@ -113,24 +103,32 @@ export const HarPage: React.FC = ({setAnalyzeStatus}) => { } useEffect(() => { - openWebSocket(); - fetch(`${mizuApiUrl}/api/tapStatus`) - .then(response => response.json()) - .then(data => setTappingStatus(data)); - - fetch(`${mizuApiUrl}/api/analyzeStatus`) - .then(response => response.json()) - .then(data => setAnalyzeStatus(data)); + (async () => { + openWebSocket(); + try{ + const tapStatusResponse = await api.tapStatus(); + setTappingStatus(tapStatusResponse); + const analyzeStatusResponse = await api.analyzeStatus(); + setAnalyzeStatus(analyzeStatusResponse); + } catch (error) { + console.error(error); + } + })() // eslint-disable-next-line }, []); useEffect(() => { if (!focusedEntryId) return; - setSelectedHarEntry(null) - fetch(`${mizuApiUrl}/api/entries/${focusedEntryId}`) - .then(response => response.json()) - .then(data => setSelectedHarEntry(data)); + setSelectedHarEntry(null); + (async () => { + try { + const entryData = await api.getEntry(focusedEntryId); + setSelectedHarEntry(entryData); + } catch (error) { + console.error(error); + } + })() }, [focusedEntryId]) const toggleConnection = () => { diff --git a/ui/src/components/StatusBar.tsx b/ui/src/components/StatusBar.tsx index 0fb25b1e4..5fe6d2738 100644 --- a/ui/src/components/StatusBar.tsx +++ b/ui/src/components/StatusBar.tsx @@ -29,12 +29,14 @@ export const StatusBar: React.FC = ({tappingStatus}) => {
{`Tapping ${amountOfPods} ${pluralize('pod', amountOfPods)} in ${pluralize('namespace', uniqueNamespaces.length)} ${uniqueNamespaces.join(", ")}`}
{expandedBar &&
- - - - + + + + + + - {tappingStatus.pods.map(pod => + {tappingStatus.pods.map(pod => )} diff --git a/ui/src/components/style/HarEntry.module.sass b/ui/src/components/style/HarEntry.module.sass index fc5442f48..5425b1786 100644 --- a/ui/src/components/style/HarEntry.module.sass +++ b/ui/src/components/style/HarEntry.module.sass @@ -23,6 +23,14 @@ margin-left: 10px margin-right: 3px +.ruleSuccessRow + border: 1px $success-color solid + border-left: 5px $success-color solid + +.ruleFailureRow + border: 1px $failure-color solid + border-left: 5px $failure-color solid + .service text-overflow: ellipsis overflow: hidden diff --git a/ui/src/helpers/api.js b/ui/src/helpers/api.js new file mode 100644 index 000000000..ca0044123 --- /dev/null +++ b/ui/src/helpers/api.js @@ -0,0 +1,50 @@ +import * as axios from "axios"; + +const mizuAPIPathPrefix = "/mizu"; + +// When working locally (with npm run start) change to: +// export const MizuWebsocketURL = `ws://localhost:8899${mizuAPIPathPrefix}/ws`; +export const MizuWebsocketURL = `ws://${window.location.host}${mizuAPIPathPrefix}/ws`; + +export default class Api { + + constructor() { + + // When working locally (with npm run start) change to: + // const apiURL = `http://localhost:8899/${mizuAPIPathPrefix}/api/`; + const apiURL = `${window.location.origin}${mizuAPIPathPrefix}/api/`; + + this.client = axios.create({ + baseURL: apiURL, + timeout: 31000, + headers: { + Accept: "application/json", + } + }); + } + + tapStatus = async () => { + const response = await this.client.get("/tapStatus"); + return response.data; + } + + analyzeStatus = async () => { + const response = await this.client.get("/analyzeStatus"); + return response.data; + } + + getEntry = async (entryId) => { + const response = await this.client.get(`/entries/${entryId}`); + return response.data; + } + + fetchEntries = async (operator, timestamp) => { + const response = await this.client.get(`/entries?limit=50&operator=${operator}×tamp=${timestamp}`); + return response.data; + } + + getRecentTLSLinks = async () => { + const response = await this.client.get("/recentTLSLinks"); + return response.data; + } +}
Pod nameNamespace
Pod nameNamespace
{pod.name} {pod.namespace}